[Next.js 13] 클라이언트 컴포넌트가 정말 클라이언트 사이드에서만 렌더링 될까 ?

◾️문제 발생

react-draft-wysiwyg 를 사용한 컴포넌트를 페이지 컴포넌트에 import했습니다. 페이지는 성공적으로 렌더링 되었지만 터미널에서 에러를 확인할 수 있었습니다. 빌드시에도 동일한 에러가 발생합니다.

image

use client를 최상단에 선언했었기에, 클리이언트 컴포넌라고 생각했고 서버 컴포넌트에서 날 법한 프리렌더링 에러가 발생하는 것이 이해가 가지 않았습니다.

◾️ 문제 원인

문제가 발생한 이유는 다음과 같습니다.

  • Next.js 13 컴포넌트는 서버에서 사전 렌더링되고 클라이언트에서 하이드레이션된다.
  • 따라서 use client 를 선언한다고해서 클라이언트에서만 렌더링 되는 것은 아니다.

컴포넌트에서 window객체에 접근하는데, 서버에서 사전 렌더링되는 과정에서 window객체를 찾지 못해 에러가 발생한 것이었습니다. 클라이언트 단에서만 실행되어야하는 코드는 서버에서 실행되지 않도록 설정이 필요합니다.

◾️ 문제 해결

✔️ 방법 1. dynamic import (동적 임포트)로 SSR 옵션 끄기

const NoticeForm = dynamic(() => import('@/components/notice/NoticeForm'), {
  ssr: false,
});

Next.js 의 dynamic import 기능을 활용해 서버 측 렌더링을 비활성화했습니다. dynamic import는 컴포넌트 외부에서 선언해야합니다.

다만 이 방법은 해당 에러가 발생하는 모든 컴포넌트를 동적으로 import해야하는 번거로움이 있었고, 에러 원인을 찾던 중 전역으로 설정해주는 방법이 있었습니다.

✔️ 방법 2. 컨텍스트를 사용한 custom hook 만들기

동적 임포트 매커니즘을 컴포넌트마다 반복적으로 구현하는 대신, 컨텍스트를 사용해 전역에서 관리하는 방법도 있습니다.

useEffect와 useState 훅을 사용한 커스텀 훅을 통해 클라이언트 환경을 실행조건으로 설정할 수 있었습니다.

이렇게하면 서버에서의 사전렌더링과 클라이언트의 첫번째 렌더링이 일치해 하이드레이션 오류도 방지할 수 있습니다.

💾 useIsClient.tsx

'use client';

import { createContext, useContext, useEffect, useState } from 'react';

const IsClientCtx = createContext(false);

export const IsClientCtxProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => setIsClient(true), []);
  return (
    <IsClientCtx.Provider value={isClient}>{children}</IsClientCtx.Provider>
  );
};

export function useIsClient() {
  return useContext(IsClientCtx);
}

💾 app/layout.tsx

import type { Metadata } from 'next';
import './globals.css';
import { IsClientCtxProvider } from '@/hooks/useIsClient';

export const metadata: Metadata = {
  title: 'Project',
  description: 'Generated by create next app',
};


export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <IsClientCtxProvider>
					{children}
        </IsClientCtxProvider>
      </body>
    </html>
  );
}

💾 사용예시

import { useIsClient } from './is-client-ctx';

function MyComponent() {
  const isClient = useIsClient();
  const scrollPosition = useScrollPosition(); // 예시를 위한 가상 훅

  return (
    <>
      {scrollPosition >= 0 && <FirstModule />}
      {isClient && scrollPosition >= window.innerHeight * 2 && <SecondModule />}
    </>
  );
}

useIsClient.tsx 훅을 통해 서버에서 렌더링할 수 있는 부분을 먼저 렌더링 후, 클라이언트 조건에 따라 업데이트 (두번째 렌더링)을 실행합니다. 이 방식을 통해 하이드레이션 오류를 피하면서 필요한 경우 클라이언트 측 업데이트를 수행할 수 있었습니다.

참고

Leave a comment