퇴사 후 한달이 넘는 여행이 끝나고
나름 리액트 감 좀 살려놨다고 생각해서 FE 과제를 자신있게 진행했다.
불합격할 수가 없다고 생각했는데..
과제가 끝나고 과제 진행했던걸 떠오르면 떨어질만 했다고 생각이 든다ㅠ
아무튼 회고를 해보겠다
순서는 상관 없이 작성하겠다
1. 테스트 코드 관련 이슈
테스트 코드가 전부 실패했다 왤까?
이유: jest 를 사용하는 테스트에서 ESM 모듈을 사용했기 때문
lodash-es 를 프로젝트마다 사용해서 과제에서도 동일하게 사용했다.
그러나,,, 그냥 lodash 도 아닌 lodash-es 는 추가적인 세팅이 필요한데 까먹어버렸다.
lodash-es 를 자세히 보면 뒤에 'es' 라는 단어가 붙어있다.
그렇다. CommonJS 가 아닌 ESM 모듈이라는 뜻이다.
Jest 에서 추가적인 세팅을 하지 않고서 esm 모듈을 사용하면 테스트 코드에서 에러가 나게 된다.
이러한 에러를 해결하기 위해
1. Jest 의 experimental support for ECMAScript Modules 사용 (링크)
2. babel-jest 사용 (esModule 도 같이 transform 시키도록 함)
- jest config 중 transformIgnorePatterns 의 기본 값은 아래와 같기 때문에 모든 node modules 가 babel 에 의해 transform 되지 않는다.
- 하지만 변경해야하기 때문에.. 변경해야할 esmModules 를 ignore 하지 않게 따로 빼준다.
const esModules = ['lodash-es'].join('|')
/** esModules 를 제외한 모든 node_modules 를 transform 할 때 무시 */
transformIgnorePatterns: [`<rootDir>/node_modules/(?!${esModules})`],
3. ts-jest 사용 (esm 관련 옵션이 존재함)
개인적인 생각으론,, 2번 babel 을 사용하는게 나은 것 같다.(실무에서도 2안을 사용했다)
실제 라이브될 코드는 바벨로 트랜스파일링 될거니까 동일한 환경에서 테스트를 해야하지 않을까?라는 생각에서 그렇게 결정했다.
테스트코드가 그래도 실패한다 왤까?
이유: window 객체의 메소드를 사용했기 때문
실무에서 테스트코드 관련 환경을 설정할 때와 비슷한 감정을 느끼기 시작했다.
하나 해결하면 또 나오는 에러들..

toast 를 띄우기 위해 사용했던 'react-hot-toast' 라이브러리에서 window.matchMedia 를 사용 중인데
jest 자체는 노드 환경에서 작동하기 때문에 해당 메소드가 존재하지 않기 때문에 에러가 발생했다.
공식 문서에도 나오는 오류이다. (링크)
해결방법은 간단하다. 모킹 해주면 끝!
// 1. 간단하게 해결
global.window.matchMedia = jest.fn();
// 2. 공식문서 예제코드
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
global.window.matchMedia = () => {}
이렇게 덮어써도 오류는 해결된다.
빠르게 에러 확인만 하려면 덮어쓰기만 해도 되겠지만 모킹함수 관련된 걸 사용하려면 2번처럼 함수 자체를 모킹하는게 맞는 것 같다.
2. Typescript 관련 이슈
isEmpty 로 데이터 존재 여부를 확인했는데 undefined 로 추론되는 이슈..
이유: 타입 파일을 설치하지 않았음
import { isEmpty } from 'lodash-es';
if(isEmpty(data)) {
return <div>데이터가 없습니다.</div>
}
console.log(data.something) // data 가 undefined 로 추론됨
과제할 때는 이 오류를 내가 못 본건지 안 뜬건지 모르겠지만.. 아래처럼 설치하면 해결된다
yarn add -D @types/lodash-es
useContexts 라는 커스텀 훅을 사용해 Context 존재 여부를 확인했는데 null 로 추론되는 문제..
이유: ts 버전 이슈
프로젝트 하면서 이런 이슈가 생기면 useContexts 를 사용해서 Context 존재 여부를 확인하면 괜찮았다.
그런데,, 이번에는 사용해도 null 이 함께 추론되었다.
/**
* Context 를 사용하는 유틸
* 범위 밖의 Context 를 사용하지 않게끔 도와주는 커스텀 훅
*/
const useContexts = <T>(Context: ContextType<T>, name = '') => {
const context = useContext(Context);
if (!context) {
throw new Error(`Cannot find ${name}Context`);
// Context.displayName 을 name 대신 사용해도 괜찮다.
}
return context
// 내 기존 프로젝트에서는 NonNullable<T> 로 추론;
// 과제 프로젝트에서는 그냥 T 로 추론
};
그래서 무엇이 다를까하고 봤을 때
typescript 의 버전이 다른걸 발견했다. 심지어 메이저 버전이 달랐다.
그리고 관련된 아티클을 본 것 같은 기억이 나서 찾아보았다.
typescript 버전을 4.8 이상으로 업그레이드 하니까 에러가 해결되었다.
4.8 버전의 changes 에 영향을 받아서 그렇다. (링크)
자나깨나 버전확인!
+) 여기서 에러가 또 안 나면 섭하다 ㅎㅎ
ts 변경사항(링크)
- 버전 이슈 때문에 버전을 올리면 관련된 다른 라이브러리들과의 버전 호환성도 확인해야한다.
관련된 라이브러리들을 업그레이드 해준다.
- @typescript-eslint/eslint-plugin
- @typescript-eslint/parser
React-query 를 Suspense 와 함께 사용할 때 타입 추론 안되는 문제
useQuery 의 suspense option 을 true 로 하고 React 의 Suspense 컴포넌트로 감싸주면,
원래는 api fetch 가 성공했을 때 해당 UI 가 렌더되는 것이기 때문에 당연히 data 가 undefined 면 안되겠지만,
undefined 가 포함되어 추론되고 있다. (이슈링크)
그렇다면 어떻게 해결해야할까?
1. 항상 옵셔널 chaining
- 이렇게 해왔지만 매번 옵셔널 chaining 하는 것도 귀찮고 코드도 난잡해 보인다.
2. React-query 최신 버전의 'useSuspenseQuery' 사용
- 과제 프로젝트에서는 괜찮지만 v5로 마이그레이션 해야하는 프로젝트에서는 버전을 쉽게 올릴 수 없다.
3. useSuspenseQuery 라는 hook 을 따로 만든다.
- 아래에 이어서 도전해보겠다..!
react-query use SuspenseQuery 의 실제 타입을 가져와봤다.
- 코드를 보면 알겠지만 enabled 와 suspense 를 강제하고 있다.
1. 대충 as 를 사용하여 따라해봤다.
일단 undefined 는 사라졌는데 단순히 타입을 강제한 것이기 때문에
option 에 suspense false 를 줘도 항상 ResType 으로 추론되어버린다.
-> 실제 데이터와 타입이 맞지 않으니 커스텀 훅을 하나 만들어야 할 것 같다.
type UseSuspenseQueryResult<TData = unknown, TError = unknown> = Omit<
DefinedQueryObserverResult<TData, TError>,
'isPlaceholderData'
>;
const {data} = useQuery({..생략}) as UseSuspenseQueryResult<ResType>
// data 의 타입에서 undefined 가 없어지고 ResType만 추론이 된다.
2. toss/slash 의 useSuspendedQuery (링크)
- 대체로 동일하지만 data가 non-nullable, error, isLoading, isError, isFetching 이 존재하지 않는 점, enabled false 를 사용할 수 있는 점이 달랐다. (아래 사진 참고)
3. 그러면 나는 어떻게 할까?
- 항상 진리의 프바프! 프로젝트의 상황에 따라 다른 방법을 선택할 것이다.
- 기존 프로젝트는 당장 마이그레이션 할 순 없어서 useSuspenseQuery 를 만들어서 사용하기로 결정했다.
새로운 프로젝트라면 ? v5 + react18 사용한다.
기존 프로젝트인데 마이그레이션 할게 너무 많으면? v4 + useSuspenseQuery 만들어서 사용한다.
구현 계획
enabled, suspense 를 true 로 강제해서 data 가 undefined 가 나오는 상황을 막겠다.
>> susepnse 를 사용하면서 enabled 를 쓰고 싶은 경우가 있지 않을까?
- 이에 대한 답변은 깃헙 이슈에 등록되어 있다.
그래도 나한테 필요한 것만 정리해보자면?
Dependent Query 의 경우
Suspense 가 감싸고 있는 하나의 컴포넌트에서 n개의 요청을 할 때 사진처럼 네트워크 병목이 발생한다.
이는 처음 요청한 api 가 끝나고 다음 api 가 호출된다는 것을 알 수 있다.
그렇기 때문에 다음 api 의 data 도 NonNullable 하다는게 보장이 된다.
https://tanstack.com/query/v5/docs/react/guides/dependent-queries
다른 API 의 결과가 특정 조건에 만족해야만 실행하는 쿼리의 경우
- enabled 를 사용하는 대신 컴포넌트를 나누거나 queryFn 에서 null 을 return 하라고 한다.
구현해보자
type UseSuspenseQueryResult<TData = unknown, TError = QueryError> = Omit<
DefinedQueryObserverResult<TData, TError>,
'isPlaceholderData'
>;
/**
*
* Suspense 와 함께 사용하는 useQuery 의 data 타입을 NonNullable 하게 추론되게 하는 useQuery 래핑 훅
*
* - suspense(true), enabled(true) 로 고정되어 있는 useQuery, NonNullable 한 데이터가 반환된다
* - React.Suspense 와 꼭 함께 사용해야한다.
*/
const useSuspenseQuery = <T extends (...args: any) => any, TData = Awaited<ReturnType<T>>>(
options: Omit<QueryOptions<T, TData>, 'suspense' | 'enabled'>
) => {
return useQuery({
...options,
suspense: true,
enabled: true,
}) as UseSuspenseQueryResult<TData>;
};
typescript 실력을 더 키워야 할 것 같다..
3. 버전 이슈
React-query 의 useQuery option 에 suspense 옵션을 넣었더니 타입 에러가 난 이유
원래 v4 를 사용했었고 v5 는 나온다는 말만 들었다가 까먹고 있었는데,,
react-query 가 v5 를 breaking changes 와 함께 들고 나왔다.
몰랐는데,, 아무튼 버전 확인을 잘 해야겠다.
마이그레이션 관련 글은 다른 포스팅에 올리겠다.
'내가 경험한 것 > 1.5' 카테고리의 다른 글
eslint config npm에 올리기 (0) | 2024.02.03 |
---|---|
prettier config npm 에 올리기 (0) | 2024.02.02 |
TanStack Query v5 의 Breaking Changes (2) | 2024.01.07 |
나를 괴롭히던 타입스크립트 - lodash 편 (2) | 2023.12.25 |
나를 괴롭히던 타입스크립트 - axios interceptor 편 (2) | 2023.12.23 |