치춘짱베리굿나이스
[프리온보딩] 220514 개인과제 #1 완성 및 배포 본문
개인과제 #1 완성, 배포
공식적인 내생각
Suspense
로 로딩창 구현해보겠다고 온갖 삽질을 하다가 오늘 드디어 해결했다
겸사겸사 localStorage
값이 뜬금없이 초기화되는 문제도 해결하고, 드래그앤 드롭도 구현했다
Suspense
+ Promise
로 로딩 구현하는 게 제일 힘들었다... 하나 잘 막으면 다른 하나에서 터지고
이게 다 비동기 때문이다 setState
가 비동기라서 그렇다 진짜 상태값 바뀌는 타이밍을 모르겠어서...
배포도 마치긴 했는데 깃허브 페이지 배포는 언제 해도 헷갈린다
배포 후에 데이터 받아오는 함수가 동작을 안해서 환경변수가 제대로 적용이 안 된줄 알고 한참을 해멨는데 CRA는 컴파일할 때 env 파일 변수를 알아서 잘 컴파일해준다는 걸 뒤늦게 알았고 사실 https 이슈였다
지금은 404 에러 뜨는거랑 싸우고 있다... 이것만 해결해주고 잘라했는데 넘 졸리다 일찍 자야지
삽질의 기록들
Suspense로 로딩창 구현하기 + 상태값 초기화 타이밍
분명 Suspense와 Promise에 대해서 열심히 공부했으니 구현만 하면 금방이겠다 했는데 생각보다 꼬이는 부분이 많았다
우선 데이터를 받아오는 지점이 Suspense
로 감싸진 컴포넌트가 렌더링되기 전에 위치해야 한다는 점 때문에 불가피하게 컴포넌트를 두 개로 쪼개야 했고, 그 과정에서 상태값의 업데이트 타이밍이 전부 꼬여서 + 무한스크롤을 위해 intersect 감지당하는 요소가 생각보다 빨리 렌더링돼서 스크롤을 내릴 때 로딩되어야 할 데이터가 자꾸 처음에 로딩되었다
결국 useEffect
를 가지고 컴포넌트 업데이트를 위한 온갖 쇼를 벌인 끝에 해결했다...
export const MovieListContainer = (): JSX.Element => {
const [searchValue, setSearchValue] = useRecoilState(searchValueState);
const res = fetchWrappedMovieData(searchValue, 1);
useUnmount(() => {
setSearchValue('');
});
return (
<div className={styles.movieContainer}>
{searchValue === '' ? (
<ContainerMessage isLoading={false} message='Movie not found!' />
) : (
<Suspense fallback={<ContainerMessage isLoading />}>
<MovieList resource={res} />
</Suspense>
)}
</div>
);
};
이 컴포넌트는 검색 화면 하단의 검색결과가 나오는 컴포넌트이고, searchValue
는 검색 바 (input
태그) 로부터 받아오기 때문에 부모를 통해서 값을 넘겨주지 않고 Recoil을 통해 전역으로 관리하였다
이 컴포넌트가 화면에서 사라질 때 (/Favorite
페이지로 넘어갈 때) 검색 결과를 없애기 위해 Unmount
시에 searchValue
를 빈 문자열로 만들어 주었다
jsx 요소로는 우선 div
로 전체 컴포넌트를 감싸주고, 이 부분이 흰 배경이 된다
searchValue
가 빈 문자열일 땐 굳이 검색을 수행할 필요가 없기 때문에 ‘검색 결과가 없습니다' 메시지를 띄워주는 컴포넌트를 반환하고, 그 외의 경우엔 검색 결과를 리스트로 출력하되 Suspense
로 감싸주었다
이 Suspense
부분이 정말 힘들었다 그냥 컴포넌트를 Suspense
로 감싸기만 하면 되는줄 알았던 것은 젊은 날의 과오였을 뿐이다
import axios, { AxiosPromise, AxiosResponse } from 'axios';
const wrapPromise = (promise: AxiosPromise) => {
let status = 'pending';
let result: AxiosResponse<any, any>;
const suspender = promise.then(
(response) => {
status = 'success';
result = response;
},
(error) => {
status = 'error';
result = error;
}
);
const read = () => {
if (status === 'pending') throw suspender;
else if (status === 'error') throw result;
else if (status === 'success') return result;
return null;
};
return { read };
};
export const fetchWrappedMovieData = (searchValue: string, page: number) => {
const promise = axios({
method: 'GET',
params: {
apikey: process.env.REACT_APP_MOVIE_API_ID,
s: searchValue,
page,
},
url: process.env.REACT_APP_MOVIE_API_URL,
});
return wrapPromise(promise);
};
저 wrapPromise
함수로 감싸줘야 pending
/ success
/ error
값을 보고 Suspense
가 적절한 컴포넌트를 반환해주거나 Error Boundary로 에러를 위임하는 것이다
axios
로 받아온 데이터를 Suspense
에 적합한 형태로 Wrapping한 건 좋은데, 문제는 추가 데이터를 받아올 땐 이러한 형태로 데이터를 감쌌다간 스크롤을 할 때마다 로딩 창이 나와버린다... 결국엔 영화 데이터를 받아오는 함수를 2개로 분할했다
하나는 wrapPromise
함수로 감싸 Suspense
에 적합한 형태로 만든 것이고, 나머지 하나는 axios
와 then
/ catch
만 수행하여 별도의 로딩 매커니즘을 적용하지 않았다
export const MovieList = ({ resource }: IMovieListData): JSX.Element => {
const searchResult = resource.read();
const totalResults = searchResult?.data.totalResults;
const response = searchResult?.data.Response;
const searchValue = useRecoilValue(searchValueState);
const [movieArray, setMovieArray] = useState<IMovie[]>([]);
const [isNextPage, setIsNextPage] = useState(true);
const [pages, setPages] = useState(1);
const getMoreMovie = async () => {
if (totalResults && totalResults < pages * 10) {
setIsNextPage(false);
return;
}
const responseData = await fetchMovieData(searchValue, pages + 1);
if (responseData.Response === 'True') {
setMovieArray((prevState) => _.uniqBy(prevState.concat(responseData?.Search), 'imdbID'));
setPages((prevState) => prevState + 1);
}
};
useEffect(() => {
const arrTemp = _.uniqBy(searchResult?.data.Search as IMovie[], 'imdbID');
setMovieArray(arrTemp);
setPages(1);
}, [searchValue, searchResult]);
...
Suspense
로 감싸진 컴포넌트의 일부분이다 (searchValue
는 input
이 submit될 때마다 변한다)
searchResults
가 아까 wrap한 Promise
를 읽어오는 변수이고, 저 값이 없으면 어떠한 요소도 렌더링할 수 없기 때문에 아직 searchResults
가 대기 상태일 때 Suspense
가 로딩 컴포넌트로 해당 부분을 대체해준다 (fallback)
totalResults
, response
는 컴포넌트 내에서 지속적으로 변하는 값이 아니기 때문에 굳이 상태값을 사용하지 않았고, searchValue
가 변경될 때마다 영화 배열을 초기화시키고 컴포넌트를 리렌더링해야 하기 때문에 값만 useRecoilValue
훅으로 받아왔다
무한스크롤을 통해 영화를 누적해서 받아올 것이기 때문에 IMovie 타입의 배열과 무한 렌더링 가능 여부를 알려주는 boolean 값, 무한 스크롤 시에 특정 페이지의 값을 받아오기 위한 number 값을 상태값으로 선언해주었다
검색 값이 바뀔 때마다 페이지는 새로고침되지 않고 컴포넌트만 리렌더링되기 때문에, pages를 그때그때 초기화해 주지 않으면 이전에 무한스크롤으로 증가시켰던 페이지 수가 남아있으므로 searchValue
가 변할 때마다 페이지 번호도 초기화해 준다
무한스크롤을 구현할 때 교차 이벤트가 발생할 때마다 실행시킬 콜백 함수를 넘겨주어야 하는데, 그 함수를 getMoreMovie
로 하여 movieArray
에 값이 누적될 수 있도록 설정하였다
useEffect
부분이 가장 까다로웠는데, 도대체 어느 시점에서 어떤 값을 초기화해야 이상한 동작을 방지할 수 있을지 헷갈렸기 때문이다
가장 이상적인 건 searchValue
(검색어) 가 바뀔 때마다 지금까지 값이 누적되던 배열 movieArray
와 페이지 번호를 초기화해 주는 것이었다
searchValue
가 바뀔 때마다 처음에는 빈 배열로 초기화했더니 아예 영화가 한 개도 안 받아지기도 하고, pages
를 컴포넌트 마운트 시에 바꾸려고 하니 최초에 한번 바뀌고 끝끝내 안 바뀌고, 아예 부모 컴포넌트에서 pages
랑 movieArray
상태값을 초기화하여 자식으로 넘겨주는 방법을 시도해보니 pages가 중첩되어 증가하여 (???) 지금 받아오면 안 되는 영화들이 들어오거나 이전 영화들이 남아있기도 했다
결국 searchValue
와 searchResult
의 결과로 배열을 초기화해주니 해결되어서 속이 시원하다
localStorage 값 받아오기 / 초기화 시점
두 번째는 로컬 스토리지에서 값을 받아오는 시점을 Favorite 페이지가 처음 렌더링될 때로 잡아두니 툭하면 로컬 스토리지 내부 값들이 초기화되는 현상이 일어났다
분명 검색 페이지에선 좋아요를 눌렀는데, Favorite 페이지만 진입하면 값들이 다 날아갔다
이유인즉슨 로컬 스토리지 내부에 전역 상태값인 favoriteData
를 초기화하는 코드 때문이었다
로컬 스토리지에서 좋아요 누른 영화 목록을 불러와 favoriteData
에 저장하고, favoriteData
가 바뀔 때마다 로컬 스토리지에 새로 저장하는 방식을 채택하였는데, favorite 페이지가 렌더링될 때 favoriteData
를 초기화하는 코드를 심었기 때문이었다 (....)
export const FetchFavoriteData = () => {
const setFavoriteData = useSetRecoilState(favoriteDataState);
useMount(() => {
const storageData = store.get('storageData');
if (!storageData) {
store.set('storageData', INITIAL_FAVORITE);
setFavoriteData(INITIAL_FAVORITE);
} else setFavoriteData(storageData);
});
return null;
};
두 페이지를 오갈 때마다 데이터를 날릴 순 없었기 때문에, 내비게이션 바와 마찬가지로 최상위에 favoriteData
랑 로컬 스토리지만 관리하는 컴포넌트를 따로 빼 주었다
이 컴포넌트가 마운트될 때마다 로컬 스토리지에서 값을 가져와 favoriteData
를 초기화해 주고, 어떠한 jsx 태그도 렌더링하지 않는다
<BrowserRouter basename={process.env.PUBLIC_URL}>
<FetchFavoriteData />
<NavigationBar />
<Routes>
<Route path='/' element={<MainPage />} />
<Route path='/favorites' element={<FavoritePage />} />
</Routes>
</BrowserRouter>
라우팅 시에 Routes 위에 최상위 컴포넌트로 분리하면 하위 페이지가 바뀌어도 모습만 안 보인다 뿐이지 최상단에서 위치를 지키고 있기 때문에 새로고침을 할 때마다만 favoriteData
를 로컬에서 불러오거나 초기화한다
보이지 않는 곳에서 묵묵히 일하는 컴포넌트 덕에 앱을 열었을 때나 새로고침을 할 때 로컬 스토리지에서 데이터를 잘 불러오는 앱이 되었다
오늘 정리한 내용
lodash
'프로젝트 > 원티드 프리온보딩' 카테고리의 다른 글
[프리온보딩] 220510 강의 메모 01 (간단 코드리뷰) (0) | 2022.05.16 |
---|---|
[프리온보딩] 220515 개인과제 #1 종료 (0) | 2022.05.16 |
[프리온보딩] 220513 개인과제 #1 (0) | 2022.05.14 |
[프리온보딩] 220512 개인과제 #1 (0) | 2022.05.13 |
[프리온보딩] 220511 개인과제 #1 (0) | 2022.05.12 |