카카오_구름/리액트

리액트 심화 커리큘럼 1주차 : Next.js App Router에서의 렌더링 전략 이해

코딩의 세계 2025. 6. 6. 16:12

목표

  • Server Component와 Client Component의 차이를 구분할 수 있다.
  • Next.js의 fetch() 사용 위치와 옵션에 따라 어떤 렌더링 전략이 적용되는지 설명할 수 있다.
  • SSR, SSG, ISR, CSR의 개념과 차이를 실제 코드 예제를 통해 체감할 수 있다.
  • 어떤 상황에서 어떤 렌더링 전략이 적절한지 판단할 수 있다.

참고 키워드

  • cache: 'force-cache' / 'no-store'
  • next: { revalidate: N }
  • 'use client'와 fetch
  • Server Component의 async 동작
  • HTML에 포함되는 시점, 클라이언트 JS 필요 여부

공유 방식

  • 발표 or 코드 예제 (5분 내외)
  • 예제 페이지 1개 이상 (/ssr, /csr, /isr 등)

Server Component와 Client Component의 차이를 구분할 수 있다.

서버 컴포넌트 / 클라이언트 컴포넌트에 대한 공식 문서 >
https://nextjs.org/docs/app/getting-started/server-and-client-components
next.js에서는 서버 컴포넌트와 클라이언트 컴포넌트를 지원해 준다.

공식 문서의 서버 / 클라이언트 구성 요소

기본적으로 next.js의 레이아웃/페이지는 서버 컴포넌트로 이루어져 있다.
서버에서 데이터를 가져올 수 있고, 선택적으로 결과를 캐싱할 수 있다.
그리고 당연히 클라이언트 컴포넌트도 사용할 수 있다.
그렇다면 이 두 개의 차이는 무엇일까?


먼저 서버 컴포넌트는 서버 측에서 실행되는 컴포넌트이다. 서버 측에서 미리 자바스크립트 코드를 가져온다. 이후 HTML 코드를 만든 후에 클라이언트로 보내주는 형태가 된다. 이는 빠른 페이지 로딩을 가져오고 구글링 검색 노출을 유리하게 만든다. 서버에서 HTML만 전송하고 클라이언트에 보내주기에 클라이언트 JS를 꺼도 화면 표시가 된다. (js 번들이 필요 없다.)
클라이언트 컴포넌트는 클라이언트 측에서 실행이 되는 컴포넌트이다. 이 컴포넌트는 기본적으로 브라우저에서 실행이 되는 자바스크립트 코드로 실행된다. 이미 서버에서 렌더링 된 HTML을 받아와서 동적을 처리 가능하며 상호 작용한다. 다만, 서버 컴포넌트보다는 로딩 시간이 더 걸리게 된다. (상호작용 로직을 위해서 js 번들이 필요하다.)
결국 요약을 좀 해보자면, 렌더링을 "어디서" 하는가의 차이라고 볼 수 있을 것 같다. (렌더링을 서버에서 하냐.. 클라이언트에서 하냐..)
https://youtu.be/jYJ3ygUfPrU?si=UNZ9haR89YmEcY4d

코딩 애플

(참고한 영상)

언제 위 요소를 쓸까

구성 요소를 쓸 때에서도 차이점이 발생하게 된다. 이 부분이 공식 문서에서도 나온다.

공식 문서의 서버 / 클라이언트 구성 요소

클라이언트 컴포넌트는 다음 상황에서 사용하게 된다.

  • 상태 및 이벤트 핸들러에서 사용한다. 예) onClick / onChange 등
  • 생명 주기. 예) useEffect
  • 브라우저 전용의 API. 예) localStorage / window / Navigator.geolocation 등
  • HOOK. 예) useState, useMemo -커스텀 훅도 포함- 등등

서버 컴포넌트는 다음 상황에서 사용하게 된다.

  • 소스에 가까운 데이터베이스나 API에서 데이터를 가져올 때.
  • 클라이언트에게 공개하지 않고 API 키, 토큰 및 기타 비밀을 사용하고 싶을 때.
  • 브라우저에 전송되는 자바스크립트 양을 줄이고 싶을 때. (use client 남발 금지..)
코드

개념적인 부분을 했으니 이제 코드를 예시로 들어보자.
서버 컴포넌트 >
(기본적인 심플한 코드)

export default function Page() {
  return <h1>Hello Next.js!</h1>
}

async > 서버에서 데이터 불러오고 HTML 생성

export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

(async을 이용한 비동기 처리 함수 / fetch를 이용해서 서버에 있는 데이터를 받아오고, json의 형태로 출력하게 됨)

동기(synchronous)란, 어떤 작업을 실행할 때 그 작업이 끝나기를 기다리는 방식을 의미한다. 
즉, 작업이 완료될 때까지 다음 코드의 실행을 멈추고 기다리는 것이다. 
이러한 방식은 작업의 순서를 보장하고, 작업이 끝날 때까지 결과를 기다리는 것이 가능하다. 

비동기(asynchronous)란, 어떤 작업을 실행할 때 그 작업이 완료되지 않더라도 다음 코드를 실행하는 
방식을 의미한다. 
즉, 작업이 완료되지 않았더라도 결과를 기다리지 않고 다음 코드를 실행하는 것이다. 
이러한 방식은 작업이 오래 걸리는 경우 시간을 절약하고, 병렬적인 작업 처리가 가능하다.

클라이언트 컴포넌트 >

'use client';

import { useState } from 'react';

export default function ClientComp() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

클라이언트 컴포넌트의 핵심은 맨 최상단에 'use client';를 선언하는 것이다. 이를 선언해야 next.js가 클라이언트 컴포넌트임을 이해할 수 있다.


Next.js의 fetch() 사용 위치와 옵션에 따라 어떤 렌더링 전략이 적용되는지 설명할 수 있다. 
(and)
SSR, SSG, ISR, CSR의 개념과 차이를 실제 코드 예제를 통해 체감할 수 있다.

https://developer.mozilla.org/ko/docs/Web/API/Fetch_API

Fetch API - Web API | MDN

Fetch API는 네트워크 통신을 포함한 리소스 취득을 위한 인터페이스를 제공하며, XMLHttpRequest보다 강력하고 유연한 대체제입니다.

developer.mozilla.org

^ (fetch API) > fetch()는 쉽게 설명하자면 "외부에 있는 API"를 요청하는 것. (API는 일종의 메뉴판...)
(위 2개를 합쳐서 이야기를 해보고자 한다.)
브라우저에서 cache(캐시) 옵션은 fetch의 요청이 브라우저의 HTTP 캐시와 어떻게 상호작용할지를 결정하게 된다.
이런 확장 기능을 통해 캐싱을 다루게 된다.
> 공식 문서 링크 (https://nextjs.org/docs/app/api-reference/functions/fetch#optionscache)

options.cache
fetch(`https://...`, { cache: 'force-cache' | 'no-store' })

- auto no cache(기본적인 값)
> next.js에서는 개발 중인 모든 요청은 원격 서버에서 리소스를 가져오지만, next build경로가 정적으로 사전 렌더링되므로 개발 중에는 한 번만 가져온다. 경로에서 다이다믹 API가 감지되면 next.js에서는 모든 요청의 리소스를 가져오게 된다. (이게 무슨 말일까....)
- no-store
> next.js의 기준, 경로에서 다이다믹 API가 따로 감지되지 않더라도 모든 요청에서 원격 서버의 리소스를 가져온다.
- force-cache
> 이건 데이터 캐시에서 일치하는 요청을 찾는 옵션이다.
(일치하는 항목이 존재하고 그게 최신이면 캐시에서 반환된다. 일치하는 항목이 없거나 오래된 항목이면 원격 서버에서 리소스를 가져와서 다운로드한 리소스로 캐시를 업데이트한다.)

options.next.revalidate
fetch(`https://...`, { next: { revalidate: false | 0 | number } })

위 옵션은 리소스의 캐시 수명(보통 초 단위)을 설정하는 옵션이다.

  • false- 리소스를 기한 없이 캐시 한다. 의미적으로는 revalidate: Infinity 와 동일하고 볼 수 있다. HTTP 캐시는 시간이 지남에 따라 오래된 리소스를 제거할 수 있다.
  • 0- 리소스가 캐시 되는 것을 방지하는 옵션.
  • number- (초) 리소스의 캐시 수명이 최대 n초이어야 함을 지정한다. 예를 들어.. revalidate: 5 이라면 5초가 캐시 수명이 된다.

(revalidate은 재검증을 뜻함)


위에서 fetch에 대한 옵션(및 위치)에 따라서 렌더링 전략이 좀 달라진다. 이 부분을 표로 보면 다음과 같을 것.

 

Server Component 기본 (force-cache) SSG 빌드 시 정적 HTML 생성
Server Component cache: 'no-store' SSR 매 요청마다 fetch & HTML 생성
Server Component next: { revalidate: N } ISR N초마다 HTML 재생성
Client Component 😊 CSR HTML만 내려주고, 클라이언트에서 fetch
SSR, SSG, ISR, CSR의 개념과 차이

일단 SSR과 CSR은 저번에도 이야기했던 개념이다.

SSR은 서버 사이드 렌더링을 뜻하며, CSR은 클라이언트 사이드 렌더링을 뜻한다.

말 그대로 서버에서 렌더링을 할 것이냐? 클라이언트에서 렌더링을 할 것이냐의 차이이다. (코드는 뒤에서 이야기함)

그리고 기본적으로 next.js는 서버 사이드 렌더링으로 렌더링이 된다. (엄밀히 하자면 SSG가 기본 생성 값이라고 합니다.)

이때, Next js에서 SSG나 ISR과 구분되는 이유는 렌더링 되는 시점에 있다.

SSR은 사용자가 요청할 때마다 그 시점에 페이지를 새롭게 렌더링 한다. 그렇기 때문에 Fetching 해야 하는 데이터가 빈번하게 변경될 때 사용된다.


SSG

SSG는 Static Site Generation의 약자로 Next js에서 페이지를 생성할 때 기본으로 적용되는 설정이다. SSR과 다른 점은 클라이언트가 요청하는 시점이 아니라 빌드 시에 페이지를 미리 생성해 놓는 것이다. 모든 사용자가 같은 HTML을 받고, 데이터가 자주 안 바뀌는 페이지에 적합하다. 

ISR

ISR은 Incremental Static Regeneration의 약자로 빌드 시점에 페이지를 렌더링 한 후, 설정한 시간마다 페이지를 새로 렌더링 한다. SSG의 확장판(?)이라고 볼 수 있다. 즉, 초기에는 SSG처럼 동작하다가 설정된 시간 후에 서버가 백그라운드에서 HTML을 갱신하게 된다.

코드 예제


위에서 SSG / ISR / CSR / SSR의 개념을 이야기했으니 렌더링 전략에 따라서 코드 예제를 정리해 보자.


SSG (Static Site Generation)

export default async function Page() {
  const res = await fetch('https://api.com/posts'); // default: force-cache
  const data = await res.json();
  return <div>{data.title}</div>;
}

위에서 말한 옵션에 의거하면, 따로 옵션을 주지 않는 코드라고 여길 수 있다. (왜냐하면 force-cache는 기본 값이니깐!)
특징을 말하자면 빌드 시에 데이터는 고정이 될 것이다. 이러면 속도는 겁나(?) 빨라지는 특성이 있다. 물론 실시간성은 없다...
SSR (Server Side Rendering)

export default async function Page() {
  const res = await fetch('https://api.com/posts', { cache: 'no-store' });
  const data = await res.json();
  return <div>{data.title}</div>;
}

옵션 값을 no-store를 주어서 매 요청마다 fresh 한 데이터를 가져오는 코드이다.
fresh 하다는 것은 "지금 이 순간 최신 상태 데이터"라고 생각하면 된다. 속도는 SSG보다 느리다.
ISR (Incremental Static Regeneration)

export default async function Page() {
  const res = await fetch('https://api.com/posts', {
    next: { revalidate: 60 },
  });
  const data = await res.json();
  return <div>{data.title}</div>;
}

이 코드는 "일정한주기" 로 갱신이 되는 코드가 된다. (revalidate 뒤에 60이 오니깐 60초마다 갱신이 될 것이다.)
SSR은 지금 최신 상태를 항상 요청마다 가져오지만, ISR은 특정 주기를 주어서 가져오게 되는 것이다.
CSR (Client Side Rendering)

'use client';
import { useEffect, useState } from 'react';

export default function Page() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/posts').then((res) => res.json()).then(setData);
  }, []);

  return <div>{data?.title}</div>;
}

csr를 하고 싶다면 최상단에 'use client'; 를 선언해 주면 된다. (어차피 hook을 쓰면 최상단에 선언해야 한다)
클라이언트 서버 렌더링에서는 우리가 흔히 리액트에서 하듯이 fetch를 다루게 된다.
useEffect(컴포넌트를 마운트 할 때에 데이터를 불러온다.) 안에 묶어서 fetch를 처리하게 된다. 
클라이언트에서 렌더링이 되기에 초기에 로딩 속도는 느릴 것이다. (모든 일은 클라이언트에서 처리함)


뭐가 더 절대적으로 좋은 것은 없고, 상황에 따라서 적절하게 전략을 선택하면 좋을 것 같다.

어떤 상황에서 어떤 렌더링 전략이 적절할까?

절대적으로 좋은 것은 없다.
상황에 따라서 좋은 렌더링 전략이 존재할 뿐이다.
그렇다면 어떤 상황에 어떤 렌더링 전략을 사용해야 적절할까?
상황을 예시로 들어보자.


정적인 블로그 글이 필요한 상황

"정적"인 블로그 글은 매번 fresh 한 데이터를 가져올 필요가 없어 보인다.
그렇다면 굳이 SSR를 할 필요는 없으니, SSG가 더 적절해 보인다.

실시간으로 바뀌는 주식이나 뉴스라면?

이 상황은 "가장 최신 데이터"가 정말 중요하다.
이러면 가장 최신의 데이터를 가져오는 SSR이 더 적절해 보인다. 물론 이러면 속도가 늦어질 수 있겠지만, 이 부분은 코드의 전반적인 성능 향상을 통해 극복해봐야 하겠다.

일반적인 게시판

지금 당장의 최신 데이터가 생명은 아니지만, 사용자의 데이터를 가져오는 것이 메인이라면, ISR를 이용해 보는 것이 좋아 보인다.
(revalidate: 60) 으로 10~60초 정도의 갭을 주는 것도 좋아 보인다.

댓글 좋아요

댓글 좋아요 구독 버튼 등등 여러 클라이언트와의 상호작용이 필요한 상황이라면 CSR이 제격이다.
클라이언트에서 처리하고 그 정보를 서버/데이터로 넘겨보자.

결론

하나의 서비스에는 여러 렌더링 전략이 존재할 것이다.
네이버만 봐도 클라이언트 사이드 렌더링도 있을 것이고, 서버 사이드 렌더링이 존재한다.
상황에 맞는 가장 최선의 렌더링을 고수한다면 서비스의 품질은 오를 것이라고 생각한다.