Instagram Clone : React Native - part 4 [ LIKES, SEARCH & PHOTO]
Likes
Photo-id 전달
좋아요를 누른 유저의 목록을 받는 query (seePhotoLikes
)를 요청하기 위해 Like 스크린으로 사진의 id를 전달해야한다. navigation.navigate
을 이용하여 전달한다.
seePhotoLikes query
전달 받은 id로 query 요청을 한다. User 목록은 재사용 가능하니, fragments로 만들어준다.
Skip in useQuery
photo의 id가 없이 Likes 스크린으로 이동할 경우도 있다. 그럴 경우, query 요청을 하지 않도록 한다.
useQuery의 옵션인 skip에 route로 전달받은 photoId가 없으면 요청을 스킵하도록 한다.
// Likes.tsx
...
function Likes({ route }: LikesProps) {
const { ... } = useQuery<...>(LIKES_QUERY, {
variables: { id: route?.params?.photoId! },
skip: !route?.params?.photoId, // 파라미터 없이 likes 스크린으로 이동하면 스킵
});
...
FlatList 만들기
Instagram Clone Front-End | FEED - FlatList 참고
마찬가지로, 받아온 User 데이터로 FlatList를 만든다.
ItemSeperatorComponent
FlatList는 ItemSeperatorComponent
라는 props로 리스트의 separator 기능을 제공한다. props 안에 seperator로 사용할 스타일 컴포넌트를 함수형으로 넣는다. 컴포넌트는 처음과 마지막 요소에 위와 아래에 구분선이 없이 렌더된다.
// Likes.tsx
...
<FlatList
ItemSeparatorComponent={() => (
<View
style={{
width: "100%",
height: 1,
backgroundColor: "rgba(255, 255, 255, 0.2)",
}}
></View>
)}
...
Serialize
query 변경으로 인해 schema가 바뀌게 되면 기존에 저장되어 있던 cache랑 충돌하여 에러가 발생한다. 이를 방지하기 위해, persistCache
에서 serialize
의 값을 false
로 지정한다.
// App.tsx
export default function App() {
...
const preload = async () => {
...
await persistCache({
cache,
storage: new AsyncStorageWrapper(AsyncStorage),
serialize: false,
});
return preloadAssets();
};
if (loading) {
return (
<AppLoading
startAsync={preload}
...
/>
);
}
Header Domination
Navigation.setOptions
Navigation으로 이동하면서 Navigator의 옵션을 바꿔준다. route로 전달받은 username을 Header에 띄운다.
// Profile.tsx
...
function Profile({ navigation, route }: ProfileProps) {
useEffect(() => {
// route로 전달받은 username이 존재할 경우,
if (route?.params?.username) {
// username을 Header로 띄운다.
navigation.setOptions({ title: route.params.username });
}
}, []);
...
useMe Hook
Instagram Clone Front-End | FEED - useUser Hook 참고
Web에서 로그인 유저의 정보를 가져오는 Hook을 그대로 똑같이 사용한다. 이 Hook으로 내 프로필 탭에서 로그인 유저의 정보를 가져와서 렌더시킨다.
// Me.tsx
...
function Me({ navigation }: MeProps) {
const { data } = useMe();
useEffect(() => {
navigation.setOptions({ title: data?.me?.username });
}, []);
...
Search
Header에 검색창 만들기
마찬가지로, navigation.setOptions
을 사용하여 헤더에 검색창을 만든다. useEffect
로 컴포넌트가
마운트 될 시 실행시킨다.
DissMissKeyboard 컴포넌트화
Instagram Clone Front-End | AUTHENTICATION - dissMissKeyboard Function 참고
검색창 바깥을 눌렀을 때, 키보드가 사라지게 한다. Keyboard.dismiss
를 사용하여 여러 곳에 재사용 가능한 컴포넌트를 만든다.
// DismissKeyboard.tsx
...
function DismissKeyboard({ children }: { children: React.ReactNode }) {
const dissmissKeyboard = () => {
Keyboard.dismiss();
};
return (
<TouchableWithoutFeedback
style={{ height: "100%" }}
onPress={() => dissmissKeyboard()}
disabled={Platform.OS === "web"}
>
{children}
</TouchableWithoutFeedback>
);
}
useLazyQuery
Header에 생긴 인풋 창으로 searchPhotos
요청을 보낸다. useQuery
는 컴포넌트가 마운트 될 시, 자동으로 실행되기 때문에 useLazyQuery
라는 Hook을 사용한다. useLazyQuery
는 mutation과 같이 요청을 발동시키는 함수가 제공되고 나머지 옵션도 같다.
React-Hook-Form
본격적으로 검색창에 검색 기능을 적용시킨다. 이전과 마찬가지로, 네이티브에서는 register
(+ 유효성 검사)와 이벤트를 직접 등록해주어야 한다. 인풋 박스를 submit 했을 때 (onSubmitEditing
), handleSubmit
으로 onValid
함수를 실행시킨다. submit을 바로 쿼리와 연결시키면, 인풋 입력마다 요청이 발생한다.
검색이 유효성 검사를 통과할 경우 (onValid
), 쿼리를 요청한다.
Search Result UI
검색 결과의 경우의 수에 따라, 그에 맞는 메시지를 렌더시켜야 한다. 경우의 수는 다음과 같다.
- 요청 중일 경우
- 아직 검색하지 않았을 경우
- 검색한 후 결과를 받아왔을 경우
- 검색했는데 결과가 없을 경우
// Search.tsx
...
return (
<DismissKeyboard>
<View style={{ flex: 1, backgroundColor: "black" }}>
// 1. 요청 중일 경우
{loading && (
<MessageContainer>
<ActivityIndicator size="large" />
<MessageText>Searching...</MessageText>
</MessageContainer>
)}
// 2. 아직 검색하지 않았을 경우
{!called && (
<MessageContainer>
<MessageText>Search by keyword</MessageText>
</MessageContainer>
)}
{data?.searchPhotos !== undefined &&
data?.searchPhotos?.length === 0 ? (
// 3. 검색했는데 결과가 없을 경우
<MessageContainer>
<MessageText>Could not find anything</MessageText>
</MessageContainer>
) : (
// 4. 검색한 후 결과를 받아왔을 경우
<FlatList
numColumns={numColumns}
data={data?.searchPhotos}
keyExtractor={(photo) => "" + photo.id}
renderItem={renderPhoto}
/>
)}
</View>
</DismissKeyboard>
);
...
Photo Grid
검색 결과 나온 사진들을 4열로 정렬한다. FlatList의 numColumns
로 열을 지정하여 정렬할 수 있다. 화면 여백 없이 4등분 하기 위해서, useWindowDimensions
으로 화면의 너비를 구한 후, numColumn
에 내려준 수만큼 나누어 주면 된다.
// Search.tsx
...
function Search({ navigation }: SearchProps) {
const numColumns = 4;
...
const renderPhoto: ListRenderItem<searchPhotos_searchPhotos> = ({
item: photo,
}) => (
<TouchableOpacity>
<Image
source={{ uri: photo.file }}
style={{ width: width / numColumns, height: 100 }}
/>
</TouchableOpacity>
);
return (
...
<FlatList
numColumns={numColumns}
...
/>
)}
...
);
}
export default Search;
Photo Screen
Search to Photo
검색된 photo와 photo 스크린을 연결한다. 마찬가지로 사진을 눌렀을 때, photo 스크린으로 넘어가면서 route를 통해 photo-id를 전달받을 수 있게한다. 전달받은 photo-id로 seePhoto 쿼리 요청을 한다. Photo 컴포넌트를 이미 작성해놓았기 때문에 응답으로 받은 데이터를 그대로 Photo 컴포넌트로 내려주면 된다.
Scroll View Refresh
View가 화면을 넘어갈 수도 있기 때문에, ScrollView를 이용한다. 당겨서 새로고침을 구현하는데, FlatList와 약간의 차이가 있다. ScrollView는 RefreshControl
이라는 컴포넌트 안에 refreshing
(새로고침 사용 여부)와 onRefresh
(새로고침 시 실행할 함수)를 넣어준다.
그리고 스크롤 할 내용이 담긴 View와 뒷 배경이 각각 contentContainerStyle
과 style
로 스타일링이 구분된다.
// screens/Photo.tsx
...
const SEE_PHOTO = gql`
query seePhoto($id: Int!) {
seePhoto(id: $id) {
...
`;
function PhotoScreen({ route, navigation }: PhotoProps) {
const [refreshing, setRefreshing] = useState(false);
const { data, loading, refetch } = useQuery(SEE_PHOTO, {
variables: { id: route.params?.photoId },
});
...
return (
<ScreenLayout loading={loading}>
<ScrollView
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
style={{ backgroundColor: "black" }}
contentContainerStyle={{ ... }}
>
<Photo {...data?.seePhoto} />
</ScrollView>
</ScreenLayout>
);
}
Author And Source
이 문제에 관하여(Instagram Clone : React Native - part 4 [ LIKES, SEARCH & PHOTO]), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@gwanuuoo/Instagram-Clone-React-Native-part-4-LIKES-SEARCH-PHOTO저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)