데이터 요청 최적화
로더로 페이지 진입 전에 데이터 요청해서 캐시하기
react-router-dom의 loader는 컴포넌트 라우팅 이전에, 데이터 패칭을 진행 한 후 응답값을 컴포넌트에 전달한다. 나는 클라이언트에서 이 응답값을 활용하고자 하였기에, loader에서 데이터 패칭을 하고 그 값을 캐시하여 활용하면 페이지 렌더링보다 데이터 패칭을 먼저할 수 있어서 빠를 것이라고 생각했다.
loader의 데이터 요청 => 렌더링 => 캐싱 => 마운팅
react-router-dom에서 url을 전달받고 요청을 보낸 후 컴포넌트 안에서 tanstack query의 initialData에 응답값을 넣어주면 캐시를 할 수 있다고 믿었다. 그러나 네트워크 요청이 loader에서 한번, tanstack query에서 한 번 총 두번 진행되고 있었다. 처음에는 캐시의 문제라고 생각해서 객체로 들어간 queryKey를 수정해봤다. 하지만 내부적으로 JSON.stringify()를 사용하고 있어서 객체로 지정해도 같은 키로 간주하고 있었고 실제로 devTools에서도 캐시되고 있었다. 즉 initialData로 인해 캐시가 되기는 하지만, initialData는 첫 번째 네트워크 요청이 끝날 때까지 UI에 의미 있는 데이터를 보여주며 대기하는 역할이기 때문에, 네트워크 요청이 두번 진행되는 것이었다.
loader의 데이터 요청 => 렌더링 => loader 데이터로 마운팅 => tanstack query의 데이터 요청 => 리렌더링
액션에 따라 미리 캐시하고 캐시 데이터 사용하기
이렇게 되면 결국 loader의 대기시간에 보여지지 않는 것은 마찬가지여서 로더에서의 데이터 패칭을 제거하고 placeholder를 통해 스켈레톤 데이터를 제공하였다. 그러나 관리자 페이지였기 때문에 스켈레톤은 불필요하다고 생각해 제거해 결국 원상복구되었다. 대신 로더와 같이 데이터 요청의 시점을 당길 수 있는 방법을 찾게 되었다. 유저의 흐름을 생각하다보니 로더가 라우팅의 변경에 의해 트리거 되듯이 네비게이션을 통해서만 페이지를 진입하고 있었다. 따라서 네비게이션을 클릭할 때 데이터 패칭을 진행해 캐시하고 라우팅 후에는 캐시된 데이터를 활용하였다. queryClient.prefetchQuery를 사용하면 컴포넌트 외부에서 미리 패칭하여 캐시할 수 있다.
네비게이션 클릭 => prefetch => 페이지 렌더링 => prefetch 데이터로 마운팅
이를 통해 데이터 패칭 과정이 최적화되어 다른 페이지 진입 속도가 73ms에서 17ms로 줄어들었다.
권한 확인 최적화
로더를 통해서 권한 확인하기
그러나 문제는 다시 로더로 돌아와서, 모든 페이지에서 권한이 있는지 확인을 해본 후 없을 때는 로그인 페이지로 넘겨줘야했다. 일단 최선은 토큰이 없으면 로그인 페이지로 돌리고, 토큰이 있으면 유효한지 데이터 요청을 한번 거치는 과정이 필요했다. 토큰이 유효하지 않다면 데이터패칭 인스턴스에서 에러로 잡아서 로그인 페이지로 돌려줄 수 있었지만, 유효할 때의 최초 페이지 진입 속도에 영향을 미쳤다.
페이지 요청 => 스토리지 확인(아니면 로그인) => 권한 여부 데이터 요청(아니면 로그인) => 페이지 진입
이 때 처음으로 로더의 무용함을 깨달았다. 결국에 로더를 통해서 확인하나, hook을 통해서 확인하나 같은 진입 속도가 똑같다고 생각했다. 페이지 지연이 생기는 건 마찬가지였고 오히려 hook이나 상태에 따라서 판단하는게 선언형 UI 적이었다. 바로 공식문서가서 찾아보니 They are only called on the server when server rendering or during the build with pre-rendering,,
로더는 서버 사이드 렌더링에서 유용하다
생각해보면 클라이언트 사이드에서 데이터 요청을 미리하는 게 무의미한게 당연한데, 그걸 놓치고 계속 돌고 있었다. 서버 사이드 렌더링은 라우팅이 서버에서 처리된다. 클라이언트가 서버에게 URL 요청을 보내면 해당 HTML을 생성하여 클라이언트에게 전송된다. 따라서 로더를 통해 데이터를 미리 받아와서 완성된 페이지를 응답할 수 있다. 이 로더를 클라이언트에서 사용하게 되면, 최초의 페이지 진입이 된 후 로더를 통한 데이터 페이지 요청이 진행되고 이 페이지에서 전환되는 것이기 때문에 사용되지 않는다. 이전에 내가 동작한다고 느꼈던 것은 페이지 전환을 통한게 아니라 새로고침을 통해서 확인했기 때문이었다.
즉, 결국에는 로더를 useEffect 훅처럼 사용하고 있었다. 그렇지만 마운팅 => useEffect로 데이터 요청 후 리렌더링 => 마운팅 과정보다는 로더 => 마운팅 이기 때문에 빠를 수 있다. 하지만 이것은 클라이언트 사이드의 장점을 전혀 활용 못하는 것이다.
가상 DOM 실행 전에 데이터 요청하기
하지만 useEffect를 판단하고 렌더링을 시키는 것은 그냥 모든 데이터 요청에서 권한을 확인하는게 더 낫다고 생각했다. 그래서 네비게이션처럼 미리 데이터 요청을 할 수 있는 방법을 찾아보기 위해 성능 탭을 통해 렌더링 과정을 더 자세하게 알아보았다.
브라우저의 동작 과정
- HTML 문서 파싱: 페이지 요청 후 HTML 응답 및 파싱 시작
- HTML 문서의 각 태그를 해석하여 DOM 트리 형성
- DOMContentLoaded 이벤트: HTML 문서가 완전히 파싱되어 DOM 트리가 형성되었을 때 발생, 외부 리소스는 로드되지 않을 수 있음. 스크립트가 HTML 문서와 자바스크립트 코드가 완전히 로드되고 실행될 준비가 되었을때 트리거.
- 외부 리소스 로딩: CSS, 이미지, iframe, 스크립트 등 외부 리소스 로드 후 스타일 적용
- 렌더링(페인팅): DOM과 CSSOM을 결합하여 계산된 레이아웃을 바탕으로 실제 화면에 요소를 그림.
- requestAnimationFrame: 화면을 다시 그릴 준비가 되었을 때 호출
- 렌더링될 때 자바스크립트 코드 실행. DOM을 수정하거나 페이지의 상태를 업데이트 가능
- 리액트의 경우) 가상 DOM 트리 생성 후 업데이트 시 각 컴포넌트 렌더링
여기서 DOMContentLoaded 이벤트는 HTML 문서가 파싱되어 DOM 트리가 형성된 후, 그러나 자바스크립트 코드는 실행되지 않은 시점에 실행된다. 즉, 자바스크립트의 전역 컨텍스트가 생성되고 평가되는 과정 중 이벤트 리스너가 등록되며 실행 전에 발생하는 이벤트이다. 그래서 가상 DOM이 생성되기 전에 네트워크 요청을 보내 권한을 판단해보았다. APP.tsx에서 데이터 요청을 하고, DOM Content Load 시점에 데이터 요청을 진행하는 것이다.
여기서 중요한 지점은 자바스크립트 코드가 언제 실행되느냐 이다. 'DOMContentLoaded' 발생 시점이 가상 DOM 생성 이후라면 데이터 요청을 하는 이유가 없다. 따라서 defer 속성 등으로 자바스크립트 실행을 HTML 파싱 완료 이후로 지정해주어야했다. 하지만 굳이 defer 속성을 따로 지정해주지 않아도 네트워크 요청이 먼저 가고 있었다.
이 이유는 type=module에 있었다. 자바스크립트 파일이 모듈 스크립트로 처리할 때는 defer와 비슷하게 HTML 파싱을 차단하지 않고 HTML 파싱이 완료된 후, DOM이 완전히 준비된 상태에서 실행된다. 참고로 defer와 다른 점은 로드된 순서대로 실행된다는 점이다.
변경 전: 페이지 진입 후 네트워크 요청을 통해 권한 판단

변경 후: 스크립트 실행 전 권한 판단을 스토리지에 저장 후 판단

네트워크 요청 속도가 일정하지 않지만, 구문 분석부터 대시보드 페이지가 완전히 나오기까지의 시간이 803ms에서 473ms로 줄었다.