현재 면접연습서비스인 Gomterview Git를 개발 중입니다. 이전의 제 포스팅 중 하나인 createBrowserRouter 를 통한 라우터 처리에서 확인할 수 있듯, 저희 서비스는 createBrowserRouter를 통해서 router처리를 진행하고 있습니다.
이번 글은 해당 createBrowserRouter를 통한 로직에서 발생한 문제와 이를 해결한 방식을 담고 있습니다!
저희 프로젝트에서는 다음과 같이 CreateBrowserRouter 기반의 router를 설정해주기 위해 다음과 같은 코드를 작성해 주었었습니다.
다음과 같이 path로 진입하려하는 경우 element의 <InterviewVideoPublicPage />
로 이동시키며, loader는 페이지에 진입하기 전 필요한 데이터를 load하고, 실패한다면 redirect 시켜주기 위해서 사용되었습니다.
저는 지금 사진과 같이 InterviewViedoPublicLoader에서 useIsAllSuccess라는 hooks를 실행시켜서 현재 전역으로 설정한 recoil 상태값을 바탕으로 setting이 모두 정해졌다면, 해당 isAllSuccess의 유무에 따라서 페이지의 routing을 설정해 주려 했습니다.
하지만 다음과 같은 에러가 useIsAllSuccess에서 발생했습니다.
React Hook "useIsAllSuccess" is called in function "interviewVideoPublicLoader" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".eslintreact-hooks/rules-of-hooks
해당 에러는 interviewVideoPublicLoader라는 함수가 React Router v6의 loader 함수 내에서 useIsAllSuccess라는 React 훅을 호출하고 있다면, 이는 React의 훅 사용 규칙을 위반하기 때문에 발생한 문제였습니다.
React Router의 loader 함수는 React 컴포넌트가 아니므로, 이 안에서 React 훅을 사용할 수 없습니다. 이는 React의 기본 규칙 중 하나로, 훅은 오직 React 함수 컴포넌트나 다른 사용자 정의 훅 내에서만 호출될 수 있습니다.
함수형 컴포넌트나 user Custom Hooks 내부 에서만 호출이 가능합니다. React 훅은 반드시 함수 컴포넌트나 사용자 정의 훅 내에서만 호출되어야 합니다. 일반 함수, 클래스 컴포넌트의 메서드, 이벤트 핸들러, 타이머 콜백 등에서는 훅을 사용할 수 없습니다.
이를 해결하기 위해선 아래와 같은 방법을 사용해야 했습니다.
즉 recoil 기반의 hooks를 통해서 routing을 처리하기 위해선 페이지 내부로 이동해서 해결해야 합니다.
우리는 위에서 이런 결론을 내릴수 있었습니다. "recoil 기반의 hooks를 통한 페이지 예외처리는 createBrowserRouter와 같은 외부에서는 동작이 불가하다. 즉 페이지 내부에서 해결해야한다." 라는 결론을 내렸습니다.
아래는 제가 예외처리를 진행하고 싶은 Interview 를 진행하는 InterviewPage 컴포넌트 입니다.
해당 컴포넌트는 다음과 같이 router 처리를 진행했습니다.
const InterviewPage: React.FC = () => {
const { method } = useRecoilValue(recordSetting);
const { currentQuestion, getNextQuestion, isLastQuestion } =
useInterviewFlow();
const [stream, setStream] = useState<MediaStream | null>(null);
const [isRecording, setIsRecording] = useState(false);
const [isScriptInView, setIsScriptInView] = useState(true);
const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]);
const [selectedMimeType, setSelectedMimeType] = useState('');
const [interviewIntroModalIsOpen, setInterviewIntroModalIsOpen] =
useState<boolean>(true);
// .... 굉장히 많은 비지니스 로직...
return (
<InterviewPageLayout>
<InterviewHeader
isRecording={isRecording}
intervieweeName={intervieweeName}
/>
<InterviewMain
mirrorVideoRef={mirrorVideoRef}
isScriptInView={isScriptInView}
question={currentQuestion.questionContent}
answer={currentQuestion.answerContent}
/>
// .... 굉장히 많은 컴포넌트 들...
다음과 같이 페이지 컴포넌트를 보면 수많은 비지니스 로직을 담고 있습니다. 하지만 이 페이지에는 적절한 권한을 갖춘 유저만이 접근할 수 있어야 합니다.
그래서 구현한 방식은 다음과 같습니다!
제가 생각한 로직은 페이지 상단에서 isAllSuccess라는 boolean 요소로 인해서 페이지의 라우팅을 결정하는 것이었습니다.
하지만 이때 다음과 같은 문제가 발생하게됩니다.
이 경고 메세지는 navigate 함수를 useEffect외부에서 사용했기 때문에 발생한 문제입니다.
React 컴포넌트는 여러 라이프사이클 단계를 거치며 렌더링됩니다. 초기 렌더링 시에는 컴포넌트가 DOM에 처음으로 마운트되는 단계입니다. 이 단계에서 사이드 이펙트(예: 데이터 가져오기, 타이머 설정, 네비게이션 등)를 실행하면 예상치 못한 문제가 발생할 수 있습니다. useEffect 훅은 컴포넌트 렌더링 이후에 사이드 이펙트를 수행하기 위한 React의 방법입니다. useEffect 사용하면 React가 DOM 업데이트를 완료한 후에 사이드 이펙트를 안전하게 실행할 수 있습니다.
컴포넌트가 처음 렌더링될 때 navigate()를 직접 호출하면, 렌더링 과정이 완전히 끝나기 전에 페이지 네비게이션이 발생할 수 있습니다. 이는 렌더링 사이클을 방해하고, 예상치 못한 렌더링 이슈나 버그를 초래할 수 있습니다. useEffect 내에서 navigate()를 호출하면, 컴포넌트가 화면에 완전히 마운트된 후에 네비게이션이 이루어집니다. 이는 더 안정일 수 있으며, 렌더링 React의 렌더링 사이클에 side effect를 부과하지 않습니다.
따라서 다음과 같이 수정할 수 있습니다!
다음과 같이 수정하면, 위와 같은 경고가 사라지게 됩니다.
하지만 여기까지 진행해보면 살짝 신경 쓰이는 점이 있습니다.
컴포넌트가 화면에 완전히 마운트된 후에 네비게이션이 이루어집니다.
그렇다면 하위 컴포넌트가 무수히 많다면...?
그 무수히 많은 컴포넌트의 렌더링이 모두 마친 후에야 navigate가 동작하게 된다면, 이는 크나큰 문제를 일으킬 수도 있습니다.
물론 UI 상에서 막는 방법도 있으나 User가 직접 url을 입력한 경우에는 대처하지 못합니다. (사실 이를 극복하기 위해 createBrowserRouter의 loader 를 사용하려 했습니다.)
그렇기 때문에 가장 적은 비용으로 최적화가 필요했습니다.
사실 아주 간단한 해결책이 있었습니다. React Router에서 제공하는 Navigate 컴포넌트는 이러한 상황에 적합한 해결책을 제공합니다. Navigate 컴포넌트는 JSX 내에서 조건부 렌더링을 통해 사용됩니다. 이는 useEffect와 navigate 함수를 사용하는 것보다 선언적이고 직관적인 방법을 제공합니다!
무엇보다 InterviewPageLayout에 해당하는 컴포넌트들을 모두 렌더링 하지 않는다는 점은 꽤나 매력적입니다.
사실 여기까지 잘 읽으셨다면, 아직 문제가 있다는 점을 정말 쉽게 눈치 채실 수 있습니다.
이렇게 함수형 컴포넌트의 하단의 return 구문에서 Navigate를 반환하게 된다면, 아래의 컴포넌트들을 반환하기 전에 Navigate 가 동작하게 되지만, 해당 컴포넌트의 모든 비지니스 로직이 동작하게 됩니다.
그렇다고 이를 막기 위해서 최상단에서 Navigate를 반환해 버린다면, rule of hooks 라는 경고 걸리게 됩니다. 해당 경고는 hooks가 호출되는 시점에 대한 규칙을 의미하는데, 여기서는 상단에 Navigate를 반환해 비지니스 로직 부분의 hooks가 호출이 되지 않을 수 있어 발생하는 경고 입니다.
따라서 완벽하진 않지만 적은 비용으로 작지 않은 문제를 해결 근처까지 도달한듯합니다.