TIL102. Next js의 pre-rendering과 hydrarion

조연정·2023년 5월 1일
1
post-thumbnail

next.js pre-rendering 방식의 종류와 hydration개념 및 관련 오류에 대해 정리해보자.

렌더링 방식(CSR, SSR)

CSR(Client Side Rendering): client에서 렌더링

유저가 웹사이트 방문 → 브라우저에서 서버로 소스(컨텐츠) 요청 → 서버에서 브라우저로 빈 html로 연결된 js 링크를 보내줌 → 브라우저에서 js 파일 다운로드 후 동적 dom 생성

CSR의 특징

  • 빈 HTML을 내려주기 때문에 서버 부하가 적다.
  • js까지 모두 다운로드 받은 후 동적 DOM을 생성하는 시간을 기다려야하기 때문에 초기 로딩속도가 느리다.
  • 처음에 모두 받아왔기 때문에 서버에 해당 페이지의 데이터만 받아오면 되서 페이지 전환이 빠르다.
  • 웹 크롤러봇는 html기반으로 요소를 찾는데 html이 비어있어서 정보를 얻을 수가 없기 때문에 SEO(검색엔진이 정보를 얻는 과정)에 불리하다.
    (*구글 크롤러 봇은 js로도 크롤링할 수 있지만, 국내 검색봇들은 html기반으로 요소를 찾음)

SSR(Server Side Rendering)

유저가 웹사이트 방문 → 브라우저에서 서버로 소스(컨텐츠) 요청 → 서버에서 브라우저로 렌더링 준비를 마친 html과 js code를 보내줌 → 브라우저는 전달받은 html 렌더하고, js code를 다운로드 후, html에 js로직을 연결

SSR의 특징

  • js 다운로드를 받기 전에 html을 먼저 렌더링하기 때문에 초기 로딩속도가 빠르다.
    (*하지만, 이 시점에 사용자가 버튼을 클릭하면, 인터렉션이 일어나지 않을 수 있다.)
  • SEO에 유리

Next.js의 렌더링 과정(hydrate)

CSR + SSR

: 첫페이지는 서버에서 받아 렌더링하고, 뒤에 발생하는 라우팅은 next/link, next/router 를 활용해서 내부적으로 CSR 방식을 사용함(url창에 직접 주소를 치고 들어갔을 시에는 SSR or SSG인데 link, router를 이용하면 CSR)

hydration

: Next.js는 클라이언트에게 웹페이지를 보내기 전에 server side 단에서 Pre-Rendering 된 HTML 파일을 클라이언트에게 전송한다. 이후에 리액트가 번들링된 자바스크립트 코드들을 클라이언트에게 전송한다. 자바스크립트 코드 이전에 보내진 HTML DOM 요소 위에서 한번 더 렌더링을 하면서 각자 요소들을 찾아가며 매칭힌다.이 과정을 통해 웹 페이지가 정상적으로 동작을 하게 되고, 이 과정을 바로 ‘Hydration’ 이라고 한다.

→ 맨 처음 응답받는 요소 document Type의 HTML 파일-> css 파일 → react 코드들이 렌더링된 js파일

서버 단에서 한 번 클라이언트 단에서 한 번 더 렌더링하면 비효율적이라고 생각할 수 있지만, pre-rendering한 document는 모든 js 요소들이 빠진 굉장히 가벼운 상태이므로 클라이언트에게 빠른 로딩이 가능하다.

*각 DOM 요소에 자바스크립트 속성을 매칭 시키기 위한 목적이므로 실제 웹 페이지를 다시 그리는 paint 함수는 호출하지 않는다.

관련 코드

// server/render.tsx

const renderDocument = async () => {
    // ...
    async function loadDocumentInitialProps(
      renderShell?: (_App: AppType, _Component: NextComponentType) => Promise<ReactReadableStream>
    ) {
      // ...
      const renderPage: RenderPage = (
        options: ComponentsEnhancer = {}
      ): RenderPageResult | Promise<RenderPageResult> => {
        // ...
        const html = ReactDOMServer.renderToString(
          <Body>
            <AppContainerWithIsomorphicFiberStructure>
              {renderPageTree(EnhancedApp, EnhancedComponent, {
                ...props,
                router,
              })}
            </AppContainerWithIsomorphicFiberStructure>
          </Body>
        );
        return { html, head };
      };
    }

ReactDOMServer.renderToString(element) : reactNode를 HTML 문자열로 반환

// CLIENT/NEXT.JS
 
function renderReactElement(domEl: HTMLElement, fn: (cb: () => void) => JSX.Element): void {
  //...
  const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete);

  // ...
  if (shouldHydrate) {
    ReactDOM.hydrate(reactEl, domEl);
    shouldHydrate = false;
  } else {
    ReactDOM.render(reactEl, domEl);
  }
}

ReactDOM.render(element, container[, callback]) : ReactElement 를 렌더링

ReactDOM.hydrate(element, container[, callback]) : DOM요소에 이벤트 리스너를 적용하고 렌더링을 진행

-> Next.js는 서버에서 HTML 문서를 문자열로 가져온 후, 클라이언트에서는 ReactDom.render 함수를 이용하여 ReactElement 를 렌더링하고, , ReactDOM.hydrate라는 함수를 이용하여 DOM요소에 이벤트 리스너를 적용하고 렌더링을 진행한다.

pre-rendering 종류

SSG(Static-Site-Generation)

: HTML을 빌드 타임에 생성한다. 그 이후에는 CDN으로 캐시되어지고, pre-render된 HTML은 매 요청마다 재사용 된다. 인터넷 연결이 느리거나 불안정한 경우에도 사용자에게 신속하게 제공될 수 있다.
ex)블로그 게시물, 제품 목록: 이커머스 사이트에서 상품 세부 정보, 포트폴리오

SSG 방식으로 데이터를 가져올 때 getStaticProps, getStaticPaths 사용한다.(*next.js 12 버전 기준)

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

export async function getStaticProps() {
// 빌드 시, api 호출해서 html을 만들어준다.
  const res = await fetch('https://.../posts');
  const posts = await res.json();

  return {
    props: {
      posts,
    },
  }
} 

export default Blog

SSR

: 매 요청이 일어날 때 마다 HTML을 생성하는 방식이다. 데이터가 자주 업데이트되는 페이지에 사용된다. CDN에 캐쉬를 저장하지 않아 SSG와 비교해서 속도는 느리지만 항상 최신상태를 유지할 수 있다.

function DynamicPage({ data }) {
 ...jsx
}

export async function getServerSideProps() {

  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // Pass data to the page via props
  return { props: { data } }
} 

export default DynamicPage

ISR(Incremental Static Regeneration)

: SSG의 장점을 살리면서 비교적 최신 데이터를 반영할 수 있다. SSG와 동일하게 빌드 시점에 페이지를 생성하지만, 일정 시간이 지난 후 페이지를 새로 생성한다. 빌드를 배포한 이후에도 설정한 주기마다 데이터의 갱신여부를 검사하고 업데이트된 데이터로 페이지를 다시 정적으로 생성한다. 빌드된 후 이 페이지에 재방문이 없으면, revalidate 시간이 경과했더라도 백그라운드에서 재 빌드되지 않는다.

export async function getStaticProps() {

  const res = await fetch('https://.../posts');
  const posts = await res.json();

  return {
    props: {
      posts,
    },
     revalidate: 15,
  }
}

hydration 관련 오류

서버에서 받아온 UI(pre-render)와 브라우저에서 자체적으로 렌더링한 UI tree 간의 차이때문에 hydration 오류가 발생한다. 구체적인 원인은 밑에 서술

원인

  • 환경에 따라 다른 ui 나타내는 코드

node 서버에서 실행 시에는 최상위 객체가 window가 없고, 브라우저에서 실행 시에는 최상위 객체 window가 존재하기 때문에 서로 환경이 다르다.

function MyComponent() {
  // This condition depends on `window`. During the first render of the browser the `color` variable will be different
  const color = typeof window !== 'undefined' ? 'red' : 'blue'
  // As color is passed as a prop there is a mismatch between what was rendered server-side vs what was rendered in the first render
  return <h1 className={`title ${color}`}>Hello World!</h1>
}
  • element 순서 안지킬 경우
<p>
  <div>This is test</div>
</p>

실제 겪었던 hydration 오류

실제 개발환경에서는 반응형을 위해 사용했던 ‘useMediaQuery’ hook 때문에 hydration 문제가 발생했다.useMediaQuery는 window.innerWidth 등과 같은 window 객체를 참조하는 코드를 가지고 있는데 서버에서 렌더링한 html에는 window 객체가 초기화되지 않았기에 오류가 났었다.

→ 컴포넌트가 마운트되고, 그 이후에 useEffect hook 내부에서 isMobile 변수를 업데이트시켜 컴포넌트를 재랜더링 시키는 방법으로 해결했다.

function App() {
    const [isMobile, setIsMobile] = useState(false);
    const mobile = useMediaQuery({ query: "(max-width: 600px)"});
    
    useEffect(() => {
    	if(mobile) setIsMobile(mobile);
    }, [mobile])
    
    return <div style={ { 
    	display: 'flex',		
        flexDirection: isMobile ? 'column' : 'row',
    } } />
}
profile
Lv.1🌷

0개의 댓글