• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2024-12-18

리스트·네비게이션 — FlatList와 Expo Router

리스트·네비게이션 — FlatList와 Expo Router

요약: 긴 목록은 FlatList로 가상화하고, 화면 이동은 Expo Router의 파일 기반 라우팅으로 구성한다.


1. 왜 FlatList와 파일 라우팅인가

목록이 길어지면 웹처럼 map으로 전부 그리면 안 된다. 화면에 안 보이는 행까지 마운트되어 스크롤이 끊긴다. FlatList는 보이는 근처만 렌더한다.

화면 이동은 Expo Router가 기본이다. app/ 폴더가 URL이 된다. Next.js App Router와 감각이 비슷하다. React Navigation을 직접 짜는 것보다 파일 구조가 문서가 된다.

둘을 같이 익혀야 “목록에서 상세로”가 한 흐름이 된다.


2. 핵심 개념

컴포넌트 언제
ScrollView 짧은 고정 콘텐츠
FlatList 긴 동종 리스트. 기본
SectionList 섹션 헤더가 있는 목록
FlashList 더 공격적인 가상화 (Shopify)

라우트는 파일이 경로다. 괄호 폴더 (tabs)는 URL에 안 나온다. [id]는 동적 세그먼트다.

이동은 Link 또는 router.push. 큰 객체를 params에 넣지 않는다. id만 넘기고 상세는 캐시나 fetch로 채운다.

인증 분기는 레이아웃에서 한다. 각 화면마다 if (!user)를 반복하지 않는다.

딥링크는 app.json의 scheme이다. myapp://posts/1처럼 연다. HTTPS 유니버설 링크는 스토어·도메인 검증이 필요하다.


3. 예제

import { FlatList, Text, View } from 'react-native';

type Item = { id: string; title: string };

export function PostList({ data }: { data: Item[] }) {
  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16 }}>
          <Text>{item.title}</Text>
        </View>
      )}
      ItemSeparatorComponent={() => (
        <View style={{ height: 1, backgroundColor: '#e2e8f0' }} />
      )}
      ListEmptyComponent={<Text style={{ padding: 24 }}>게시글이 없습니다</Text>}
      contentContainerStyle={{ flexGrow: 1 }}
    />
  );
}

행 높이가 고정이면 getItemLayout으로 스크롤 측정 비용을 줄인다. 행 컴포넌트는 memo를 검토한다.

<FlatList
  initialNumToRender={10}
  windowSize={7}
  removeClippedSubviews
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
/>

당겨서 새로고침과 무한 스크롤:

<FlatList
  refreshing={refreshing}
  onRefresh={onRefresh}
  onEndReached={loadMore}
  onEndReachedThreshold={0.3}
  ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
/>

페이지 데이터는 React Query useInfiniteQuery와 맞는다.

폴더 예시:

app/
├── _layout.tsx
├── index.tsx
├── (auth)/
│   ├── _layout.tsx
│   └── login.tsx
├── (tabs)/
│   ├── _layout.tsx
│   ├── index.tsx
│   └── settings.tsx
└── posts/
    ├── index.tsx
    └── [id].tsx
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
      <Stack.Screen name="(auth)" />
      <Stack.Screen name="posts/[id]" options={{ headerShown: true, title: 'Post' }} />
    </Stack>
  );
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';

export default function TabsLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="settings" options={{ title: 'Settings' }} />
    </Tabs>
  );
}

이동과 파라미터:

import { Link, router, useLocalSearchParams } from 'expo-router';

<Link href={`/posts/${id}`}>열기</Link>
router.push(`/posts/${id}`);
router.replace('/login');
router.back();
// app/posts/[id].tsx
export default function PostDetail() {
  const { id } = useLocalSearchParams<{ id: string }>();
  return <Text>Post {id}</Text>;
}

로그인 전후 분기(버전별 API는 Redirect로도 가능):

import { Redirect } from 'expo-router';

if (isLoading) return <Splash />;
if (!user) return <Redirect href="/login" />;

typed routes 옵션을 켜면 href 자동완성이 좋아진다.


4. 흔한 실수

실수 대안
ScrollView + 수천 개 map FlatList / FlashList
keyExtractor를 index로만 안정적 id
상세에 객체 전체 params id + Query 캐시
모든 화면에 로그인 가드 루트/그룹 레이아웃
인라인 renderItem + 인라인 style 과다 행 컴포넌트 분리, memo
빈 목록에 높이 0 contentContainerStyle={{ flexGrow: 1 }}

5. 정리

목록은 가상화, 화면은 파일이다. 이 두 규칙이면 피드·상세·탭의 뼈대가 끝난다.

  • 짧은 스크롤만 ScrollView.
  • 탭은 (tabs), 인가는 (auth), 상세는 [id].
  • params는 식별자만. 데이터는 fetch.

연습

  1. FlatList로 50개 이상 아이템 리스트를 만든다.
  2. pull-to-refresh와 empty 상태를 구현한다.
  3. 탭 + posts/[id] 상세를 Expo Router로 연결한다.
  4. 로그인 여부에 따라 (auth) / (tabs)로 보낸다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글상태·훅·폼 — 사용자 입력과 데이터 흐름다음 글네트워킹 — Fetch, React Query, 에러 처리
← 목록으로