ReactJS에서처럼 NextJS에서도 컴포넌트는 웹사이트를 만드는 데 사용되는 기본적인 재사용 가능한 UI(사용자 인터페이스) 조각인데요(본질적으로 같음). JSX(JavaScript XML)를 return하는, 레고 블록처럼 조립해서 복잡한 웹 페이지를 만들 수 있는 재료인데요.
NextJS는 클라이언트 컴포넌트만 제공하는 ReactJS와 다르게 클라이언트 컴포넌트와 서버 컴포넌트의 두 가지 유형의 컴포넌트를 제공하는데요. 오늘은 이 둘에 대해서 알아보도록 하겠습니다.
1. 클라이언트 컴포넌트
클라이언트 컴포넌트는 전통적인 React 컴포넌트와 가장 유사하다고 생각하면 이해하기 쉽습니다.
1.1 클라이언트 컴포넌트의 특징
- 브라우저(클라이언트)에서 실행: 이름 그대로 사용자의 웹 브라우저에서 JavaScript 코드가 실행됩니다. 페이지가 로드된 후 브라우저가 이 컴포넌트의 JavaScript 코드를 다운로드하고 실행하죠.
- 상호작용성: 사용자 상호작용(클릭, 입력 등)이 필요한 모든 기능은 클라이언트 컴포넌트에서 구현됩니다. useState, useEffect와 같은 React Hooks를 사용할 수 있어서, 버튼 클릭 시 상태 변경, 애니메이션, 폼 제출 처리 등 동적인 기능을 만들 수 있어요.
- 번들 사이즈 증가: 클라이언트 컴포넌트의 JavaScript 코드는 사용자의 브라우저로 전송되기 때문에, 너무 많은 클라이언트 컴포넌트를 사용하면 초기 로딩 속도가 느려질 수 있습니다.
- 선언 방식:Next.js App Router에서는 'use client'; 지시어를 사용하지 않으면 기본적으로 모든 컴포넌트를 서버 컴포넌트로 인식하기 때문에 파일 맨 위에 'use client'; 지시어를 명시적으로 추가해야 합니다.
1.2 클라이언트 컴포넌트 렌더링 방식
클라이언 컴포넌트는 다음과 같은 순서로 작동합니다.
1)사용자가 페이지를 요청합니다.
2)서버는 최소한의 HTML과 함께 클라이언트 컴포넌트의 JavaScript 파일 전체를 브라우저로 보냅니다.
3)브라우저는 이 JavaScript 파일을 다운로드하고, 파싱하고, 실행하여 UI를 만들고 화면에 보여줍니다. 이 과정에서 사용자 상호작용이 가능해집니다.
4)만약 JavaScript 파일의 용량이 크거나 네트워크 환경이 좋지 않으면, 사용자는 화면이 완전히 로드되고 상호작용이 가능해질 때까지 더 오래 기다려야 할 수 있습니다.
1.3 클라이언트 컴포넌트 사용 예시
- 상태 관리: 사용자 입력을 받는 폼, 토글 버튼, 드롭다운 메뉴 등
- 이벤트 리스너: 클릭 이벤트, 스크롤 이벤트, 키보드 입력 등
- 브라우저 API 접근: localStorage, geolocation 등 브라우저 환경에서만 사용 가능한 API
- 애니메이션 효과: 인터랙티브한 UI 애니메이션
// app/components/Counter.tsx
'use client'; // 클라이언트 컴포넌트임을 명시
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
클릭: {count}
</button>
);
}
이렇게 해주면 사용자의 화면에서 버튼이 렌더링되게 됩니다.
2. 서버 컴포넌트
서버 컴포넌트는 Next.js App Router의 가장 혁신적인 변화 중 하나로, 사용자에게 보여질 웹 페이지의 초기 HTML을 서버에서 미리 만들어서 보내주는 컴포넌트입니다. 이전에 NextJS 소개글에서 다뤘던 ReactJS의 단점을 보완하기 위해 도입된 개념인데요.
서버 컴포넌트든 클라이언트 컴포넌트든, Next.js와 React에서 컴포넌트의 핵심 역할은 UI를 묘사하는 것입니다. 그리고 UI를 묘사하는 표준적인 방법이 바로 JSX이기 때문에 JSX를 return해야 하는 것은 동일합니다.
서버 컴포넌트로는 ㄹㄹ
2.1 서버 컴포넌트 렌더링 방식 (1) - SSR(서버 사이드 렌더링)
SSR은 사용자가 특정 페이지를 요청할 때마다 서버에서 해당 페이지의 HTML을 실시간으로 만들어 보내는 방식인데요. 다음과 같은 순서로 작동합니다.
SSR 방식의 작동 순서
1)사용자가 페이지를 요청합니다.
2)Next.js 서버가 해당하는 경로의 서버 컴포넌트를 실행
3)서버 컴포넌트가 완성된 HTML을 만들어냄
4)완성된 HTML이 사용자의 브라우저로 바로 전송되고, 브라우저는 받은 HTML을 즉시 파싱해서 보여줌. 추가적인 javascript 다운로드 및 실행 없이 초기 화면이 바로 나타남.
SSR 방식의 주요 특징
- 항상 최신 데이터: 요청 시마다 데이터를 가져오므로 항상 최신 정보를 사용자에게 보여줄 수 있습니다.
- 개인화된 콘텐츠: 사용자 로그인 상태나 다른 동적인 조건에 따라 다른 내용을 보여줘야 하는 페이지(예: 개인 대시보드, 실시간 주식 시세)에 적합합니다.
- 초기 로딩 속도 및 SEO: 브라우저는 완전한 HTML을 받자마자 화면에 표시할 수 있어 초기 로딩이 빠르고, 검색 엔진이 페이지 내용을 쉽게 파악할 수 있어 SEO에 유리합니다.
2.2 서버 컴포넌트 렌더링 방식 (2) - SSG(정적 사이트 생성)
SSG는 웹사이트를 배포하기 위해 빌드(Build)하는 시점에 서버에서 미리 HTML 파일을 만들어 두는 방식입니다.
SSG 방식의 작동 순서
1)개발자가 Next.js 프로젝트를 배포하기 위해 next build 명령어를 실행
2)Next.js 서버가 SSG로 설정된 서버 컴포넌트를 서버 환경에서 실행. 이 때 주로 한 번만 실행되는 데이터 패칭 함수 실행됨
3)서버 컴포넌트가 완성된 HTML을 미리 생성하고 파일로 저장(pre-render)
4)미리 만들어진 HTML 파일과 javascript, css 등의 정적 자원들이 웹 서버나 CDN에 배포됨
5)사용자가 페이지 요청하면 웹 서버나 CDN는 빌드 시점에 이미 만들어져있는 HTML 파일 찾아서 즉시 브라우저로 전송
6)브라우저는 받은 HTML을 즉시 파싱하여 화면에 보여줍니다. SSR과 마찬가지로, 추가적인 JavaScript 다운로드 및 실행 없이 초기 화면이 바로 나타납니다. 브라우저는 그냥 준비된 그림(HTML)을 받아서 보여주기만 하면 되는 거죠. 그래서 일반적으로 SSR보다 좀 더 빠릅니다.
2.3 서버 컴포넌트의 특징
- 서버에서 실행: 컴포넌트의 코드가 사용자의 브라우저가 아닌, Next.js 서버에서 실행됩니다. 웹 페이지가 사용자에게 전달되기 전에 서버에서 미리 HTML 형태로 렌더링되죠.
- 초기 로딩 속도 개선: 서버에서 미리 HTML을 생성하기 때문에, 브라우저는 완성된 HTML을 받아 빠르게 화면을 그릴 수 있어요. 클라이언트 컴포넌트처럼 JavaScript를 다운로드하고 실행할 필요가 줄어들어 초기 로딩 성능이 향상됩니다.
- 서버 리소스 직접 접근: 서버에서 실행되기 때문에 데이터베이스 쿼리, 파일 시스템 접근, API 키 사용 등 서버 전용 리소스에 직접 접근할 수 있어요. 이는 보안성을 높이고 클라이언트에서 민감한 정보를 노출할 위험을 줄여줍니다.
- 번들 사이즈 감소: 컴포넌트의 JavaScript 코드가 브라우저로 전송되지 않으므로, 최종 JavaScript 번들 사이즈를 줄여 페이지 로딩 속도를 더욱 빠르게 합니다.
- 기본값: 별도의 지시어가 없으면 Next.js의 모든 컴포넌트는 기본적으로 서버 컴포넌트로 간주됩니다.
2.4 서버 컴포넌트 사용 예시
- 데이터 패칭: 데이터베이스에서 블로그 글 목록을 가져오거나, 외부 API에서 정보를 불러오는 등
- 정적인 콘텐츠 렌더링: 블로그 게시글 내용, 상품 상세 설명, 회사 소개 페이지 등 사용자 상호작용이 적고 내용이 자주 바뀌지 않는 부분
- 보안이 필요한 로직: API 키 사용, 환경 변수 접근 등 서버에서만 실행되어야 하는 코드
- 초기 SEO 최적화: 검색 엔진 크롤러가 JavaScript 실행 없이도 완전한 HTML을 받아갈 수 있게 하여 SEO에 유리합니다.
// app/blog/page.tsx (기본적으로 서버 컴포넌트)
async function getBlogPosts() {
// 서버에서 직접 데이터베이스 또는 API 호출
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function BlogPage() {
const posts = await getBlogPosts();
return (
<div>
<h1>블로그 포스트</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
이 서버컴포넌트도 클라이언트 컴포넌트와 똑같이 웹을 렌더링해주는 컴포넌트지만, 클라이언트 컴포넌트가 js 파일을 다 다운받아야 화면에 보여지는 것과 다르게 서버에서 미리 만들어놓고 바로 렌더링 해줍니다.
3. 클라이언트 컴포넌트와 서버 컴포넌트의 상호작용
Next.js는 이 두 가지 컴포넌트 유형을 함께 사용하여 최적의 성능과 유연성을 제공하는데요.
3.1 서버 컴포넌트 안에서 클라이언트 컴포넌트 사용
서버 컴포넌트는 클라이언트 컴포넌트를 자식으로 포함할 수 있는데요. 예를 들어, 서버 컴포넌트가 데이터를 가져와서 정적인 블로그 글을 렌더링하고, 그 안에 "좋아요" 버튼(클라이언트 컴포넌트)을 넣어 사용자와 상호작용하게 할 수 있죠.
// app/blog/[slug]/page.tsx (서버 컴포넌트)
import LikeButton from './LikeButton'; // 클라이언트 컴포넌트 불러오기
export default async function BlogPostPage({ params }) {
const post = await getPost(params.slug); // 서버에서 데이터 패칭
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton postId={post.id} /> {/* 클라이언트 컴포넌트 사용 */}
</div>
);
}
이렇게 서버 컴포넌트 안에 클라이언트 컴포넌트를 넣으면,
1)클라이언트 컴포넌트를 포함한 초기 렌더링을 서버에서 진행
2)완전한 HTML과 클라이언트 컴포넌트에 필요한 javascript 코드를 브라우저로 전송
3)브라우저는 js코드를 바탕으로 하이드레이션 및 상호작용 준비 시작
하는 과정으로 작동한다고 볼 수 있습니다.
3.2 클라이언트 컴포넌트 안에서 서버 컴포넌트 사용
서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 포함할 수 있듯이, 반대로 클라이언트 컴포넌트도 서버 컴포넌트를 자식으로 전달받아 렌더링할 수 있습니다. 예를 들어, 사용자 상호작용이 필요한 탭 메뉴(클라이언트 컴포넌트) 안에 각 탭의 콘텐츠는 서버에서 데이터를 가져와 렌더링되는 정적인 내용(서버 컴포넌트)으로 구성하고 싶은 경우에 쓰면 됩니다.
// app/components/ServerContent.tsx
// 'use client' 지시어가 없으므로 서버 컴포넌트입니다.
async function getServerData() {
// 실제 서버에서 데이터베이스나 외부 API 호출
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data.message;
}
export default async function ServerContent() {
const message = await getServerData();
return (
<div style={{ border: '1px solid gray', padding: '10px', marginTop: '10px' }}>
<h3>서버에서 가져온 콘텐츠:</h3>
<p>{message}</p>
<p>(이 내용은 서버에서 미리 생성됩니다)</p>
</div>
);
}
이렇게 서버 컴포넌트를 만들고,
// app/components/ClientButton.tsx
'use client'; // 클라이언트 컴포넌트임을 명시
import { useState } from 'react';
export default function ClientButton({ children }: { children: React.ReactNode }) {
const [clickCount, setClickCount] = useState(0);
return (
<div style={{ border: '1px solid blue', padding: '10px' }}>
<h2>클라이언트 컴포넌트 (동적 버튼)</h2>
<button onClick={() => setClickCount(clickCount + 1)}>
클릭! ({clickCount}번 클릭됨)
</button>
<hr />
{children} {/* 여기에 서버 컴포넌트가 렌더링됩니다. */}
</div>
);
}
이렇게 클라이언트 컴포넌트를 만들어줍시다.
// app/page.tsx
// 'use client' 지시어가 없으므로 서버 컴포넌트입니다.
import ClientButton from '@/app/components/ClientButton';
import ServerContent from '@/app/components/ServerContent';
export default function HomePage() {
return (
<main style={{ padding: '20px' }}>
<h1>Next.js 하이브리드 렌더링 예시</h1>
{/* ClientButton (클라이언트 컴포넌트)을 렌더링하면서,
그 안에 ServerContent (서버 컴포넌트)를 children으로 전달합니다.
*/}
<ClientButton>
<ServerContent />
</ClientButton>
</main>
);
}
그리고 페이지에 클라이언트의 자식으로 서버 컴포넌트를 넣게 되면,
1)사용자가 페이지를 요청하면 Nextjs서버는 HomePage 서버 컴포넌트 싫애
2)HomePage 안에서 <ClientButton>을 만나는데, 이 ClientButton의 children으로 <ServerContent /> 서버 컴포넌트가 전달
3) Next.js 서버는 이 ServerContent를 먼저 실행하여 데이터를 가져오고 HTML로 미리 렌더링합니다
4) 그 후, ClientButton의 정적인 HTML 구조 안에 ServerContent의 미리 렌더링된 HTML을 포함시켜 하나의 완성된 HTML 문자열 생성
5)이 완성된 HTML과 ClientButton에 필요한 JavaScript 코드가 브라우저로 전송됩니다.
6)이후 hydration 진행
하는 방식으로 작동합니다.
클라이언트 컴포넌트 안에서 서버 컴포넌트를 쓸 때 하나 주의할 점이 있다면, 클라이언트 컴포넌트가 서버 컴포넌트를 직접 import하는 방식은 불가능하다는건데요. 왜냐하면 클라이언트 컴포넌트에 포함된 javascript 번들은 브라우저에서 실행되는데, 서버 컴포넌트는 서버에서만 실행될 수 있기 때문이죠.
4. 요약: 언제 무엇을 사용할까?
- 서버 컴포넌트 (기본):
- 데이터 패칭 (DB 접근, API 키 사용)
- 정적인 콘텐츠 렌더링
- 초기 로딩 성능 및 SEO 최적화가 중요할 때
- 클라이언트 컴포넌트 ('use client';):
- 사용자와의 상호작용(클릭, 입력 등)이 필요할 때
- 브라우저 전용 API (localStorage, geolocation 등)를 사용해야 할 때
- useState, useEffect 등 React Hooks가 필요할 때
요약해보면 위와 같겠습니다.
이상입니다.
읽어주셔서 감사합니다.
'웹개발 > NextJS' 카테고리의 다른 글
[NextJS] NextJS에서 쓰는 라우팅용 특수 파일들 (0) | 2025.07.03 |
---|---|
[NextJS] NextJS 프레임워크에 대해 araboza (1) | 2025.07.01 |