Expo 아이콘을 탭바와 버튼에 적용할 때 패키지와 폰트 로딩을 어떻게 구분하나요?
아이콘은 이름·크기·색을 전달해 표시하며 상태가 바뀌면 이름과 접근 가능한 설명을 함께 바꿉니다. 새 예제 패키지와 기존 @expo/vector-icons는 구분해 설치·폰트 로딩 조건을 확인합니다.
적용 조건·범위: 정리 기준은 단순합니다. 아래 @expo/vector-icons 코드는 기존 프로젝트를 읽기 위한 이전 방식입니다. 새 프로젝트의 권장 경로는 이 글의 독립 실습에 둡니다. 반대로 Expo가 아닌 순수 React Native 환경이라면 같은 계열의 다른 패키지를 직접 다루는 쪽이 더 자연스럽습니다. 이 차이를 먼저 잡아두면 블로그 글마다 import 방식이 조금씩 달라 보여도 왜 그런지 맥락이 보입니다.
본문 근거: 해당 기능의 Expo 공식 문서 · 공식 이전 안내
이 글에서 정리하는 내용
아이콘 컴포넌트에는 세트에서 확인한 name, size, color를 전달하고 상태에 따라 이름을 바꿉니다. 새 독립 실습은 @react-native-vector-icons/ionicons로 저장 버튼의 그림과 접근성 이름을 함께 전환합니다. 기존 @expo/vector-icons 코드는 이전 프로젝트의 사용법으로 구분해 읽으며, 탭 적용과 커스텀 자산은 별도 준비가 필요합니다.
목차
@expo/vector-icons가 무엇인지

@expo/vector-icons는 아이콘 폰트를 React 컴포넌트로 사용하는 인터페이스입니다. 겉으로 보면 그냥 아이콘 컴포넌트 모음처럼 보이지만, 실제로는 Expo 환경에서 아이콘 폰트를 다루기 쉽게 만든 호환 레이어에 가깝습니다. 그래서 단순히 그림 하나를 넣는 도구로 보기보다, Expo 앱 안에서 아이콘 폰트를 불러오고 렌더링하는 기본 진입점으로 이해해두는 편이 훨씬 덜 헷갈립니다.
정리 기준은 단순합니다. 아래 @expo/vector-icons 코드는 기존 프로젝트를 읽기 위한 이전 방식입니다. 새 프로젝트의 권장 경로는 이 글의 독립 실습에 둡니다. 반대로 Expo가 아닌 순수 React Native 환경이라면 같은 계열의 다른 패키지를 직접 다루는 쪽이 더 자연스럽습니다. 이 차이를 먼저 잡아두면 블로그 글마다 import 방식이 조금씩 달라 보여도 왜 그런지 맥락이 보입니다.
패키지의 위치를 먼저 이해하기
코드 위치: src/components/IconPreview.tsx. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.
// Expo 프로젝트에서는 아이콘 세트를 바로 가져와 쓸 수 있습니다.
import Ionicons from '@expo/vector-icons/Ionicons';
export default function IconPreview() {
return <Ionicons name="checkmark-circle" size={28} color="#16a34a" />;
}
이 코드를 보면 컴포넌트 하나를 import 해서 바로 쓰는 단순한 구조처럼 보입니다. 하지만 내부적으로는 아이콘 폰트 자산과 연결되어 있기 때문에, 웹의 일반 이미지 태그를 쓰는 감각과는 다릅니다. 결국 이름이 맞는 글리프를 폰트에서 찾아 그려주는 방식이라고 이해하면 전체 구조가 정리됩니다.
가장 기본적인 사용법
기본 사용법 자체는 어렵지 않습니다. 보통은 Ionicons, MaterialIcons, FontAwesome 같은 세트 중 하나를 import 하고, name, size, color를 넘겨 렌더링하면 끝입니다. 다만 실무에서는 여기서 끝나지 않고, 어떤 세트를 고를지와 아이콘 이름을 어디서 찾을지를 함께 알아야 작업 속도가 올라갑니다.
공식 문서 기준으로 @expo/vector-icons는 create-expo-app 템플릿에서 기본 설치 상태로 시작하며, 인기 아이콘 세트를 포함하고 있어 icons.expo.fyi에서 이름을 검색해 바로 가져다 쓰는 흐름이 가장 자연스럽습니다. 그래서 @expo/vector-icons를 쓸 때는 먼저 세트를 정하고, 그다음 이름을 정확히 확인하는 순서가 실무에서 가장 덜 흔들립니다.
name, size, color를 읽는 법
코드 위치: src/components/BasicIcons.tsx. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.
import { View } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function BasicIcons() {
return (
<View style={{ flexDirection: 'row', gap: 12 }}>
{/* 홈 아이콘 */} <Ionicons name="home-outline" size={24} color="#111827" />
{/* 검색 아이콘 */} <Ionicons name="search" size={24} color="#111827" />
{/* 활성 상태를 표현하는 하트 아이콘 */}
<Ionicons name="heart" size={24} color="#ef4444" />
</View>
);
}
여기서 가장 중요한 값은 name입니다. 아이콘 세트가 같아도 이름이 정확히 맞지 않으면 아무것도 안 보이거나 경고가 날 수 있습니다. 그래서 실무에서는 먼저 디자인이 아니라 아이콘 세트와 이름을 확정한 뒤 코드를 붙이는 흐름이 더 안정적입니다.
아이콘 이름을 찾을 때는 세트별 디렉터리에서 먼저 검색하는 습관이 좋습니다. 같은 의미의 아이콘이라도 세트마다 이름과 스타일이 다르고, outline 버전과 filled 버전이 따로 있는 경우도 많기 때문입니다. 이 단계에서 이름을 확실히 맞춰두면 뒤에서 탭바나 버튼을 만들 때 시행착오가 크게 줄어듭니다.
| 항목 | 실무에서 보는 기준 |
|---|---|
| 아이콘 세트 | 앱 전체 톤을 맞추기 위해 한두 개 세트로 좁혀서 쓰는 편이 관리가 쉽습니다. |
| 아이콘 이름 | 추측해서 쓰기보다 디렉터리에서 정확한 이름을 확인해야 오류를 줄일 수 있습니다. |
실무에서 자주 쓰는 패턴
실제로 많이 만나는 위치는 정해져 있습니다. 하단 탭바, 헤더 우측 액션, 일반 버튼 왼쪽 아이콘, 목록 아이템 상태 표시가 대표적입니다. 이 지점에서는 아이콘 하나를 예쁘게 띄우는 것보다, 상태에 따라 바뀌는 규칙을 만드는 쪽이 더 중요합니다.
Expo Router 탭에서 아이콘 연결하기
코드 위치: src/app/(tabs)/_layout.tsx. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.
import { Tabs } from 'expo-router';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function TabLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: '#111827' }}>
<Tabs.Screen
name="index"
options={{
title: '홈',
tabBarIcon: ({ color, size, focused }) => (
<Ionicons
name={focused ? 'home' : 'home-outline'}
size={size}
color={color}
/>
),
}}
/>
<Tabs.Screen
name="settings"
options={{
title: '설정',
tabBarIcon: ({ color, size, focused }) => (
<Ionicons
name={focused ? 'settings' : 'settings-outline'}
size={size}
color={color}
/>
),
}}
/>
</Tabs>
);
}
탭바에서는 특히 focused 값을 활용한 상태 분기가 자주 쓰입니다. 같은 탭이어도 활성화 여부에 따라 outline과 filled 아이콘을 바꾸면 시각적 피드백이 훨씬 분명해집니다. 이 패턴은 북마크, 좋아요, 알림처럼 상태가 바뀌는 요소에도 그대로 응용할 수 있습니다.
아이콘이 들어간 버튼 만들기
src/components/FavoriteButton.tsx 전체 파일입니다. 아이콘 버튼은 그림 외에 접근 가능한 이름과 실제 동작이 필요합니다. 아래 저장 상태는 메모리 상태이므로 앱을 재시작하면 초기화됩니다. 이 버튼은 소셜 로그인을 수행하지 않습니다.
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function FavoriteButton() {
const [selected, setSelected] = useState(false);
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={selected ? '즐겨찾기 해제' : '즐겨찾기 추가'}
accessibilityState={{ selected }}
onPress={() => setSelected((value) => !value)}
style={{ flexDirection: 'row', gap: 8, padding: 16 }}
>
<Ionicons name={selected ? 'heart' : 'heart-outline'} size={24} color="#be123c" />
<Text>{selected ? '저장됨' : '저장'}</Text>
</Pressable>
);
}
Pressable이 터치 영역과 접근 가능한 이름을, Ionicons가 시각적 상태를 맡습니다. selected 상태가 바뀌면 아이콘과 텍스트가 함께 갱신됩니다. 화면에서 버튼을 렌더링하고 두 번 눌러 추가·해제 상태가 왕복하는지 확인하세요.
폰트 preload와 커스텀 아이콘
npx expo install @expo/vector-icons expo-font로 SDK 호환 의존성을 설치합니다. 다음은 src/components/IconPreview.tsx 전체 파일이며 Router의 _layout.tsx를 대체하지 않습니다. 레이아웃에서 글꼴을 준비한다면 로딩 후 Stack 또는 Slot을 반드시 렌더링하세요.

import { Text } from 'react-native';
import { useFonts } from 'expo-font';
import Ionicons from '@expo/vector-icons/Ionicons';
export default function IconPreview() {
const [loaded, error] = useFonts(Ionicons.font);
if (error) return <Text>아이콘을 불러오지 못했습니다.</Text>;
if (!loaded) return <Text>아이콘 준비 중</Text>;
return <Ionicons name="home" size={24} color="#111827" />;
}
처음 앱이 열릴 때 아이콘이 잠깐 비어 보이는 현상을 만날 수 있습니다. 이때는 아이콘도 결국 폰트 자산이라는 점을 떠올리면 이유가 보입니다. 첫 로드에서 깔끔하게 보이게 하려면 루트 레이아웃에서 미리 폰트를 불러오는 방식을 고려하는 편이 안전합니다.
여기서 한 단계 더 가면 커스텀 아이콘으로 확장할 수 있습니다. 기본 제공 세트로 대부분 해결되지만, 브랜드 전용 심볼이나 서비스 전용 아이콘이 필요하면 IcoMoon이나 Fontello로 만든 폰트 파일과 설정 파일을 프로젝트에 넣고, @expo/vector-icons가 노출하는 createIconSet 계열 함수로 새로운 아이콘 컴포넌트를 만들 수 있습니다. 이 흐름은 단순히 아이콘을 바꾸는 수준이 아니라, 디자인 시스템을 앱 안으로 가져오는 작업에 가깝습니다.
커스텀 아이콘 세트의 흐름
아래는 프로젝트 루트 BrandIconExample.tsx에 놓는 자산 연결 예제입니다. assets/icomoon/selection.json과 assets/icomoon/icomoon.ttf는 동일한 IcoMoon 내보내기에서 직접 준비해야 합니다. 이 글은 해당 커스텀 파일을 제공하지 않으므로 아래만 복사해서 실행되는 완성 예제가 아닙니다. JSON의 glyph 이름이 logo-symbol인지, 폰트 family가 IcoMoon인지 먼저 확인하세요. 원문의 createIconSetFromIcoMoon 예제를 사용할 때는 설치된 @expo/vector-icons가 해당 selection.json 형식을 지원하는지도 확인합니다. 현재 Expo 가이드는 이전 IcoMoon 앱 출력과 새 앱의 JSON 형식을 구분하므로 파일명만 맞춘다고 호환되는 것은 아닙니다.
코드 위치: BrandIconExample.tsx. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.
import { useFonts } from 'expo-font';
import createIconSetFromIcoMoon from '@expo/vector-icons/createIconSetFromIcoMoon';
const BrandIcon = createIconSetFromIcoMoon(
require('./assets/icomoon/selection.json'),
'IcoMoon',
'icomoon.ttf',
);
export default function BrandIconExample() {
const [fontsLoaded, fontError] = useFonts({
IcoMoon: require('./assets/icomoon/icomoon.ttf'),
});
if (fontError) throw fontError;
if (!fontsLoaded) {
return null;
}
return <BrandIcon name="logo-symbol" size={28} color="#2563eb" />;
}
이 패턴은 앱 로고 심볼, 브랜드 전용 배지, 서비스 내부 기능 아이콘처럼 범용 세트에 없는 요소를 통일감 있게 넣고 싶을 때 유용합니다. 다만 여기까지 갈 필요가 없는 프로젝트도 많습니다. 기본 세트로 충분한데 커스텀 폰트까지 도입하면 관리 대상만 늘어날 수 있으니, 정말 반복 사용되는 아이콘 체계가 필요한지 먼저 판단하는 편이 좋습니다.
결론
@expo/vector-icons는 Expo에서 아이콘을 가장 빠르게 붙이는 도구이면서, 동시에 아이콘 폰트 시스템을 이해하는 출발점이기도 합니다. 처음에는 Ionicons 하나만 가져와 써도 충분하지만, 실무로 갈수록 세트 선택, 이름 관리, 상태별 아이콘 분기, 첫 로드 preload, 커스텀 아이콘 확장까지 자연스럽게 이어집니다. 기본 세트로 시작하고, 자주 쓰는 패턴을 공용화하고, 정말 필요할 때만 커스텀 아이콘으로 확장하면 됩니다.
많이 받는 질문
Q. Expo 프로젝트면 따로 설치하지 않아도 되나요?
공식 문서 기준으로 @expo/vector-icons는 create-expo-app으로 시작한 템플릿 프로젝트에는 기본 설치되어 있고, expo 패키지의 일부로 안내됩니다. 다만 기존 프로젝트나 수동 구성 프로젝트는 상태가 다를 수 있으니 package.json에서 실제 의존성을 확인하는 습관이 좋습니다.
Q. 아이콘 이름은 외워야 하나요?
외우기보다 세트별 디렉터리에서 검색해서 정확한 이름을 복사해 쓰는 편이 안전합니다. 비슷한 이름이 많고 outline, filled, sharp 같은 변형도 있어 추측으로 쓰면 오타가 자주 납니다.
Q. 커스텀 아이콘은 언제 도입하는 게 좋나요?
브랜드 전용 심볼을 여러 화면에서 반복해서 써야 하거나, 기본 세트로는 디자인 일관성이 맞지 않을 때 고려하면 됩니다. 단순히 아이콘 하나가 마음에 들지 않는 정도라면 기본 세트 안에서 해결하는 편이 더 단순합니다.
같이 읽으면 좋은 글
- Expo Router 사용법: app 폴더와 Stack Tabs 구조 잡기
- Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리
- Expo Safe Area 사용법: 화면 여백을 안전하게 잡는 방법
문서·검증 기준 (2026-09-13): 해당 기능의 Expo 공식 문서와 코드 구조를 대조했습니다. 설치 버전은 프로젝트 Expo SDK에 맞는 npx expo install 결과와 lockfile을 기준으로 합니다. 이번 개정에서는 실제 Android·iOS 기기 실행, 원격 EAS 빌드, 외부 인증 서버 연동을 수행하지 않았습니다. 본문의 기기 동작은 독자가 확인할 기대 결과입니다.
독립 실습: 아이콘과 접근 가능한 이름 함께 전환
React 함수 컴포넌트·props·state와 TypeScript import를 알고 시작합니다. 첫 Expo 앱 환경 준비의 blank-typescript 프로젝트를 별도 연습 폴더에 사용합니다. 생성된 index.ts·package.json·lockfile은 유지하고 아래 App.tsx만 연결합니다. TaskFlow 코드는 필요 없습니다. npx expo start 후 대상 기기에서 확인합니다.
현재 Expo 아이콘 가이드와 공식 이전 안내의 세트별 패키지 경로를 사용합니다. npx expo install @react-native-vector-icons/ionicons expo-font 후 실행합니다. 같은 화면에 이전 @expo/vector-icons의 같은 폰트를 섞지 않습니다.
App.tsx 전체 파일
import { useState } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Ionicons } from '@react-native-vector-icons/ionicons';
export default function App() {
const [saved, setSaved] = useState(false);
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
<Pressable
accessibilityRole="button"
accessibilityLabel={saved ? '즐겨찾기 해제' : '즐겨찾기 추가'}
accessibilityState={{ selected: saved }}
onPress={() => setSaved(value => !value)}
style={{ padding: 16 }}
>
<Ionicons name={saved ? 'heart' : 'heart-outline'} size={24} color="#222" />
<Text>{saved ? '저장됨' : '저장'}</Text>
</Pressable>
</View>
);
}
기대 결과: 두 번 누르면 저장→저장됨→저장으로 돌아오고 접근성 이름도 바뀝니다. 앱 재시작 후 유지되는 영속 저장 예제는 아닙니다.
자가 과제: 아이콘을 숨긴 상태에서도 버튼의 이름과 현재 저장 상태를 이해할 수 있는지 확인합니다. 글리프가 비면 설치 세트·name·폰트 로딩 오류를 구분합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow Mobile은 할 일을 관리하는 모바일 앱입니다. 이 선택 실습에서는 기존 앱에 이번 기능을 이어서 적용합니다.
이번 주제: Expo vector icons 사용법: 탭바와 커스텀 아이콘 적용하기.
누적 프로젝트: TaskFlow Mobile
시작 위치: 아래 기존 환경·파일 구성에서 시작합니다. 이 글의 완료 ZIP은 결과 확인용입니다.
시작 체크포인트: Expo Router 사용법: app 폴더와 Stack Tabs 구조 잡기의 누적 파일까지 적용한 상태에서 이어갑니다.
npx expo install @react-native-vector-icons/ionicons expo-font 후 탭 레이아웃만 교체합니다. 탭 제목은 유지하여 아이콘 없이도 목적을 알 수 있게 합니다. 상세의 완료·삭제 버튼은 텍스트 버튼을 유지합니다. 탭 세 개를 오가며 제목·글리프·선택 상태를 확인합니다.
전체 파일·코드 보기
src/app/(tabs)/_layout.tsx — 이번 회차 전체 파일
import { Tabs } from 'expo-router';
import { Ionicons } from '@react-native-vector-icons/ionicons';
export default function Layout() {
return <Tabs>
<Tabs.Screen name="index" options={{ title: '목록', tabBarIcon: ({ color, size, focused }) => <Ionicons name={focused ? 'list' : 'list-outline'} color={color} size={size} /> }} />
<Tabs.Screen name="add" options={{ title: '추가', tabBarIcon: ({ color, size }) => <Ionicons name="add-circle-outline" color={color} size={size} /> }} />
<Tabs.Screen name="settings" options={{ title: '설정', tabBarIcon: ({ color, size }) => <Ionicons name="settings-outline" color={color} size={size} /> }} />
</Tabs>;
}
다음 회차로 넘길 상태: 위 추가·수정 파일을 적용한 로컬 연습 폴더입니다. 실제 실행한 플랫폼과 결과를 기록하고 원래 독립 ZIP과 구분해 보관합니다.
아래 원래 과제의 확장 방향은 위 전체 파일이 제공하는 범위와 구분해 읽습니다. 아이콘 액션 확장·실제 세션 인증·background tracking은 추가 구현 과제입니다.
회차별 확장 질문과 확인 기준
독자 질문: 탭 아이콘을 넣을 때 패키지와 접근성 이름을 어떻게 고르나요?
앱에서 따라 하기
- 5회차 Tabs에 현재 Expo 아이콘 가이드가 안내하는 @react-native-vector-icons의 Ionicons 세트를 설치·연결합니다.
- 목록·추가·설정 탭에 제공된 아이콘을 연결합니다. 현재 파일은 목록 탭에서만 선택/비선택 모양을 바꾸며, 완료·삭제는 기존 텍스트 버튼을 유지합니다. 액션 아이콘 추가는 이후 확장 과제입니다.
- 아이콘만 있는 버튼에는 accessibilityLabel을 넣고 이름·크기·색을 비교합니다.
기기·빌드별 확인
예상 확인: Expo Go의 동적 아이콘 폰트와 개발 빌드의 폰트 로딩 결과는 각각 확인해야 합니다. 여기서는 어느 쪽도 실행하지 않았습니다.
실행 확인 범위: 위의 예상 동작은 직접 따라 할 때 확인할 기준입니다. Android/iOS 기기·에뮬레이터, 웹 브라우저, development build, EAS 원격 빌드와 APK 설치의 실제 결과는 미확인입니다. 환경별로 실행한 뒤 화면, 오류 로그, 빌드 상태를 각각 확인하세요.
오류 점검과 다음 학습
아이콘이 비면 name 오타와 설치 세트를 확인하고, 폰트가 준비되기 전 화면 전체를 무기한 숨기지 않습니다. 기존 @expo/vector-icons 예제는 이전 방식으로 읽습니다.
글 소개·학습 준비·전체 목차
학습 목표·선수 지식: 아이콘 이름·크기·색상을 연결하고 폰트 로딩 실패를 처리합니다. 독립 버튼 실습에는 React props·useState를 알면 됩니다. Router 탭 구조는 뒤의 탭바 적용 예제를 읽을 때 필요합니다.
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 13개 · 필수 9개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.