Tab Navigator는 Stack Navigator와 마찬가지로 페이지를 이동할 수 있다. 대신 Tab은 하단의 탭들로 이동한다. 사용법은 Stack Navigator와 마찬가지로 Tabs를 셍성한후 option을 설정한다.
option들은 공식 문서에서 확인할 수 있다.
//LoggedInNav.js
const Tabs = createBottomTabNavigator();
export default function LoggedInNav() {
return (
<Tabs.Navigator
screenOptions={{
tabBarActiveTintColor: "white",
tabBarShowLabel: false,
title: false,
headerBackTitleVisible: false,
headerTransparent: true,
tabBarStyle: {
borderTopColor: "rgba(255, 255, 255, 0.3)",
backgroundColor: "black",
},
}}
>
<Tabs.Screen
name="Feed"
component={Feed}
options={{
tabBarIcon: ({ focused, color, size }) => (
<Ionicons name="home" color={color} size={focused ? 24 : 20} />
),
}}
/>
Tab Navigator안에 Stack Navigator가 필요한 경우가 있다. 인스타그램을 예로 들면 Search에서 user의 사진과 프로필 페이지를 들어갈 수 있고 Feed에서도 사진과 프로필에 들어갈 수 있다. 이 때 Tabs Navigator에서 Stack Navigator를 통해 사진과 프로필 페이지를 공유하게 한다.
아래와 같이 스크린 이름을 받아와서 스크린 이름에따라 네비게이션을 설정한다.
//StackNavFactory.js
const Stack = createStackNavigator();
export default function StackNavFactory({ screenName }) {
return (
<Stack.Navigator
screenOptions={{
headerBackTitleVisible: false,
headerTintColor: "white",
headerStyle: {
shadowColor: "rgba(255, 255, 255, 0.3)",
backgroundColor: "black",
},
}}
>
{screenName === "Feed" ? (
<Stack.Screen name={"Feed"} component={Feed} />
) : null}
{screenName === "Search" ? (
<Stack.Screen name={"Search"} component={Search} />
) : null}
{screenName === "Notifications" ? (
<Stack.Screen name={"Notifications"} component={Notifications} />
) : null}
{screenName === "Me" ? <Stack.Screen name={"Me"} component={Me} /> : null}
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Photo" component={Photo} />
</Stack.Navigator>
);
};
Tab Navigator에서 child로 stack Navigator로 설정해준다. 이 때 Stack Navigator를 사용하면 Tab.Screen의 prop으로 component를 지정해주면 안된다.
//LoggedInNav.js
<Tabs.Screen
name="Feed"
options={{
tabBarIcon: ({ focused, color, size }) => (
<TabIcon iconName={"home"} color={color} focused={focused} />
),
}}
>
{() => <StackNavFactory screenName="Feed" />}
</Tabs.Screen>
Feed를 볼 때 스크롤을 내리는 방법이 2가지 있다.
Scroll View
data를 한번에 랜더링 한다. 스크롤 안에 너무 많은 것을 로딩하면 Navigator의 움직임이 이상해진다.
height대신 width를 쓰면 가로로 스크롤이 가능하다.
//Feed.js
<ScrollView>
<View style={{ height: 20000, flex: 1, backgroundColor: "blue" }}>
<Text>SCrollllll</Text>
</View>
</ScrollView>
FlatList
FlatList는 항목들을 게으르게 랜더링 한다. 스크롤하는 중 item이 저 밑에 있거나 맨 위에 있으면 랜더링하지 않는다. 오직 화면에 있는 것만 랜더링 한다.
//Feed.js
export default function Feed() {
const { data, loading } = useQuery(FEED_QUERY);
const renderPhoto = ({ item: photo }) => {
return (
<View style={{ flex: 1 }}>
<Text style={{ color: "white" }}>{photo.caption}</Text>
</View>
);
};
return (
<ScreenLayout loading={loading}>
<FlatList
data={data?.seeFeed}
keyExtractor={(photo) => "" + photo.id}
renderItem={renderPhoto}
/>
</ScreenLayout>
);
}
React Native에서는 웹에서 이미지를 불러오기 위해서는 width와 height 값이 필수이다.
useWindowDimensions을 통해 app의 width 와 height를 받아올 수 있다.
//Photo.js
const { width, height } = useWindowDimensions();
const [imageHeight, setImageHeight] = useState(height - 450);
useEffect(() => {
Image.getSize(file, (width, height) => {
setImageHeight(height / 3);
});
}, [file]);
return (
<Container>
<Header onPress={() => navigation.navigate("Profile")}>
<UserAvatar resizeMode="cover" source={{ uri: user.avatar }} />
<Username>{user.username}</Username>
</Header>
<File
resizeMode="contain"
style={{
width,
height: imageHeight,
}}
source={{ uri: file }}
/>
아래로 당겨 새로고침하는 기능을 FlatList의 refreshing과 onRefresh로 만들 수 있다.
useQuery의 refetch로 다시 백엔드에서 불러온다.
//Feed.js
export default function Feed() {
const { data, loading, refetch } = useQuery(FEED_QUERY);
const renderPhoto = ({ item: photo }) => {
return <Photo {...photo} />;
};
const [refreshing, setRefreshing] = useState(false);
const refresh = async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
};
return (
<ScreenLayout loading={loading}>
<FlatList
refreshing={refreshing}
onRefresh={refresh}
style={{ width: "100%" }}
showsVerticalScrollIndicator={false}
data={data?.seeFeed}
keyExtractor={(photo) => "" + photo.id}
renderItem={renderPhoto}
/>
</ScreenLayout>
);
}
Flatlist의 props 중 onEndReached와 onEndReachedThreshold가 있다.
onEndReached : 스크롤의 끝에 도달하면 실행한다.
onEndReachedThreshold : 스크롤의 끝을 지정할 수 있다.
useQuery의 fetchMore을 이용하여 더 불러올 수 있다.
// Feed.js
const { data, loading, refetch, fetchMore } = useQuery(FEED_QUERY, {
variables: {
offset: 0,
},
});
return (
<ScreenLayout loading={loading}>
<FlatList
onEndReachedThreshold={0.05}
onEndReached={() =>
fetchMore({
variables: {
offset: data?.seeFeed?.length,
},
})
}
DB에서 불러오고 apollo 캐시에도 저장을 해줘야한다.
keyArgs는 argument인 offset을 무시해준다.
merge에서 existing(기존 value), incoming(새로운 value)를 합쳐준다.
// apollo.js
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
seeFeed: {
keyArgs: false,
merge(existing = [], incoming = []) {
return [...existing, ...incoming];
}
}
},
},
},
}),
});
위 처럼 직접해줘도 되지만 이미 만들어진 함수도 있다.
offsetLimitPagination 함수는 위와 동일한 역할을 한다.
// apollo.js
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
seeFeed: offsetLimitPagination(),
},
},
},
}),
});
인스타그램은 로딩을 할 때 로딩화면만 보여주지 않는다. 내가 전에 봤던 피드들을 보여주고 로딩이되면 불러온다.
오프라인일 때도 나의 프로필을 볼 수 있고 이전에 봤던 피드 몇개를 볼 수 있다.
이러한 이유는 cache persist로 핸드폰의 하드웨어에 미리 저장해놓고 불러오기 때문이다.
apollo3-cache-persist를 설치해준다.
npm install apollo3-cache-persist
앱을 실행할 때 preload히면서 같이 불러온다.
cache는 apollo.js에서 export해준다.
// App.js
const preload = async () => {
const token = await AsyncStorage.getItem("token");
if (token) {
isLoggedInVar(true);
tokenVar(token);
}
await persistCache({
cache,
storage: new AsyncStorageWrapper(AsyncStorage),
});
return preloadAssets();
};
useQuery는 component가 mount될 때 자동으로 바로 실행되므로 우리가 검색을 원할 때만 실행될 수 있는 함수가 필요하다. 그것이 바로 useLazyQuery이다.
startQueryFn을 부르기 전까지 request하지 않는다.
called는 요청되었는지를 boolean타입으로 반환한다.
const [startQueryFn, { loading, data, called }] = useLazyQuery(SEARCH_PHOTOS);