마주한 상황
어느날 갑자기 페이지를 리로드 하니 간헐적으로, 무작위 하게 “인증되지 않은 사용자”라며 “다시 로그인 하세요.”라는 알림을 띄워준다… 심지어 다른 팀원의 PC 에서는 발생하지 않고 일부 PC 에서만 재현되는 상황이었다. 페이지 이동, 혹은 다른 상호작용 시에는 발생하지 않았다.
프로젝트의 구조(개요)
// App.tsx
<QueryClientProvider client={queryClient}>
<AppProvider>
<Router/>
</AppProvider>
</QueryClientProvider>
AppProvider 내부에는 아래와 같은 함수들이 있다.
// 1. 쿠키에서 authToken 을 가져온다.
// 2. 가져온 authToken 으로 로그인을 시도한다.
// 3. 로그인에 성공하면 권한별 메뉴를 가져온다.
const LoginByTokenFetch = async (
...
await getRoleMenu(authToken);
) => {};
const getRoleMenu = async (authToken) => {};
분석
- 우리 프로젝트는 페이지를 새로고침 하면 토큰을 새로 가져와서 인증을 하는 방식이다. 그러니 페이지를 이동하거나 다른 상호작용을 할 때는 기존에 토큰을 사용하면서 에러가 발생하지 않는다. 하지만 페이지 새로고침 시에는 새로운 토큰을 발급받으면서 현재 이슈가 발생한다.
- 개발자 도구를 통해 API 호출 순서를 확인했다. 순서는 ‘token → roleMenu → 기타 API 호출’ 이 정상적인 호출 순서인데, ‘token → 기타 API 호출 → roleMenu’ 같이 ‘기타 API 호출’ 이 먼저 실행되는 현상이 발생했다. 문제는 내 팀원의 PC 에서는 몇번을 테스트 해봐도 호출 순서가 꼬이지 않는다는 것이었다.
원인 예측
- 얼마 전 팀에서 리액트18 로 마이그래이션 한다고 코드를 수정한 것이 생각났다.
- App 컴포넌트, Router 등 기본적인 구조와 로직의 변경이 있었다.
- 잘 되던 프로젝트가 동작하지 않으니, 구조를 변경 하면서 생겨난 문제일 것이다.
해결 과정
1. 위의 예측을 바탕으로 프로젝트 구조에서 예상하지 못한 렌더링 이슈가 발생하는지 파악했다.
최상위 컴포넌트인 App 부터 최하위 컴포넌트인 Register 까지, 전혀 이상없이 예상한 렌더링 순서를 잘 지키고 있다.
2. 원인 예측과는 다르게 프로젝트 구조의 문제는 아니었던 것같다. 그래서 우리 프로젝트에서 발생하는 문제를 재현해보기로 한다.
AppContext 내부에서 ‘비동기 토큰 획득’ 함수와 ‘비동기 권한 함수’ 가 순서대로 호출해야 하는데, ‘비동기 토큰획득’ 함수를 실행시키고 AppContext가 종료되고, ‘비동기 권한 획득’ 이 실행되는 것을 볼 수 있었다. 토큰 획득과 권한 획득 사이에서 시간이 얼마나 걸리는지에 따라 다른 API 가 그 사이에 호출될 가능성을 눈치챘다. 이쯤 되니 “이건 라우터나 프로젝트 구조의 문제가 아니라 어떤 비동기 구조의 문제일 수 있겠구나…” 라고 깨달았다.
3. 프로젝트의 AppContext 컴포넌트 내부 함수 구조를 확인하고 흐름을 다시 파악했다.
// AppContext 내부 개요
useEffect(() => {
LoginByTokenFetch();
}, [])
const LoginByTokenFetch = async () => {
await getToken();
...
await getRoleMenu(authToken);
};
const getRoleMenu = async (authToken) => {...};
return (
<AppContext.Provider value={store}>
{children}
</AppContext.Provider>
)
처음 프로젝트 에러만 봤을 때는 이상함을 느끼지 못했는데, 문제를 재현하다보니 발생한 이슈에 대해서 이해할 수 있었다.
- useEffect 의 내부에서 LoginByTokenFetch 이라는 비동기 함수를 실행
- 비동기 함수를 실행 했으니, 그대로 useEffect 종료
- LoginByTokenFetch 내부에 getRoleMenu 비동기 함수가 중첩으로 실행
- LoginByTokenFetch 함수가 모두 실행되는 사이에 리액트의 lifeCycle 정상적으로 진행
- getRoleMenu 를 포함한 LoginByTokenFetch 함수가 모두 실행 되었는가?
- “예” → 정상적으로 페이지 렌더링 및 실행
- “아니오” → 함수가 종료되기 전에 라우터 내부의 함수가 실행되면서 호출 에러
4. 그래서 확실하게 AppContext 의 비동기 함수가 종료되고 컴포넌트를 이동하면 되겠다는 결론에 도달했다.
그래서 isLoading 이라는 state 를 이용해서 상태가 true 일 때 다른 컴포넌트를 렌더링 하도록 처리한다. 이렇게 하니 확실하게 AppContext 의 토큰과 권한을 얻어오고 내부의 컴포넌트를 보여준다.
// AppContext 내부 개요
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
getAuth();
}, [])
// 새로운 함수
const getAuth = async () => {
setIsLoaded(false);
const authToken = await LoginByTokenFetch();
getRoleMenu(authToken);
setIsLoaded(true); // 모두 완료되면 true
}
const LoginByTokenFetch = async () => {
await getToken();
...
// 비동기 중첩 삭제
};
const getRoleMenu = async (authToken) => {...};
return (
<AppContext.Provider value={store}>
{isLoaded && children}
</AppContext.Provider>
)
결론
처음에는 프로젝트의 구조 문제일 것이라고 판단해서 이슈 해결의 방향성에서 조금 벗어났다. 나혼자 섣부른 예측을 하고 거기에 너무 몰입 했던게 아닌지 되돌아보게 된다. 이번 기회를 통해 프론트엔드 개발자에게 있어서 에러를 파악하고 이해하는 능력과 그것을 적절히 조치할 수 있는 능력의 중요함을 느꼈다.
이번 이슈를 처리하면서 또 하나의 신기한 경험은 PC 의 성능 차이였다.
몇몇 PC 에서는 에러가 발생하는데 다른 PC 에서는 발생하지 않는 상황. 개인적으로 생각하기에 컴퓨터 성능의 차이로 브라우저의 실행 속도, 혹은 서버와의 통신 속도에서 차이가 발생하는 것같다… 덕분에 이런 치명적인 에러를 발견할 수 있었고, 나의 PC 에서만 해결할 수 있었다. 이런 경우는 생각치도 못했는데… 프론트엔드 개발자로서 한 발자국 더 앞으로 나아간 것 같아 내심 기쁘다.
'Development' 카테고리의 다른 글
Github Action S3 설정 에러 부서지고 부수기 (4) | 2023.10.18 |
---|---|
리액트 폴더 구조(Architecture) (0) | 2023.01.08 |
Chart.js 사용하기 (0) | 2022.12.19 |
도서 정리 프로젝트(4) (0) | 2022.06.26 |
도서 정리 프로젝트(3) (0) | 2022.06.13 |