가수면
Next.js 심화 본문
기본 동작
use client를 사용한 클라이언트 컴포넌트라도 초기 페이지 로드 시에는 서버에서 생성된 html파일을 받아온다. 실제로 개발자 도구에서 받아온 페이지의 요소들을 확인해보면 클라이언트 컴포넌트 안에 있는 요소들도 다 포함이 되어있다. 초기 페이지 로딩 성능을 최적화하려는 Next의 최적화 전략이다.
문제는 이러한 SSR + CSR를 혼합한 방식으로 인해 클라이언트 컴포넌트에 몇 가지 제약이 생긴다는 것이다.
1. 초기 렌더링 시 어떤 요소를 보여줄 건지 결정하는 로직들이 사용 불가능하다.
서버 사이드 렌더링된 컨텐츠와 클라이언트 사이드에서 렌더링하려는 컨텐츠가 다르면 안 된다.
예를 들어 쿠키, 리액트 쿼리의 isLoading 등을 이용해 초기 뷰를 분기 처리하려는 경우 수화 과정에서 받아온 HTML과 불일치 오류가 발생한다.
"use client"
...
const { data, isLoading, isError } = useQuery({
queryKey: [QUERY_KEY.userInfo],
queryFn: () => authApi.getUserInfo(),
});
...
{isLoading ? <div>로딩중</div> : <button>버튼</button>}
간단하게 위와 같은 예시 코드가 있다고 해보자.
초기 서버에서 렌더링하는 HTML은 정적이므로 isLoding 값이 false가 되어 버튼을 받아올 것이다.
그러나 실제로 클라이언트 컴포넌트가 클라이언트 사이드에서 렌더링되어 마운트가 발생한다면, isLoading의 초기 값이 true가 될 것이므로 '버튼'으로 받아온 HTML이 '로딩중'과 충돌하여 수화 불일치 오류가 발생하게 된다. 이는 isLoading 대신 getCookie한 것을 통해 분기처리 해도 마찬가지다.
클라이언트 컴포넌트가 SSR과 CSR의 경계에 있기 때문에 발생하는 오류다.
2. localStorage, sessionStorage 사용이 제한적이다.
앞서 말했듯 클라이언트 컴포넌트라도 서버 사이드 렌더링 과정을 거치게 된다.
그렇기 때문에 클라이언트 사이드에서만 접근 가능한 API를 사용하게 되면 서버 사이드에서 호출하는 과정에서 참조 오류가 발생하게 된다.
"use client"
...
<div>{sessionStorage.getItem("username")}<div>
예를 들어 위와 같은 예시 코드가 있다고 해보자.
서버 사이드에서 HTML을 렌더링할 때 세션 스토리지를 참조를 시도하는데 세션 스토리지는 브라우저에 있으니 당연히 참조 오류가 발생하게 된다.
이 때문에 클라이언트 사이드에서만 실행되는 것이 보장되는 useEffect 안에서(마운트 되어 실행되는 시점) 호출하거나, typeof window이 "undefined"인지 아닌지 비교해서 분기하는 방법을 사용해 서버 사이드에서 호출되지 않도록 분기해야 한다.
그리고 이러한 문제들은 아래 추가적으로 lazy loading의 dynamic을 통해서도 해결 가능하다.
추가적인 정보) 그렇다면 어째서 sessionStorage와는 다르게 import된 라이브러리나 라이브러리를 사용한 로직에서는 오류가 발생하지 않는가?
보통 사용되는 로직들의 경우 React 내부에서 정의된 상태나, 서버에서도 처리할 수 있는 로직을 통해 생성된 값들이다. 또한, import된 라이브러리들은 Node.js 환경(서버 사이드)에서도 실행될 수 있는 코드로 구성되어 있거나, 빌드 과정에서 클라이언트 사이드와 서버 사이드에서 실행될 수 있는 방식으로 변환되기 때문에 오류가 발생하지 않는 것이다.
Lazy Loading
일반적으로 next에선 자동으로 lazy loading이 적용된다.
next/dynamic
React.lazy()와 Suspense를 합친 개념. lazy loading이 자동으로 적용되는 next에서 lazy loading할 수 있는 기능이다.
그럼 lazy loading이 이미 자동으로 되는데 이걸 어디다 쓰느냐
1. 조건부 렌더링
조건이 충족되었을 때에 비로소 로드된다.
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
// Client Components:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
export default function ClientComponentExample() {
const [showMore, setShowMore] = useState(false)
return (
<div>
<ComponentA /> // 즉시 로드되지만, 코드 스플리팅 적용된 상태
{showMore && <ComponentB />} // 조건 충족되면 로드
<button onClick={() => setShowMore(!showMore)}>Toggle</button>
</div>
)
}
2. SSR에서 제외시키고 싶은 때
const ComponentB = dynamic(() => import('../components/B'), { ssr: false })
컴포넌트가 서버에서 렌더링되지 않아야 하지만, 클라이언트에서는 필요한 경우에 사용.(본 글 기본 동작 파트의 2번에 해당한다.)
클라이언트 전용 기능이나 라이브러리를 사용할 때 유용하다.
const GridLoader = dynamic(
() => import('react-spinners').then((lib) => lib.GridLoader), { ssr: false }
);
3. suspense 대신 사용
예를 들어 로딩 컴포넌트가 로딩되는 도중 보여줄 로딩(...)을 설정할 수 있다.
const WithCustomLoading = dynamic(
() => import('../components/WithCustomLoading'),
{
loading: () => <p>Loading...</p>,
}
)
'React > Next.js' 카테고리의 다른 글
Next 12버전과 13버전 다른점 정리 (0) | 2023.07.24 |
---|---|
React 앱을 Next로 마이그레이션 하기 (0) | 2023.07.09 |
라우팅 심화 (0) | 2023.06.12 |
리액트에서 사용했던 기능들 (0) | 2023.06.07 |
API 백엔드 로직 (0) | 2023.06.06 |