ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Next.js에서 알아보는 서버 컴포넌트, 클라이언트 컴포넌트, 정적/동적 라우트 & 캐싱
    Javascript 2024. 10. 18. 19:06

    Next.js에서 알아보는 서버 컴포넌트, 클라이언트 컴포넌트, 정적/동적 라우트 & 캐싱

    Next.js의 서버 컴포넌트(Server Component)와 클라이언트 컴포넌트(Client Component), 그리고 정적(Static) 라우트와 동적(Dynamic) 라우트, 캐싱에 대해 혼란스러우신가요?

     

    React 개발자로서 Next.js와 React Server Components(RSC)에 입문하게 되면, 기존에 익숙했던 클라이언트 사이드 React와 서버 사이드 개념이 섞이면서 어떻게 접근해야 할지 막막할 수 있습니다.

     

    이 글에서는 이러한 새로운 패러다임을 이해하기 위한 정신적 모델(Mental Model)을 구축해 나갈 텐데요, 중요한 내용들을 하나씩 간단하게 설명드리겠습니다.

     


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

     

    먼저, 서버 컴포넌트클라이언트 컴포넌트의 기본적인 차이점을 정리해볼까요?

     

    서버 컴포넌트는:

    • 서버에서 렌더링됩니다.
    • 서버 자원(데이터베이스, 파일 시스템 등)에 접근할 수 있습니다.
    • 정적인 HTML을 생성합니다.
    • 상태나 상호작용을 포함할 수 없습니다.
    • 데이터 페칭이나 콘텐츠 중심의 부분에 적합합니다.

    클라이언트 컴포넌트는:

    • 서버와 클라이언트 양쪽에서 렌더링됩니다.
    • 상호작용과 상태 관리를 처리합니다.
    • 'use client' 지시어를 사용합니다.
    • 상호작용이 필요한 UI 요소에 적합합니다.

     

    서버 컴포넌트는 애플리케이션의 기초를 담당하고, 클라이언트 컴포넌트는 그 위에 상호작용을 추가한다고 생각하면 됩니다.

     


    데이터 흐름: 서버 컴포넌트에서 클라이언트 컴포넌트로

    React 서버 컴포넌트 모델에서 데이터는 서버 컴포넌트 → 클라이언트 컴포넌트단방향으로 흐릅니다.

     

    이 데이터 흐름을 이해하는 것이 애플리케이션 구조를 잡는 데 매우 중요합니다.

     

    다음은 이 개념을 시각적으로 표현한 예시입니다:

     

     

    서버 트리(Server Tree)에서 데이터를 준비한 후, 이를 props로 클라이언트 트리(Client Tree)에 전달합니다.

     

    클라이언트 트리는 받은 데이터를 바탕으로 HTML과 JavaScript 번들을 생성합니다.

     

    이 과정을 더 잘 이해하기 위해 간단한 예제를 살펴볼까요?

    // ServerComponent.tsx
    async function ServerComponent() {
      const data = await fetch('https://api.example.com/data');
      const json = await data.json();
    
      return <ClientComponent serverData={json} />;
    }
    
    // ClientComponent.tsx
    'use client'
    
    import { useState } from 'react';
    
    function ClientComponent({ serverData }) {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <h1>{serverData.title}</h1>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
      );
    }

     

    위 코드에서 ServerComponent는 서버에서 데이터를 가져와 ClientComponent에 전달하고, ClientComponent는 전달된 데이터를 사용해 상호작용을 처리합니다.

     

    이런 방식으로 서버 컴포넌트는 데이터 페칭을 처리하고, 클라이언트 컴포넌트는 렌더링과 상호작용을 담당합니다.

     


    Hydration 에러란?

    서버 컴포넌트와 Next.js를 사용하다 보면 Hydration 에러를 만날 수 있는데요, 이 에러는 개발 중에는 잘 드러나지 않다가, 프로덕션 환경에서 문제가 발생하는 경우가 많습니다.

     

    Hydration 에러는 서버에서 미리 렌더링된 콘텐츠와 클라이언트에서 렌더링된 콘텐츠가 일치하지 않을 때 발생합니다.

    이런 불일치는 다음과 같은 상황에서 발생할 수 있습니다:

     

    • 서버 컴포넌트에서 브라우저 전용 API를 사용할 때
    • 서버와 클라이언트가 서로 다른 내용을 생성할 때
    • 랜덤 값이나 시간 의존적인 로직을 제대로 처리하지 못할 때

    이 문제를 방지하기 위해서는 클라이언트 전용 코드는 useEffect 훅으로 처리하고, 서버와 클라이언트가 동일한 콘텐츠를 생성하도록 주의해야 합니다.

     

    다음은 서버와 클라이언트의 차이를 안전하게 처리하는 예시입니다:

    'use client'
    
    import { useState, useEffect } from 'react'
    
    export default function Component() {
      const [isClient, setIsClient] = useState(false)
    
      useEffect(() => {
        setIsClient(true)
      }, [])
    
      return (
        <div>
          {isClient ? (
            <p>This content is rendered on the client</p>
          ) : (
            <p>This content is rendered on the server</p>
          )}
        </div>
      )
    }

     

    이 컴포넌트는 처음에는 서버에서 렌더링된 콘텐츠를 표시하고, Hydration이 완료되면 클라이언트 전용 콘텐츠로 전환됩니다.

     

    이렇게 하면 서버와 클라이언트의 데이터 불일치 문제를 피할 수 있습니다.

     


    정적 라우트와 동적 라우트의 차이

    Next.js 애플리케이션을 개발하다 보면, 두 가지 라우트 유형인 정적 라우트(Static Routes)동적 라우트(Dynamic Routes)를 만나게 됩니다.

     

    이 차이를 이해하는 것이 애플리케이션의 성능 최적화에 매우 중요합니다.

     

    정적 라우트는 빌드 시 미리 렌더링된 페이지로, 콘텐츠가 자주 변경되지 않는 경우에 적합합니다.

    예를 들어:

    • /about
    • /contact

    동적 라우트는 요청 시마다 생성되거나 동적 매개변수에 따라 빌드된 페이지로, 빈번하게 변경되는 콘텐츠에 적합합니다.

    예를 들어:

    • /blog/[slug]
    • /products/[id]

    Next.js 14부터는 Route Segment Config를 도입해 각 페이지, 레이아웃, 혹은 라우트 핸들러의 동작을 세부적으로 제어할 수 있습니다.

     

    다음은 그 예시입니다:

    // layout.tsx | page.tsx | route.ts
    
    export const dynamic = 'auto'
    export const dynamicParams = true
    export const revalidate = false
    export const fetchCache = 'auto'
    export const runtime = 'nodejs'
    export const preferredRegion = 'auto'
    export const maxDuration = 5
    
    export default function MyComponent() {
      // Your component code here
    }

     

    이 설정을 통해 각 라우트의 캐싱, 렌더링 방식, 실행 환경 등을 세밀하게 관리할 수 있습니다.


    Next.js 캐싱 전략

    Next.js에서 제공하는 캐싱 기능은 성능을 개선하는 데 큰 도움을 줍니다.

     

    캐싱은 여러 종류가 있는데요, 그중 몇 가지를 소개하겠습니다:

    • Request Memoization: 같은 요청에 대해 결과를 캐싱하여 여러 번 호출해도 한 번만 실행됩니다.
    • Data Cache: fetch()로 서버 컴포넌트에서 데이터를 가져올 때 자동으로 결과를 캐싱합니다.
    • Full Route Cache: 전체 경로에 대한 렌더링 결과를 캐싱합니다.
    • Router Cache: 클라이언트 사이드에서 정적 및 동적 라우트의 렌더링 결과를 캐싱합니다.

    캐싱을 효과적으로 사용하려면, revalidate 옵션을 사용해 데이터를 얼마나 자주 재검증(Revalidate)할지 설정할 수 있습니다.

    // in fetch
    fetch('https://...', { next: { revalidate: 3600 } })
    
    // in layout.tsx | page.tsx | route.ts
    export const revalidate = 3600

     

    캐시를 사용하고 싶지 않을 때는 cache: 'no-store' 옵션을 사용해 캐싱을 비활성화할 수 있습니다.

    // in fetch
    fetch('https://...', { cache: 'no-store' })

     

    또한, 캐싱 및 라우트 설정을 디버깅할 때는 전체 URL 로그를 활성화하는 것이 유용합니다.

    // next.config.js
    module.exports = {
      logging: {
        fetches: {
          fullUrl: true,
        },
      },
    }

    결론

    React 서버 컴포넌트와 Next.js는 React 애플리케이션을 빌드하는 방식에 큰 변화를 가져옵니다.

     

    서버와 클라이언트 컴포넌트를 명확히 분리하고, 캐싱을 효율적으로 활용하며, 정적/동적 라우팅을 적절하게 사용하는 것이 Next.js 애플리케이션의 성능을 극대화하는 핵심입니다.

     

    이러한 기술들을 잘 이해하고 활용하면, SEO 성능을 크게 개선하고 사용자 경험을 향상시킬 수 있습니다.

     

    처음에는 서버 컴포넌트와 클라이언트 컴포넌트의 역할 분담이 복잡하게 느껴질 수 있지만, 적절한 캐싱 전략과 라우팅 설정을 사용하면 애플리케이션의 유연성과 성능을 극대화할 수 있습니다.


    실전에서의 활용

    이제 Next.js와 React Server Components를 실무에서 어떻게 적용할 수 있는지 간단한 예시를 통해 정리해볼게요.

    1. 서버 컴포넌트에서 데이터를 가져오고, 클라이언트 컴포넌트로 전달하여 상호작용을 처리합니다.
    // ServerComponent.tsx
    async function ServerComponent() {
      const data = await fetch('https://api.example.com/data');
      const json = await data.json();
    
      return <ClientComponent serverData={json} />;
    }
    
    // ClientComponent.tsx
    'use client'
    
    import { useState } from 'react';
    
    function ClientComponent({ serverData }) {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <h1>{serverData.title}</h1>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
      );
    }
    1. Hydration 에러를 방지하기 위해 서버와 클라이언트 간의 차이점을 적절히 처리합니다.
    'use client'
    
    import { useState, useEffect } from 'react'
    
    export default function Component() {
      const [isClient, setIsClient] = useState(false)
    
      useEffect(() => {
        setIsClient(true)
      }, [])
    
      return (
        <div>
          {isClient ? (
            <p>This content is rendered on the client</p>
          ) : (
            <p>This content is rendered on the server</p>
          )}
        </div>
      )
    }
    1. 정적 라우트동적 라우트를 적절히 사용하여 애플리케이션의 성능을 최적화합니다.
    // layout.tsx | page.tsx | route.ts
    
    export const dynamic = 'auto'
    export const revalidate = 60
    export const fetchCache = 'force-no-store'
    
    export default function MyComponent() {
      // Your component code here
    }
    1. 캐싱 전략을 활용해 성능을 높이고, 불필요한 서버 요청을 줄입니다.
    // in fetch
    fetch('https://...', { next: { revalidate: 3600 } })
    
    // 캐시 방지
    fetch('https://...', { cache: 'no-store' })

    요약

    React Server Components와 Next.js는 사용자 경험을 최적화하고, 성능을 극대화할 수 있는 강력한 도구입니다.

     

    이 기술들을 잘 이해하고 적절히 활용하면, 서버 렌더링, 정적 페이지 생성, 동적 콘텐츠 제공을 균형 있게 처리할 수 있습니다.

    • 서버 컴포넌트는 데이터 페칭과 콘텐츠 중심의 역할을 담당하며, 클라이언트 컴포넌트는 상호작용과 상태 관리를 처리합니다.
    • Hydration 에러를 방지하려면 서버와 클라이언트 간의 차이점을 적절히 처리해야 합니다.
    • 정적 라우트동적 라우트의 차이를 이해하고, Route Segment Config를 활용해 페이지의 동작을 세밀하게 제어할 수 있습니다.
    • 캐싱을 적절히 활용하면 서버 부하를 줄이고, 애플리케이션의 응답 속도를 크게 개선할 수 있습니다.

    이제 실전에서 React Server Components와 Next.js를 활용해 다양한 프로젝트를 진행해 보세요.

     

    처음에는 다소 복잡해 보일 수 있지만, 차근차근 익히다 보면 이 기술들이 얼마나 강력한지 깨닫게 될 것입니다.

     

    계속해서 실험하고, 새로운 패턴을 시도하면서 한층 더 발전된 애플리케이션을 만들어보세요.


Designed by Tistory.