[Nextjs] 서버 컴포넌트 사용 시 주의사항

김채운·약 19시간 전
0

Next.js

목록 보기
26/26

React Server Component는 Next.js의 App Router와 함께 도입된 핵심적인 기능으로, 서버에서만 실행되는 React 컴포넌트이다. 서버 컴포넌트를 올바르게 활용하기 위해 알아두어야 할 주요 주의사항 네 가지를 정리해 보았다. 이러한 주의사항을 지키지 않으면 의도하지 않은 동작이 발생하거나 오류를 만날 수 있다.


1. 서버 컴포넌트에서는 브라우저에서 실행되는 코드가 포함되면 안 된다.

서버 컴포넌트는 서버 측에서만 실행되므로 브라우저에서 실행되는 코드(useState, useEffect와 같은 React Hooks나 onClick, onChange와 같은 이벤트 핸들러)를 포함할 수 없고, 브라우저에서는 아예 실행조차 되지 않는다.
또한 브라우저에서 실행되는 라이브러리도 서버 컴포넌트에서는 사용할 수 없다.

// ❌ 서버 컴포넌트에서 브라우저 전용 코드 사용 (오류 발생)

export default function ServerComponent() {
  const [count, setCount] = useState(0); // useState 사용 불가

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

위와 같은 코드를 작성하면 Next.js는 오류를 발생시키며, React Hooks나 이벤트 핸들러를 제거하거나 클라이언트 컴포넌트로 전환하도록 요구한다.

✨ 해결 방법

이럴 경우, 해당 컴포넌트를 클라이언트 컴포넌트로 전환해야 한다. 파일 최상단에 "use client"를 추가하면 된다.

// ✅ 클라이언트 컴포넌트로 전환

"use client";

import { useState } from "react";

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

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

❓ 브라우저에서 실행되는 라이브러리?

브라우저 환경에서만 사용할 수 있는 Web API, DOM 조작, 클라이언트 측 상태 관리 등을 활용하는 라이브러리를 의미한다. 서버 컴포넌트는 브라우저 환경이 아닌 서버에서 실행되므로, 브라우저에서만 동작하는 이러한 라이브러리를 사용할 수 없다.

1. DOM 조작 라이브러리

  • 대표적인 예: jQuery, D3.js(DOM 관련 작업 시), 일부 애니메이션 라이브러리

  • 이유: 서버에서는 DOM(Document Object Model)이 존재하지 않기 때문이다.

import $ from "jquery"; // 서버 컴포넌트에서 사용 시 오류 발생

$("body").css("background", "blue"); // 브라우저 환경에서만 실행 가능

2. 클라이언트 측 상태 관리 라이브러리

  • 대표적인 예: zustand, Redux Toolkit

  • 이유: 이들은 브라우저에서 실행되는 상태 저장소(ex: localStorage)를 활용하거나 클라이언트 측 상호작용을 기반으로 작동한다.

3. Window 및 Document API를 사용하는 라이브러리

  • 대표적인 예: chart.js(렌더링할 때 DOM 요소 필요), moment-timezone(브라우저 시간대 기반 처리)

  • 이유: window, document와 같은 브라우저 전역 객체에 의존한다.

console.log(window.innerWidth); // 서버에서는 'window'가 정의되지 않음

4. 브라우저 네이티브 API 활용 라이브러리

  • 대표적인 예: Geolocation API, WebRTC, Canvas, AudioContext

  • 이유: 브라우저 전용 API이기 때문이다.

5. 스타일 관련 라이브러리

  • 대표적인 예: emotion, styled-components(서버 측에서 설정을 하지 않을 경우)

  • 이유: 일부 CSS-in-JS 라이브러리는 브라우저에서만 동작하거나 서버 환경에 추가 설정이 필요하다.


2. 클라이언트 컴포넌트는 서버와 클라이언트에서 모두 실행된다.

서버 컴포넌트는 서버에서 한 번만 실행되지만, 클라이언트 컴포넌트는 서버와 클라이언트 모두에서 실행된다.
클라이언트 컴포넌트는 서버에서 HTML을 생성하기 위해 먼저 실행되고, 브라우저에서는 상호작용을 위한 Hydration을 위해 다시 실행된다.

"use client";

export default function ClientComponent() {
  console.log("Rendered!");
  return <div>클라이언트 컴포넌트</div>;
}

위 코드에서 "Rendered!"라는 메시지는 서버 콘솔과 브라우저 콘솔에 각각 출력된다.
따라서 클라이언트 컴포넌트는 서버와 브라우저에서 모두 실행되므로, 브라우저에서만 실행해야 하는 코드(ex: document 객체 조작 등)는 반드시 조건을 걸어야 한다.

❗ 주의점

클라이언트 컴포넌트의 사용을 최소화하고, 서버 컴포넌트를 기본으로 사용하는 것이 JS 번들 크기를 줄이고 Hydration 시간을 단축하는 데 유리하다.


3. 클라이언트 컴포넌트에서 서버 컴포넌트를 import하지 않는다.

클라이언트 컴포넌트에서 서버 컴포넌트를 import하면 의도치 않은 동작이 발생하거나 오류가 발생할 수 있다.
이는 클라이언트 컴포넌트가 서버와 브라우저에서 모두 실행되는 반면, 서버 컴포넌트는 서버에서만 실행되기 때문이다.
그렇기 때문에 서버 측에서 실행될 때에는 이 두 컴포넌트가 모두 존재하지만, 반대로 브라우저에서 하이드레이션을 위해서 한 번 더 실행이 될 때에는 클라이언트 컴포넌트는 존재하겠지만 서버 컴포넌트는 존재하지 않게 된다.

그렇지만 개발의 규모가 커지다 보면 컴포넌트의 개수가 많아지고 그러다 보면 클라이언트 컴포넌트에서 서버 컴포넌트를 import하게 되는 상황이 생길 수도 있다. Next는 이럴 때 오류를 발생시키는 대신 서버 컴포넌트를 클라이언트 컴포넌트로 바꿔주게 된다. 그리고 그럼으로써 개발 도중에 잦은 오류를 만나는 걸 방지해 준다.

그렇기 때문에 되도록이면 클라이언트 컴포넌트의 자식으로 서버 컴포넌트를 배치하는 건 웬만하면 피하는 것이 좋다. 왜냐면, 이 클라이언트 컴포넌트는 JS번들에 포함이 된다. 그래서 이 클라이언트 컴포넌트의 개수가 많아지면 많아질수록 브라우저에게 전달되는 JS번들의 용량도 커지기 때문에 하이드레이션까지 걸리는 시간이 오래 걸리게 된다.

// ❌ 클라이언트 컴포넌트에서 서버 컴포넌트를 import (오류 발생 가능)

"use client";

import ServerComponent from "./ServerComponent";

export default function ClientComponent() {
  return <ServerComponent />;
}

위와 같은 코드를 작성하면 Next.js는 서버 컴포넌트를 자동으로 클라이언트 컴포넌트로 변환한다.
이로 인해 JS 번들이 커지고, 불필요한 Hydration 비용이 발생할 수 있다.

✨ 해결 방법 (children으로 전달)

하지만 정말 어쩔 수 없이 클라이언트 컴포넌트가 서버 컴포넌트를 반드시 자식으로 둬야하는 경우가 된다면
서버 컴포넌트를 클라이언트 컴포넌트에 바로 import해서 쓰지 말고,
서버 컴포넌트를 클라이언트 컴포넌트의 children으로 전달하면 이런 문제를 방지할 수 있다.

// ✅ 서버 컴포넌트를 children으로 전달

export default function ClientComponent({ children }) {
  return <div>{children}</div>;
}

이 구조를 사용하면 Next는 children으로 전달된 서버 컴포넌트는 클라이언트 컴포넌트로 변경하지 않는다. 클라이언트 컴포넌트는 서버 컴포넌트를 직접 실행할 필요 없이 서버 컴포넌트의 결과물만 children props로 전달 받아 렌더링하므로, 클라이언트 측에서는 이 서버 컴포넌트를 실행조차 할 필요 없이, props를 받아서 렌더링 하면 되기 때문에 서버 컴포넌트를 클라이언트 컴포넌트로 변환할 필요가 없다.


4. 서버 컴포넌트에서 직렬화되지 않는 Props는 전달할 수 없다.

React의 서버 컴포넌트는 실행 결과를 RSC Payload라는 형태로 JSON 직렬화한다.
따라서 React 서버 컴포넌트(RSC)는 직렬화 가능한 값만 클라이언트 컴포넌트로 전달할 수 있다. 만약 직렬화할 수 없는 값을 전달하려고 하면, 의도치 않은 동작이나 오류가 발생한다. 그래서 JSON으로 직렬화할 수 없는 값(ex: 함수, Symbol, DOM 요소 등)은 서버 컴포넌트에서 클라이언트 컴포넌트로 전달할 수 없다.


✔️ RSC Payload란?

RSC Payload는 React Server Component의 줄임말이고, Palyload는 순수한 데이터 또는 순수한 결과물 이라는 뜻이다. 그래서 쉽게 말하자면 RSC Payload란 React Server Component를 실행한 결과를 JSON 형태로 직렬화한 데이터이다. 서버 컴포넌트와 관련된 모든 데이터들이 다 들어가 있고, 이 데이터는 브라우저가 HTML 페이지를 최종적으로 렌더링할 수 있도록 필요한 정보를 담고 있다.

RSC Payload의 역할

  • 서버 컴포넌트의 렌더링 결과 저장

  • 클라이언트 컴포넌트의 위치와 연결 정보 저장

  • 클라이언트 컴포넌트에 전달할 Props데이터 저장

RSC Payload의 예시

{
  "type": "div",
  "props": {
    "children": [
      {
        "type": "h1",
        "props": {
          "children": "Hello from Server Component!"
        }
      },
      {
        "type": "p",
        "props": {
          "children": "This is rendered on the server."
        }
      }
    ]
  }
}

위 JSON은 서버 컴포넌트에서 직렬화된 데이터로, 브라우저가 이 정보를 활용해 페이지를 최종적으로 렌더링한다.


✔️ 직렬화란 무엇인가?

직렬화(serialization)란, JavaScript 객체나 배열 같은 복잡한 구조의 데이터를 문자열이나 Byte 형태의 단순한 데이터로 변환하는 과정이다.
이는 데이터를 데이터베이스에 저장하거나 네트워크로 전송할 때 주로 사용된다.

직렬화의 예시

const person = {
  name: "hong",
  age: 27,
};

// 직렬화된 데이터

const serialized = JSON.stringify(person);
console.log(serialized); // {"name":"hong","age":27}

위 코드는 person 객체JSON 문자열로 변환한 예제이다. 변환된 데이터는 저장이나 전송에 적합한 형태로 변환된다.

✔️ 직렬화할 수 없는 값이란?

JavaScript의 모든 값이 직렬화 가능한 것은 아니다. 함수(Function), Symbol, DOM 요소 등은 JSON으로 직렬화할 수 없다.

1. 함수

함수는 실행 가능한 코드 블록으로, 이를 JSON으로 표현할 방법이 없다. 함수는 스코프나 클로저와 같은 실행 환경에 의존하기 때문에 단순히 문자열이나 Byte로 표현할 수 없다.

const func = () => console.log("Cannot serialize functions");

const serialized = JSON.stringify(func); // 오류 발생

✔️ 함수는 실행 가능한 코드 블록이다.

실행 가능한 코드 블록?

  • 함수는 실행 가능한 명령어와 로직으로 이루어진 코드 덩어리이다.

  • 즉, 어떤 작업을 수행하기 위한 지시사항의 모음으로, 코드가 실행될 때 동작할 구체적인 프로세스를 가지고 있다.

function add(a, b) {
  return a + b; // a와 b를 더한 결과를 반환
}
  1. 두 개의 인자를 받는다: a, b

  2. 두 값을 더한 결과를 반환한다.

이 "더하기 작업"이라는 로직이 실행 가능한 코드 블록에 해당한다. 함수는 이렇게 실행 가능한 명령어를 담고 있기 때문에 단순히 데이터를 직렬화하여 저장하거나 전송할 수 없다.

✔️ 직렬화할 수 없는 이유

1. 실행 가능한 코드(명령어)는 직렬화가 불가능하다.

  • JSON은 데이터를 문자열로 저장하지만, 함수의 실행 로직(명령어)은 문자열로 표현할 수 없다.

  • 예를 들어 function add(a, b) { return a + b; }를 JSON으로 저장하려고 하면, 이 함수가 무엇을 해야 하는지 명령어를 해석할 수 없다.

2. 스코프 정보가 포함되지 않는다.

  • 함수는 어디에서 선언되었는지와 어떤 변수들에 접근해야 하는지 정보를 포함해야 한다.

  • 그러나 JSON으로는 이 정보를 함께 저장할 방법이 없다.

3. 클로저는 메모리 상태에 의존한다.

  • 클로저는 함수가 실행될 때의 메모리와 변수를 캡처한다.

  • 이 메모리 상태는 동적으로 변하며, 이를 JSON으로 저장하거나 네트워크로 전송하는 것은 불가능하다.


2. Symbol

Symbol은 고유하고 변경 불가능한 원시 값으로, JSON 변환 시 무시된다. 이는 Symbol이 고유성을 보장하기 위한 값으로 주로 객체의 고유한 키를 만들기 위해 사용되고, 다른 어떤 값과도 중복되지 않는 유일한 값을 생성한다는 특징이 있어서 일반적인 데이터 표현 방식에 적합하지 않기 때문이다.

const data = { id: Symbol("id") };
const serialized = JSON.stringify(data); // "{}"

✔️ Symbol의 기본 사용법

const uniqueSymbol = Symbol('description'); // Symbol 생성
console.log(uniqueSymbol); // Symbol(description)
  1. Symbol()을 호출하면 유일한 값을 생성한다.

  2. 동일한 설명(description)을 사용해도 다른 Symbol 값을 생성한다.

const sym1 = Symbol('id');
const sym2 = Symbol('id');

console.log(sym1 === sym2); // false (항상 고유함)

3. DOM 요소

DOM 요소는 브라우저의 문서 객체 모델을 기반으로 한 구조체로, 이 객체는 단순히 데이터를 포함하는 것이 아니라 브라우저의 내부 환경과 상태를 포함하며, 이를 단순한 데이터 형식(ex: JSON)으로 변환할 수 없다.

const element = document.createElement("div");

const serialized = JSON.stringify(element); // 오류 발생

✔️ DOM 요소의 구조

DOM 요소는 HTML의 각 태그를 JavaScript 객체로 표현한 것으로, 매우 복잡한 속성을 포함한다.

<div id="example">Hello World</div>
const div = document.getElementById('example');
console.dir(div);

이 코드를 실행하면 div 객체는 아래와 같이 많은 정보를 포함한다.

HTMLDivElement {
  innerHTML: "Hello World",
  outerHTML: "<div id='example'>Hello World</div>",
  id: "example",
  style: CSSStyleDeclaration {},
  dataset: DOMStringMap {},
  parentElement: null,
  ... // 기타 브라우저 상태와 메서드 포함
}
  • DOM 요소는 브라우저의 내부 상태와 연결된 복잡한 객체이다.

  • 이 내부 상태와 메서드는 브라우저 엔진에서 동작하며, 단순한 데이터로 표현할 수 없다.

🔖 브라우저 상태와 긴밀하게 연결됨

DOM 요소는 단순히 데이터를 담는 객체가 아니라 실시간 브라우저 상태를 반영한다.
예를 들어, DOM 요소의 style 속성은 CSS로 정의된 스타일과 브라우저의 계산된 값을 포함한다.

const div = document.createElement('div');
div.style.width = '50%';
console.log(div.style.width); // "50%"
  • div.style.width는 단순한 문자열이 아니라, 브라우저가 CSS 스타일을 계산하여 제공한 값이다.

  • 이러한 동적인 상태는 JSON으로 직렬화할 수 없다.

🔖 순환 참조(Circular References)

DOM 요소는 자기 자신을 포함한 순환 참조 구조를 가질 수 있다.
즉, 하나의 객체가 자신을 참조하거나 다른 객체를 참조하여 무한 순환이 발생한다.

const div = document.createElement('div');
div.child = div; // 자기 자신을 참조
console.log(div);
  • JSON은 순환 참조를 처리하지 못하기 때문에, 이러한 구조는 직렬화가 불가능하다.

  • 순환 참조를 가진 객체를 JSON으로 변환하려고 하면 오류가 발생한다.

🔖 메서드와 이벤트 핸들러 포함

DOM 요소는 데이터를 담는 속성뿐만 아니라 메서드와 이벤트 핸들러도 포함한다.

const button = document.createElement('button');
button.addEventListener('click', () => console.log('Clicked!'));

console.dir(button);
  • DOM 요소에는 addEventListener, removeEventListener 같은 메서드와 연결된 이벤트 핸들러가 포함된다.

  • 이러한 메서드와 핸들러는 실행 가능한 코드 블록이므로 JSON으로 직렬화할 수 없다.

🔖 브라우저 환경에 종속적

DOM 요소는 브라우저 환경에서만 동작하는 객체이다.

  • JSON은 언어 중립적인 데이터 표현 방식으로, 브라우저 외의 환경(ex: Node.js, 서버)에서도 사용된다.

  • 그러나 DOM 요소는 브라우저 환경에 강하게 의존하므로 JSON으로 변환해도 다른 환경에서는 해석할 수 없다.

🔖 요약

DOM 요소가 JSON으로 직렬화될 수 없는 이유는,

  1. 브라우저 상태와 메서드(ex: 스타일, 이벤트 핸들러 등)를 포함함.

  2. 순환 참조와 같은 복잡한 구조를 가짐.

  3. 함수, 메서드 등 실행 가능한 코드 블록을 포함.

  4. 브라우저에 강하게 종속된 객체.

필요한 경우에는 DOM 요소의 속성만 추출하여 데이터를 직렬화하거나, DOM 관련 작업은 브라우저에서 직접 수행하는 것이 적절하다.


✔️ 왜 서버 컴포넌트에서 직렬화되지 않는 값을 전달할 수 없을까?

Next.js에서 서버 컴포넌트는 클라이언트 컴포넌트에게 Props를 전달하기 전에 RSC Payload라는 형태로 직렬화된다.
즉, 서버 컴포넌트가 클라이언트 컴포넌트에게 전달하는 Props는 반드시 JSON으로 직렬화 가능한 값이어야 한다.

✨ 서버 컴포넌트의 동작 과정

서버 컴포넌트는 브라우저로부터 접속 요청이 들어올 때 Next.js 서버에서 사전 렌더링 과정 중에 실행되는데, 서버 컴포넌트들이 먼저 실행이 되고, 그 이후에 클라이언트 컴포넌트들이 뒤이어 실행이 된다. 이때 클라이언트 컴포넌트와 서버 컴포넌트 모두 서버에서 실행되어 HTML 페이지를 생성하는 데 기여한다.

사전 렌더링은 크게 두 단계로 나눌 수 있다.

  • 서버 컴포넌트 우선 실행: 서버 컴포넌트는 서버에서만 실행되며, 서버 컴포넌트들만 따로 실행을 시키게 되면 그 결과로 html태그가 바로 실행이 되는 게 아니라, 이 실행 결과를 JSON 유사 객체인 RSC Payload 형태로 변환한다.
    서버 컴포넌트는 여기서 직렬화 가능한 데이터만 처리하며, 생성된 RSC Payload는 이후 과정에서 중요한 역할을 한다.
    이런 중간 과정을 거치게 되고, 그 이후에는 아직 실행하지 못한 클라이언트 컴포넌트들도 마저 실행이 되어서 RSC Payload의 결과와 합쳐져서 html페이지가 생성이 완료되게 된다.

  • 클라이언트 컴포넌트 실행: 서버 컴포넌트 실행이 완료된 후, 클라이언트 컴포넌트가 서버에서 한 번 더 실행된다.
    이후, 클라이언트 컴포넌트는 브라우저로 전달되어 Hydration 단계에서 다시 실행된다.

이 과정에서 서버 컴포넌트는 직렬화 가능한 값만 처리할 수 있기 때문에, 클라이언트 컴포넌트에 전달되는 Props는 반드시 JSON으로 변환 가능한 값이어야 한다.

✔️ 해결방법

직렬화되지 않는 값은 서버 컴포넌트에서 처리될 수 없으므로, 이를 props로 전달하려는 시도는 오류를 발생시킨다.

따라서 다음과 같은 방법으로 문제를 피할 수 있다.

  1. 직렬화 가능한 값만 전달: JSON으로 변환할 수 있는 단순한 데이터(문자열, 숫자, 배열, 객체 등)만 props로 전달한다.

  2. 상호작용이 필요한 경우 클라이언트 컴포넌트를 활용: 함수, DOM 요소, 또는 Symbol과 같은 직렬화 불가능한 데이터가 필요하다면, 이를 처리하는 컴포넌트를 클라이언트 컴포넌트로 설정하고 브라우저 측에서 실행되도록 합니다.

  3. API 호출 등으로 데이터 처리: 서버 컴포넌트에서 직렬화되지 않는 값 대신, 적절한 데이터를 API 호출 등을 통해 전달하고 브라우저 측에서 필요한 로직을 처리하도록 설계한다.

이 원칙을 준수하면 Next.js의 서버 컴포넌트를 효과적으로 활용할 수 있으며, 오류를 방지하고 성능을 극대화할 수 있다.


React 서버 컴포넌트는 Next.js의 성능 최적화와 사용자 경험 향상을 위한 강력한 도구이다. 그러나 위 네 가지 주의사항을 반드시 지키지 않으면 의도치 않은 오류나 성능 저하가 발생할 수 있다. 하지만 이러한 개념을 잘 이해하고 실무에 적용하면 효율적인 Next.js 애플리케이션을 개발할 수 있다.

post-custom-banner

0개의 댓글