웹 프로그래밍

렌더링(Rendering)

2025-12-08

렌더링

렌더링은 작성한 코드를 실제 화면에 나타나는 사용자 인터페이스로 변환하는 작업을 의미합니다.

  • 서버에서 렌더링이 이뤄지는 것을 SSR(server side rendering)이라 함
  • 클라이언트에서 렌더링이 이뤄지는 것을 CSR(client side rendering)이라 함

렌더링 환경

렌더링 환경은 서버와 클라이언트가 있습니다.

  • 서버는 클라우드 환경 또는 직접 구축한 온프레미스 환경에서 작동하는 컴퓨터를 의미
  • 클라이언트의 요청을 받아서 처리하는 역할을 하는 것으로 실제 사용자가 사용하는 컴퓨터 또는 각종 모바일 디바이스를 의미하며, 서버에 요청을 보내는 역할을 함

일반적인 웹 개발 환경

클라이언트는 우리가 평소에 사용하는 브라우저라고 생각하면 됩니다. 일반적인 회사에서 서비스를 개발할 때는 서버를 담당하는 백엔드와 클라이언트를 담당하는 프론트엔드를 각각 나눠서 개발합니다. 이러한 경우 서버에서 사용하는 프로그래밍 언어나 프레임워크가 프론트엔드에서 사용하는 것과 다를 수 있습니다. 서버와 클라이언트의 환경이 완전히 다르기 때문에, 서로 통신을 할 때 규약을 잘 정하고 그에 따라 요청을 주고받아야 합니다.

  • 서버는 자바와 스프링 프레임워크를 사용해서 개발
  • 클라이언트는 자바스크립트와 리액트를 사용해서 개발

Next.js를 사용한 웹 개발 환경

Next.js를 사용하면 자바스크립트(또는 타입스크립트)라는 하나의 프로그래밍 언어를 사용해서 렌더링에 필요한 서버 쪽 코드와 클라이언트 쪽 코드를 모두 작성할 수 있습니다. 이처럼 서버와 클라이언트를 동일한 환경으로 개발하는 것은 인력이 적고 자금이 많지 않은 초기 스타트업에게 특히 유리합니다.

Request-Response 라이프 사이클

서버와 클라이언트가 통신하는 과정은 어떻게 될까요? 앞선 시간에 학습했던 것을 복습해보면 아래와 같습니다.

  1. 사용자가 브라우저를 통해 웹사이트에 접속을 시도하거나, 웹사이트에서 버튼 클릭, 양식 제출 등의 작업을 수행
  2. 브라우저는 사용자의 행동에 따라 서버에 HTTP 요청 전송
  3. 서버에서는 요청을 받아서 처리하고 클라이언트가 요청한 자원(예: HTML, CSS, 자바스크립트 등)을 HTTP 응답으로 보냄
  4. 클라이언트는 서버로부터 받은 자원을 이용하여 사용자 인터페이스를 렌더링

서버사이드 렌더링

서버에서 렌더링이 일어나게 된다면 어떻게 될까요? 서버에서 렌더링이 이뤄지는 경우에는 서버에서 완성된 HTML을 만들어서 응답으로 보내게 되고, 클라이언트는 응답으로 받은 HTML을 그대로 화면에 표시합니다. 하지만 이 과정은 그리 단순하지 않으며, 특히 리액트 서버 컴포넌트를 사용할 경우에는 렌더링이 되기까지 상당히 복잡한 과정을 거치게 됩니다.

네트워크 경게

네트워크 경계(network boundary)가 존재한다는 점을 이해해야 합니다.

  • 서버와 클라이언트 환경을 구분하는 것은 단순히 컴퓨터가 설치된 물리적인 위치가 아니라, 네트워크 경계라고 부르는 가상의 개념적인 경계선
  • Next.js에서는 'use client' 지시어를 사용하여 서버와 클라이언트의 경계를 정의할 수 있으며, 'use server' 지시어를 사용하여 서버 쪽에서 실행될 코드를 지정할 수도 있음

서버 컴포넌트

서버 컴포넌트는 2020년 12월 리액트 팀을 통해 실험적인 기능으로 처음 발표되었으며, 이후 2024년 12월에 출시한 리액트 19에서서 정식 기능이 되었습니다.

  • 서버에서 컴포넌트를 렌더링하고 그 결과를 클라이언트로 전송함으로써 클라이언트 측의 렌더링 부담을 줄이고 성능을 향상시키는 데 도움을 주는 기능
  • Next.js의 앱 라우터는 기본적으로 서버 컴포넌트를 사용, 개발자가 추가적인 구성을 하지 않고도 서버 렌더링을 자동으로 구현할 수 있고, 필요한 경우에는 ‘use client’ 지시어를 사용하여 클라이언트 컴포넌트 형태로 사용할 수 있음

React에서 서버 컴포넌트 렌더링 과정(a)

  1. 사용자가 웹 애플리케이션을 방문하거나 특정 사용자 요청이 발생하면 클라이언트는 서버에 컴포넌트를 렌더링하라는 요청을 보냄
  2. 서버는 클라이언트에서 요청된 페이지나 컴포넌트를 인식하고 해당 컴포넌트를 렌더링하기 시작
  3. 서버는 요청된 리액트 컴포넌트를 렌더링하며, 이 과정에서 서버 컴포넌트는 클라이언트 컴포넌트와 달리 서버에서 직접 실행

React에서 서버 컴포넌트 렌더링 과정(b)

  1. 서버 컴포넌트는 서버의 리소스(데이터베이스 쿼리, 환경변수 등)에 접근할 수 있을 뿐만 아니라 외부 API 또한 직접 호출할 수 있기 때문에, 필요한 데이터를 직접 가져와 컴포넌트 내부에서 처리
  2. 서버에서는 리액트는 서버 컴포넌트를 RSC Payload(react server component payload)라는 특수 데이터 형식으로 렌더링
  3. 서버는 생성된 RSC Payload를 클라이언트로 전송하며, 클라이언트에서 컴포넌트를 재구성하는 데 사용됨

React에서 서버 컴포넌트 렌더링 과정(c)

  1. 클라이언트는 서버에서 전송된 RSC Payload를 파싱하고, 이 데이터를 기반으로 서버 컴포넌트의 UI를 재구성함
  2. 자체적으로 렌더링한 클라이언트 컴포넌트와 결합하여 전체 UI를 완성

Next.js에서는 리액트 서버 컴포넌트 렌더링(a)

Next.js는 서버에서 리액트의 API를 사용하여 렌더링을 진행합니다. 렌더링 작업은 개별 라우트 세그먼트와 Suspense 경계(suspense boundaries)에 의한 청크(chunk)로 나뉩니다. 각 청크는 다음 두 단계로 렌더링됩니다.

  1. 리액트는 서버 컴포넌트를 RSC Payload 데이터 형식으로 렌더링합니다.
  2. Next.js는 RSC Payload와 클라이언트 컴포넌트 자바스크립트 명령어를 사용해 서버에서 HTML을 렌더링

Next.js에서는 리액트 서버 컴포넌트 렌더링(b)

  1. HTML은 라우트의 빠른 non-interactive preview를 즉시 표시하는 데 사용되며 이는 초기 페이지 로드에만 사용
  2. RSC Payload는 클라이언트 및 서버 컴포넌트 트리를 조정하고 DOM을 업데이트하는 데 사용
  3. 자바스크립트 명령어는 클라이언트 컴포넌트를 하이드레이션(hydration)하고 애플리케이션을 인터랙티브하게 만드는 데 사용

하이드레이션

  • 하이드레이션(hydration)은 서버에서 렌더링된 HTML에 클라이언트 측 자바스크립트를 연결하여 동적으로 동작하도록 만드는 과정
  • 이벤트 리스너를 DOM에 연결하여 정적 HTML을 하게 만드는 과정으로 애플리케이션이 사용자와 상호작용 가능하도록 만드는 과정이며, 백그라운드에서 하이드레이션은 hydrateRoot라는 React API를 사용해서 수행됨

렌더링 전략

서버에서 렌더링이 이뤄지는 방식은 정적 렌더링, 동적 렌더링, 그리고 스트리밍 이렇게 세 가지로 나눌 수 있습니다.

정적 렌더링

정적 렌더링(static rendering)을 사용하면 빌드 시 또는 데이터 재검증 후 백그라운드에서 라우트가 렌더링됩니다.

  • 결과는 캐시되어 콘텐츠 전송 네트워크(CDN)로 푸시될 수 있음
  • 이 최적화를 통해 사용자와 서버 요청 간에 렌더링 작업 결과를 공유할 수 있음
  • 정적 렌더링은 정적 블로그 게시물이나 제품 페이지처럼 라우트가 사용자별로 개인화되지 않고 빌드 시점에 알 수 있는 데이터가 있는 경우에 유용함

동적 렌더링

동적 렌더링(dynamic rendering)을 사용하면 요청 시점에 각 사용자에 대해 라우트가 동적으로 렌더링됩니다.

  • 동적 렌더링은 라우트에 사용자에게 맞춤화된 데이터가 있거나 쿠키 또는 URL의 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보가 있는 경우에 유용함

캐싱과 동적 렌더링(a)

캐싱된 데이터가 있는 동적 경로,대부분의 웹사이트에서 라우트는 완전히 정적이거나 완전히 동적인 것이 아니라 다양한 형태로 혼합되어 있습니다.

  • Next.js에서는 캐싱된 데이터와 캐싱되지 않은 데이터가 모두 포함된 동적 라우트를 가질 수 있음
  • RSC Payload와 데이터가 별도로 캐싱되기 때문에, 요청 시 모든 데이터를 가져올 때 성능에 미치는 영향에 대해 신경 쓸 필요 없이 동적 렌더링을 사용해도 됨

캐싱과 동적 렌더링(b)

동적 렌더링으로 전환되는 경우, Next.js는 렌더링 중에 동적 함수(dynamic functions) 또는 캐싱되지 않은 데이터 요청이 발견되면 전체 경로를 동적으로 렌더링하도록 전환합니다.

동적 함수 데이터 라우트 렌더링
사용 안 함 캐싱됨 정적 렌더링
사용함 캐싱됨 동적 렌더링
사용 안함 캐싱 안 됨 동적 렌더링
사용함 캐싱 안 됨 동적 렌더링

캐싱과 동적 렌더링(c)

특정 라우트가 완전히 정적으로 렌더링되려면 모든 데이터가 캐싱되어야 한다는 것을 알 수 있습니다. 반대로 캐싱된 데이터 가져오기와 캐싱되지 않은 데이터 가져오기를 모두 사용하는 경우에는 해당 라우트가 동적으로 렌더링됩니다.

  • 동적 함수를 사용하거나 데이터가 캐싱되어 있지 않은 경우에는 동적으로 렌더링 됨

캐싱과 동적 렌더링(d)

Next.js를 사용해서 개발하게 되면 개발자가 렌더링 방식을 일일이 선택할 필요 없이, 사용된 기능과 API에 따라 각 라우트에 가장 적합한 렌더링 전략을 Next.js가 자동으로 선택합니다.

  • 렌더링 전략에 대해서 신경 쓰지 않고 개발을 하면 됨
  • 특정 데이터를 캐싱하거나 재검증할 시기를 선택하고 UI의 일부를 스트리밍하도록 선택적으로 구현할 수는 있음

동적 APIs

동적 APIs(dynamic APIs)는 사전 렌더링 시점에서는 알 수 없고 요청 시점에만 알 수 있는 정보에 의존합니다. 이러한 API를 사용하는 것은 개발자가 이를 의도적으로 사용하고 있음을 나타내며, 해당 API를 사용하는 전체 경로가 요청 시점에 동적으로 렌더링되도록 설정됩니다.

대표적인 동적 APIs

  • cookies(): 서버 컴포넌트에서 HTTP 요청의 쿠키를 읽을 수 있게 해주는 비동기 함수
  • headers(): 서버 컴포넌트에서 HTTP 요청의 헤더를 읽을 수 있게 해주는 비동기 함수
  • connection(): 렌더링 과정에서 사용자 요청을 기다려야 하는지 여부를 나타낼 수 있게 해주는 비동기 함수
  • draftMode(): Draft Mode (Headless CMS의 preview 콘텐츠를 미리 볼 수 있는 모드)를 활성화 또는 비활성화 할 수 있게 해주는 비동기 함수

스트리밍

스트리밍(streaming) 서버에서 UI를 점진적으로 렌더링할 수 있습니다.

  • 작업이 완료되는 대로 청크로 분할
  • 사용자는 전체 콘텐츠의 렌더링이 완료되기 전에 페이지의 일부를 즉시 볼 수 있음
  • 기본적으로 Next.js 앱 라우터에 내장되어 있음
  • 초기 페이지 로딩 성능을 향상시킬 수 있으며 느린 데이터 페칭 때문에 전체 페이지 렌더링이 차단되는 현상을 개선할 수 있음

서버사이드 렌더링의 장점(a)

웹 애플리케이션에서 일반적으로 사용하는 렌더링 방식은 클라이언트 렌더링입니다. 하지만 서버에서 렌더링을 사용하게 몇가지 장점을 가집니다.

  • 서버 컴포넌트는 데이터 소스(데이터베이스, 파일)와 가까운 서버 환경에서 데이터를 다룰 수 있음
    • 데이터 가져오는 시간과 클라이언트에서 서버에 요청하는 시간이 줄어듬
    • 결과적으로 성능 향상

서버사이드 렌더링의 장점(b)

  • 민감한 데이터(API 키, 인증 토큰 등)와 로직을 서버에서만 관리 가능
    • 클라이언트에 노출되지 않으므로 보안성 향상
  • 서버 컴포넌트 렌더링 결과를 캐싱할 수 있음
    • 동일 요청 시 캐시 재사용 가능
    • 반복 렌더링/데이터 fetching 비용 줄여 성능 및 컴퓨팅 비용 절감

서버사이드 렌더링의 장점(c)

  • 클라이언트에서 필요한 자바스크립트 번들 사이즈 감소
    • 다운로드, 파싱, 실행해야 하는 JS 양이 줄어듦
    • 번들 사이즈 줄어들어 성능 향상
    • 느린 네트워크/저사양 디바이스 환경에서도 유리
  • 서버 렌더링 덕분에 클라이언트에서 별도 렌더링 없이 즉시 페이지 출력
    • 초기 페이지 로딩 속도 향상
    • FCP(first contentful paint) 등 웹 퍼포먼스 지표 개선

서버사이드 렌더링의 장점(d)

  • SSR(서버 사이드 렌더링)으로 HTML 바로 전달
    • 검색 엔진 크롤러가 페이지 쉽게 인식
    • SEO(search engine optimization) 효과

스트리밍 전송의 장점

스트리밍 방식을 사용하면 JS 파일을 청크단위로 분리해서 전송하기 때문에 서버에서 렌더링을 했을 때 얻을 수 있는 이점뿐만 아니라 청크 단위로 빠르게 처리할 수 있어서 사용성 및 성능 향상에 큰 기여를 하게 됩니다.

  • 뉴스 사이트: 헤드라인만 먼저 보여주고, 상세 기사나 이미지/댓글 등은 추후에 로드.​
  • 쇼핑몰: 상품 리스트를 먼저 보여주고, 하단 추천 상품·리뷰 등은 백그라운드에서 현재화.​
  • 대시보드: 유저별 데이터를 개별적으로 받아오면서 빠른 화면 전환을 지원.​

클라이언트 컴포넌트

클라이언트 렌더링(client rendering)은 클라이언트에서 렌더링이 이뤄지는 것을 의미합니다.

  • 클라이언트 컴포넌트를 사용하면 서버 컴포넌트에서는 사용할 수 없는 state, effects, 그리고 이벤트 리스너(event listener) 등 사용자와의 상호작용을 제공할 수 있음
  • 또한 클라이언트 컴포넌트에서는 지리적인 위치(geolocation)나 로컬 스토리지(localStorage)와 같은 브라우저 API에 접근할 수도 있음

클라이언트 컴포넌트 사용 방법

클라이언트 컴포넌트(client components)를 사용하려면 파일의 맨 위에 리액트의 'use client' 지시어를 추가하면 됩니다.

  • 'use client'는 서버 컴포넌트와 클라이언트 컴포넌트 모듈 사이의 경계를 선언하는 데 사용
  • 즉, 파일에 'use client'를 정의하면, 해당 파일에서 가져오는 모든 모듈(하위 컴포넌트 포함)이 클라이언트 번들의 일부로 간주됨

아래 코드는 버튼 클릭 횟수를 카운트하는 아주 간단한 형태의 클라이언트 컴포넌트 예시 코드입니다.

'use client';
import { useState } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count} 번 클릭</p>
      <button onClick={() => setCount(count + 1)}>클릭</button>
    </div>
  );
}
export default Counter;

클라이언트 렌더링을 사용하지 않은 경우

그렇다면 만약 ‘use client’ 지시어를 사용하지 않고 클라이언트 컴포넌트에서만 사용 가 능한 기능을 사용하면 어떻게 될까요? ‘use client’ 지시어 없이 onClickuseState()를 사용하는 경우 오류가 발생합니다.

  • ‘use client’ 지시어 유무에 따른 비교 앱 라우터의 모든 컴포넌트는 기본적으로 서버 컴포넌트이며, 서버 컴포넌트에서는 onClickuseState() 등의 API를 사용할 수 없음
  • Counter 컴포넌트에 'use client' 지시어을 사용함으로써 클라이언트 경계를 설정하고, Counter 컴포넌트 및 하위 모듈들이 클라이언트에서 작동해야 한다는 것을 리액트에게 알려줘야 함

리액트 컴포넌트 트리에서 여러 개의 ‘use client’ 진입점을 정의할 수 있습니다.

  • 이를 통해 애플리케이션을 여러 클라이언트 번들로 분할할 수 있습니다. 그러나 클라이언트에서 렌더링해야 하는 모든 컴포넌트에 ’use client’를 정의할 필요 없음
  • 경계를 정의하면 해당 경계 내에서 가져오는 모든 하위 컴포넌트와 모듈은 클라이언트 번들의 일부로 간주되기 때문에 가장 상단에 위치한 컴포넌트에만 경계를 정의해도 됨

Next.js에서의 클라이언트 컴포넌트 렌더링(a)

Next.js에서 클라이언트 컴포넌트는 요청이 전체 페이지 로드의 일부인지(애플리케이션의 초기 방문 또는 브라우저 새로고침으로 트리거되는 페이지 재로딩) 또는 후속 탐색의 일부인지에 따라 아래와 같이 다르게 렌더링됩니다.

  • 초기 페이지 로드를 최적화하기 위해 Next.js는 리액트의 API를 사용하여 클라이언트 및 서버 컴포넌트 모두에 대해 서버에서 정적 HTML 미리보기를 렌더링함
  • 즉, 사용자가 애플리케이션을 처음 방문하면 클라이언트가 클라이언트 컴포넌트 자바스크립트 번들을 다운로드, 파싱 및 실행할 때까지 기다릴 필요 없이 페이지의 콘텐츠를 즉시 볼 수 있음

Next.js에서의 클라이언트 컴포넌트 렌더링(b)

  • 서버에서는 리액트가 서버 컴포넌트를 클라이언트 컴포넌트에 대한 참조를 포함하는 RSC Payload라는 특수 데이터 포맷으로 렌더링함
  • Next.js는 RSC Payload와 클라이언트 컴포넌트 자바스크립트 명령어를 사용하여 서버에서 해당 라우트에 대한 HTML을 렌더링
  • 클라이언트에서는 서버로부터 받은 RSC Payload를 사용하여 클라이언트 및 서버 컴포넌트 트리를 조정하고 DOM을 업데이트함
  • 자바스크립트 명령어는 클라이언트 컴포넌트를 하이드레이션(hydration)하고 UI를 인터랙티브하게 만드는 데 사용

Next.js에서의 클라이언트 컴포넌트 렌더링(c)

  • 초기 페이지 로드 이후에 다른 페이지를 탐색할 경우, 클라이언트 컴포넌트는 서버에서 렌더링되는 HTML 없이 전적으로 클라이언트에서 렌더링됨
  • 클라이언트 컴포넌트 자바스크립트 번들이 다운로드되고 파싱됨
  • 번들이 준비되면 리액트는 RSC Payload를 사용하여 클라이언트 및 서버 컴포넌트 트리를 조정하고 DOM을 업데이트함

서버와 클라이언트 컴포지션 패턴

서버와 클라이언트 컴포지션 패턴(server & client composition patterns)은 서버 컴포넌트와 클라이언트 컴포넌트를 잘 교차해서 사용하기 위해서는 먼저 각 컴포넌트를 어떤 경우에 사용해야 하는지 잘 이해하는 것이 중요합니다.

서버와 클라이언트 컴포넌트 사용 사례

기능 서버 클라이언트
데이터 fetching V X
서버 자원에 직접 접근 V X
상호작용 및 이벤트 X V
브라우저 기반 훅 사용 X V

서버와 클라이언트 컴포넌트의 역할

  • 서버 컴포넌트는 주로 데이터를 직접 접근해서 가져오는 작업을 위해 사용됨
  • 클라이언트 컴포넌트는 주로 상태를 관리하거나 사용자와의 상호작용을 위해 사용됨

서버 컴포넌트 패턴

서버 컴포넌트 패턴(server components patterns)은 서버 환경에서 작동하는 컴포넌트이기 때문에 데이터베이스 등으로부터 직접 데이터를 가져오거나 백엔드 서비스에 직접 접근할 수 있습니다.

  • fetch()나 리액트의 cache() 함수를 사용해서 직접 데이터를 가져오는 형태로 구현
  • 이 중에서 fetch를 사용하는 방법의 경우 동일한 데이터에 대한 페칭이 불필요하게 여러 번 발생한다고 생각할 수 있는데, 이 부분은 리액트에서 fetch를 확장하여 자동으로 데이터를 메모이제이션(memoization)해주기 때문에 개발자가 신경 쓰지 않아도 됨 \(\to\) 이것을 리퀘스트 메모이제이션(request memoization)이라고 함
  • fetching 데이터가 아닌 경우에는 리액트에서 제공하는 cache() 함수를 사용해서 컴포넌트 간에 데이터를 공유할 수 있음
import { cache } from 'react';
import { getTotal } from '@/lib/statistics';

const cachedGetTotal = cache(getTotal);

interface MyComponentProps {
  numbers: number[];
}

function MyComponent(props: MyComponentProps) {
  const { numbers } = props;
  const total = cachedGetTotal(numbers);

  return <p>{`Total: ${total}`}</p>;
}

export default MyComponent;
  • 서버 쪽에서만 작동할 수 있거나 클라이언트 쪽에서만 작동할 수 있는 코드가 포함된 모듈의 경우 문제가 될 수 있음
  • 이러한 제약 조건을 강제로 설정할 수 있게 해주는 것이 바로 server-only 패키지임
export async function getData() {
  const res = await fetch('https://api.example.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  });
  return res.json();
}
  • server-only 패키지를 사용하면 서버 환경에서만 작동하는 모듈을 클라이언트 컴포넌트에서 사용하려고 할 때 빌드 타임 오류를 발생시킴으로써 에러를 사전에 방지할 수 있음
  • 클라이언트 컴포넌트에서 임포트할 경우 빌드 타임에 에러가 발생하게 됨
import 'server-only';

export async function getData() {
  const res = await fetch('https://api.example.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  });
  return res.json();
}
  • 클라이언트 환경에서만 작동하는 모듈을 위해서는 client-only 패키지를 사용하면 됨
  • 서버 컴포넌트가 등장하면서 클라이언트 환경에 맞춰 개발된 서드파티 패키지들을 임포트해서 사용하려고 할 때 오류가 발생

클라이언트 컴포넌트 패턴

서버 컴포넌트에서 데이터를 클라이언트 컴포넌트로 전달하고 싶은 경우가 있을 때는 서버 컴포넌트에서 클라이언트 컴포넌트로 props를 통해 데이터를 전달할 수 있습니다.

  • 서버에서 클라이언트 컴포넌트로 전달되는 props는 리액트에 의해 직렬화(serialization)가 가능해야 함
  • 리액트가 서버 컴포넌트에서 클라이언트 컴포넌트로 props를 전달할 때 JSON 형태로 직렬화하여 전송하기 때문
  • 그래서 props로 전달되는 모든 값은 JSON으로 변환할 수 있어야 하며 함수, 클래스 인스턴스, Symbol, BigInt, 순환 참조 객체 등 직렬화할 수 없는 값은 사용할 수 없음
const data = {
  pokemon: [
    {
      id: 1,
      name: '피카츄',
      type: '전기',
      description: '귀여운 전기 쥐 포켓몬. 볼에 전기를 저장한다!',
    },
    {
      id: 2,
      name: '꼬부기',
      type: '물',
      description: '등껍질이 튼튼한 물 포켓몬. 물대포가 특기!',
    },
  ],
  comments: {
    1: [
      { id: 1, comment: '피카츄는 정말 귀여워요!' },
      { id: 2, comment: '전기 공격이 최고~' },
    ],
    2: [
      { id: 3, comment: '꼬부기의 물대포는 멋져요!' },
    ],
  },
};

참고문헌