안녕하세요 이번에 소개드릴 내용은 제가 이번에 구현한 useList(가명)에 대해 소개 드리려고 합니다.
제가 어떠한 상황에서 useList를 구현하기로 마음을 먹었고
어떤 식으로 구현을 했는지에 대해 알려드리면 좋을 거 같다고 판단이 되어서 한 번 작성해 봅니다.
개발 환경
React-native + TypeScript로 진행되었습니다.
구현하게 된 계기
위 사진과 같은 화면 구현의 업무를 맡았습니다.
탭 형식으로 이루어졌으며 선택된 카테고리의 리스트를 보여주는 화면이었습니다.
어떻게 예쁘게 구현할까 고민을 한 후
custom hook으로 구현해야겠다 마음을 먹고 바로 기능들을 추출해봤습니다.
기능 추출
- A Tab
- 리스트 불러오기
- 인피니티 스크롤
- 인증하지 않은 유저라면 인피니티 스크롤 X, 리스트 하단에 인증을 유도하는 문구 노출 시키기
- B Tab
- 리스트 불러오기
- 인피니티 스크롤
- 인증하지 않은 유저라면 인피니티 스크롤 X, 리스트 하단에 인증을 유도하는 문구 노출 시키기
- 즐겨찾기 추가 및 삭제
- C Tab
- 리스트 불러오기
- 인피니티 스크롤
- 인증하지 않은 유저라면 인피니티 스크롤 X, 리스트 하단에 인증을 유도하는 문구 노출 시키기
- D Tab(즐겨찾기 리스트 탭)
- 리스트 불러오기
- 인피니티 스크롤
- 즐겨찾기 삭제
제가 이 기능 추출을 작성하는데도 CTRL+C, CTRL+V를 하게 될 정도로 공통된 기능이 많았습니다.
이것을 바탕으로 개발에 들어갔습니다.
구현
Parameter
제가 생각한 필요한 Parameter 값들은 이렇습니다.
type Parameter = {
URL: string;
flatListRef?: React.RefObject<FlatList<any>>;
generateQueryString: () => string;
errorCallback?: () => void;
};
- URL : API BASE URL이 아닌 BASE_URL/ <= 여기 붙을 URL 입니다.
- 예시 : BASE_URL/fruit
- 예시 : BASE_URL/animal
- flatListRef : ref를 받은 이유는 스크롤 위치를 컨트롤하기 위해서 받았습니다.
- 예시 : 필터를 적용하여 검색한 경우 스크롤 위치를 맨 위로 올리기 위해
- generateQueryString : list 요청에 필요한 쿼리스트링을 반환해주는 함수를 위해서 받았습니다.
- errorCallback : API 요청 시 error가 발생한 경우 처리를 위해 받았습니다.
- 처리가 모두 같다면 필요가 없었지만 특정한 곳에서는 error 발생 시 다른 처리를 해줘야했습니다.
custom Hook 선언
const useList = <T extends { id: string; isFavorite?: boolean }>(parameter: Parameter) => {
};
export default
저 같은 경우 이런 식으로 Type을 지정하여 선언했습니다.
제네릭으로 지정한 이유는 API를 통해 받은 list response가 똑같지 않기 때문입니다.
그 후 extends를 통해 위에서 작성한 즐겨찾기 기능을 구현하기 위해 id와 isFavorite 을 상속 시켰습니다.
state와 변수 선언
const { flatListRef, URL, errorCallback, generateQueryString } = parameter;
const user = useRecoilValue(userState);
const [isLoading, setIsLoading] = useRecoilState(loadState);
const isUserVerified = !!user;
const [offset, setOffset] = useState(0);
const [list, setList] = useState<T[]>([]);
- 첫 번째로 구조 분해 할당을 이용하여 props를 구조 분해 할당을 합니다.
- user 정보 같은 경우 현재 recoil을 통해 가져옵니다.
- recoil로 관리하는 로딩 상태도 가져옵니다.
- isUserVerified는 위에서 작성했던 "인증하지 않은 유저라면 인피니티 스크롤 X, 리스트 하단에 인증을 유도하는 문구 노출시키기" 로직을 반영하기 위해 할당해놨습니다.
- offset은 인피니티 스크롤의 페이지입니다.
- list는 API를 통해 받은 list입니다.
fetch 함수
const fetch = async (shouldScrollToTop = false) => {
if (isLoading) {
return;
}
const requestOffset = shouldScrollToTop ? 0 : offset;
const query = generateQueryString();
setIsLoading(true);
try {
const response = await axios.get(`${BASE_URL}${URL}?${query}&offset=${requestOffset}`);
if (shouldScrollToTop) {
setList(response.data.list);
flatListRef?.current?.scrollToOffset({
animated: true,
offset: 0,
});
} else {
setList(prevList => [...prevList, ...response.data.list]);
}
setOffset(requestOffset + LIMIT);
} catch (err) {
console.log(err);
errorCallback?.();
} finally {
setIsLoading(false);
}
};
Parameter인 shouldScrollToTop은 true라면 FlatList의 ScrollPostion을 0(맨 위)로 올려주기 위해 사용됩니다.
이 코드에서 눈여겨보면 좋은 포인트는 2개인 것 같습니다.
requestOffset : shouldScrollToTop의 값에 따라서 0 또는 offset을 보내고 있습니다. 여기서 만약 바로 setState로 offset 값을 바꾼다면 useState 특성상 바뀐 값을 사용할 수 없어 useEffect로 처리하는 과정이 생겨버립니다.
이러한 이유로 요청 후 값을 set 하고 있습니다.
shouldScrollToTop : 값에 따라서 true 면 값을 할당 후 스크롤 위치 변경 값이 false 면 기존 값에 이어 붙이기를 하고 있습니다.
이러한 이유는
true인 경우는 사용자가 검색 옵션을 바꾼 경우입니다.예시(정렬 순 변경) 이런 경우 그에 맞는 데이터만 보여야 하기 때문에 값을
할당해버립니다.
false인 경우는 사용자가 인피니티 스크롤을 통해 fetch 요청이 들어온 경우입니다. 이런 경우는 위로 다시 올렸을 때 이전 데이터가 남아있어야 하기 때문에 이어 붙입니다.
onEndReachedHandler 함수
const onEndReachedHandler = () => {
if (isUserVerified) {
fetch();
}
};
이 함수는 간단한 편입니다.
React-Native에서 List를 구현할 때 FlatList라는 React-Native에서 제공해 주는 컴포넌트를 흔히 사용하게 됩니다
스크롤이 일정 이상까지 내려왔을 때 호출해 주는 함수를
onEndReached라는 props로 받고 있습니다.
출처:https://reactnative.dev/docs/flatlist
인피니티 스크롤 구현을 위해 일정 부분까지 내려오면 fetch 함수를 요청하도록 하였습니다만
조건문이 하나 있습니다.
isUserVerified 가 true 면 요청해라 그 이유는 아까 위에서 작성한 "인증하지 않은 유저라면 인피니티 스크롤 X"라는 조건이 있었기 때문입니다.
update, toggleFavorite함수
updateFavorite
const updateFavorite = async (isFavorite: boolean, id: string) => {
//API 요청
};
코드를 생략했지만 단순하게 API 요청을 보내는 함수입니다.
toggleFavorite
const toggleFavorite = async (target: T) => {
if (!user?.uid) {
return;
}
setIsLoading(true);
try {
const cloneList = [...list];
const targetIndex: number = cloneList.findIndex(item => item.id === target.id);
const isFavorite: boolean = !!list[targetIndex].isFavorite;
await updateFavorite(!isFavorite, target.id);
if (targetIndex > -1) {
cloneList[targetIndex].isFavorite = !isFavorite;
setList(cloneList as T[]);
}
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
현재 isFavorite 값을 NOT 연산 처리를 하여 API 요청을 보낸 뒤 list에서 제거하는 로직입니다.
최종 구현 코드
type Parameter = {
URL: string;
flatListRef?: React.RefObject<FlatList<any>>;
generateQueryString: () => string;
errorCallback?: () => void;
};
const BASE_URL = API BASE URL
const useList = <T extends { id: string; isFavorite?: boolean }>(parameter: Parameter) => {
const { flatListRef, URL, errorCallback, generateQueryString } = parameter;
const user = useRecoilValue(userState);
const [isLoading, setIsLoading] = useRecoilState(loadState);
const isUserVerified = !!user;
const [offset, setOffset] = useState(0);
const [list, setList] = useState<T[]>([]);
const fetch = async (shouldScrollToTop = false) => {
if (isLoading) {
return;
}
const requestOffset = shouldScrollToTop ? 0 : offset;
const query = generateQueryString();
setIsLoading(true);
try {
const response = await axios.get(`${BASE_URL}${URL}?${query}&offset=${requestOffset}`);
if (shouldScrollToTop) {
setList(response.data.list);
flatListRef?.current?.scrollToOffset({
animated: true,
offset: 0,
});
} else {
setList(prevList => [...prevList, ...response.data.list]);
}
setOffset(requestOffset + LIMIT);
} catch (err) {
console.log(err);
errorCallback?.();
} finally {
setIsLoading(false);
}
};
const onEndReachedHandler = () => {
if (isUserVerified) {
fetch();
}
};
const updateFavorite = async (isFavorite: boolean, id: string) => {
//API 요청
};
const toggleFavorite = async (target: T) => {
if (!user?.uid) {
return;
}
setIsLoading(true);
try {
const cloneList = [...list];
const targetIndex: number = cloneList.findIndex(item => item.id === target.id);
const isFavorite: boolean = !!list[targetIndex].isFavorite;
await updateFavorite(!isFavorite, target.id);
if (targetIndex > -1) {
cloneList[targetIndex].isFavorite = !isFavorite;
setList(cloneList as T[]);
}
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
};
return {
fetch,
onEndReachedHandler,
list,
toggleFavorite,
isUserVerified,
};
};
export default useList;
후기
제가 구현한 custom hook을 사용하여 반복도 최소화하고 이번에 작업한 feature에 useList를 사용하여 대부분의 로직을 처리할 수 있었습니다.
덕분에 유저들의 니즈를 맞추면서 깔끔한 로직이 탄생한 거 같습니다.
제가 비록 블로그 글을 몇 개 쓰지는 않았지만 이 정도 분량을 쓰게 될 줄을 몰랐었습니다.
혹시나 저와 비슷한 개발 고민을 가지고 뒤적뒤적 하시는 분들께 도움이 되었으면 하는 마음으로 끝까지 글을 작성했네요
읽어주셔서 감사합니다.
'개발' 카테고리의 다른 글
useDebounce를 활용하기 (1) | 2023.06.07 |
---|---|
React Native Chip을 구현해보자 (0) | 2023.05.26 |
React Native 나만의 checkbox 구현하기 ✅ (0) | 2023.05.16 |
React Native useModal 구현하기 (0) | 2023.05.02 |
useCodePush 구현하기 (3) | 2023.04.24 |