NextJS 13버전으로 업데이트가 되면서 굉장한 변화들이 있었는데, 그 중 가장 큰 변화라고 생각이 드는 것은 앱 디렉토리(app directory)의 등장이라고 생각이 듭니다. 앱 디렉토리의 이전과 이후를 모두 경험해본 입장에서는 이후가 개발자 경험이 향상되었다고 체감이 듭니다.
NextJS 서버 클라이언트와 클라이언트 컴포넌트의 이해
nextjs에는 클라이언트 컴포넌트와 서버 컴포넌트가 있습니다.
NextJS 13버전의 app 폴더안에 있는 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다.
개인적인 생각으로는 이 두개의 컴포넌트를 이해하고 차이를 인식하고 개발하는 것이 정말 중요하다고 생각합니다.
들어가기에 앞서 간단하게 결론부터 말하자면
클라이언트 컴포넌트는 사용자 상호작용을 처리하는데 중점을 두고 있고, 서버 컴포넌트는 데이터 처리와 보안적인 요소를 담당합니다. 각 컴포넌트의 역할이 완전히 다르므로 각 컴포넌트의 특성을 이해하는 것이 중요합니다. 잘못된 설정과 적용은 오히려 성능 하락과 NextJS 프레임워크를 사용하는 의미를 잃을 수 있습니다.
Rendering: Server Components | Next.js
Learn how you can use React Server Components to render parts of your application on the server.
nextjs.org
서버 컴포넌트(Server Component)
export default function ServerComponent() {
const data = fetchData()
return (
<ClientComponent data={data} />
)
}
처음 13버전 이후의 nextjs를 접했을때 서버컴포넌트랑 클라이언트 컴포넌트가 "무슨 차이지?", "그냥 훅을 쓰면 use client 쓰지 뭐" 라고 생각하고 개발만 하기 바빳던 것 같다. 지금 보면 그렇게 코딩을 한 결과물과 이 특성의 차이를 알고 코딩한 결과물은 성능과 짜임새가 확연히 차이가 났습니다.
서버 컴포넌트는 클라이언트로 전송되기 전 HTML로 렌더링되어 실행되는 리액트 컴포넌트입니다. NextJS에서 기본적으로 컴포넌트를 만들면 서버컴포넌트로 동작됩니다. 즉, 서버 컴포넌트에서는 useState, useEffect와 같은 훅 함수를 사용하지 못하고, onClick, onChange와 같은 이벤트함수 그리고 CSS in JS 스타일링도 제한됩니다. 이를 사용하려면 "use clinet"를 최상단에 작성해 클라이언트 컴포넌트라고 명시해야 합니다.
목적
1) 클라이언트로 보내는 자바스크립트 양을 줄여서 초기 페이지 로드를 더빠르게 할 수 있게합니다. 이는 서버에서 HTML을 생성하기 때문에 사용자는 빈 화면 대신 내용을 확인할 수 있습니다. 즉 적은 양의 클라이언트 사이드 번들을 전달할 수 있도록 해 사용자가 적은 Javascript 코드를 다운로드 받게 해 성능을 향상시킵니다.
2) 읽기 쉬운 HTML구조로 검색엔진 최적화에 유리합니다.
3) 보안강화, 서버 컴포넌트는 민감한 데이터와 로직을 클라이언트로부터 차단해, 데이터 유출의 위험을 줄입니다.
장점
1) 서버 컴포넌트는 데이터베이스와 파일시스템과 같은 서버쪽의 리소스에 직접 접근할 수 있어서, 효율적인 데이터 패칭과 렌더링을 가능하게 합니다. 이는 하이드레이션 단계를 제거하여 애플리케이션 로딩과 상호작용 속도를 높입니다.
2) 서버 실행을 통해 민감한 데이터(ex. 사용자)와 로직을 클라이언트로부터 차단하여 보안을 강화합니다.
서버 컴포넌트로 인한 성능 향상
NextJS에서의 컴포넌트 특성을 활용하여 성능 향상 전략을 알아봅시다. 기본적으로 두가지의 큰 이점이 있습니다. 서버에서 데이터 패칭을 수행하면서 성능 개선의 이점을 얻고, 적절한 로딩 컴포넌트를 추가하여 페이지 로딩 속도와 사용자 경험을 극대화 할 수 있습니다.
서버 데이터 Fetch
서버에서 데이터 패칭을 수행함으로써, 클라이언트에 전송되는 데이터 양을 줄이고 이는 로딩 속도와 초기 페이지 로딩 시간을 단축하는데 큰 기여를 합니다. 이는 사용자 경험 향상에 중요한 요소입니다.
로딩 컴포넌트 적용
NextJS에 내장된 Suspense 기능을 통해 혹은 로직을 통해서 로딩중인 비동기 작업을 시각적으로 표현할 수 있습니다. 이는 사용자 경험(UX)를 크게 향상시킵니다.
활용 전략
이전에도 언급했듯이 서버 컴포넌트와 클라이언트 컴포넌트의 타입 특성을 이해하고 적절히 사용하는 것이 중요합니다.
컴포넌트 선택 기준
서버 컴포넌트
- 데이터적으로 보안이 중요하거나, 서버 자원을 사용해야 하는 경우
- 서버 데이터에 접근해야 하는 경우
- 일관된 데이터 처리가 필요한 경우
클라이언트 컴포넌트
- 사용자와 Interaction 관련된 기능
- 동적인 사용자 인터페이스, 애니메이션, 상태 관리
- 사용자 상호작용 브라우저 전용 API
데이터 처리와 백엔드 처리에는 서버컴포넌트 이용
nextjs 목적에 맞게 웹 어플리케이션에서 데이터 처리와 백엔드 리소스 접근에 대해서는 서버컴포넌트를 통해서 이루어지는 것이 바람직합니다. 이는 중요한 로직이 클라이언트에 노출되는 것을 방지하고 보안을 강화할 수 있는 이점이 있습니다.
그래서 다음과 같이 본인과 같은 경우에는 서버 환경에서 실행되어지게 따로 함수를 만들어서 관리합니다.
// 서버에서 실행
export async function DataFetchFunc() {
const response = fetch(`URL`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
credentials: 'include',
body: JSON.stringify("), // 단일 객체로 전달
})
return await response.json()
}
직렬화된 데이터 공유
서버에서 처리한 데이터를 클라이언트 컴포넌트로 전달할때 JSON 형태로 직렬화하여 전달하는 것이 일반적입니다. 이유는 데이터를 가볍고, 읽기 쉽고, 데이터를 추가적으로 쉽게 파싱할 수 있고 오류를 방지할 수 있습니다. 그리고 무엇보다 네트워크를 통한 전송이 용이하기 때문입니다.
fetch, cache 함수 활용
동일한 데이터에 대한 반복적인 요청은 당연하게 서버의 부하를 증가시키고, 애플리케이션의 성능을 저하시킵니다. 이를 방지하기 위해 fetch, cache 함수 기능을 이용하면 한번 요청된 데이터를 따로 저장해두었다가, 동일한 요청이 있을 시 캐시된 데이터를 재사용합니다. 이는 반대로 서버의 불필요한 중복 요청을 줄이고, 성능을 개선시킵니다.
const fetchData = async () => {
const cacheKey = 'user-data'; // 캐시 키를 정의합니다.
const cachedData = sessionStorage.getItem(cacheKey); // 세션 스토리지에서 캐시된 데이터를 가져옵니다.
if (cachedData) {
return JSON.parse(cachedData); // 캐시된 데이터가 있으면 이를 파싱하여 반환합니다.
}
try {
const response = await fetch('/api/user'); // API 엔드포인트에 네트워크 요청을 보냅니다.
if (!response.ok) {
throw new Error('네트워크 응답이 정상적이지 않습니다'); // 응답 상태를 확인하고 문제가 있으면 오류를 발생시킵니다.
}
const data = await response.json(); // 응답을 JSON 형식으로 파싱합니다.
sessionStorage.setItem(cacheKey, JSON.stringify(data)); // 받은 데이터를 세션 스토리지에 캐시로 저장합니다.
return data; // 데이터를 반환합니다.
} catch (error) {
console.error('Fetch 오류:', error); // 오류 발생 시 콘솔에 오류 메시지를 출력합니다.
throw error; // 오류를 상위 호출자에게 전달합니다.
}
};
예를 들면 이런식의 아이디어로 작성할 수 있으며, 이는 캐싱을 통해 동일한 데이터를 반복해서 요청하는 것을 방지하여 애플리케이션의 성능을 향상시킬 수 있습니다.
react context와 상태 관리
context란 리액트 컴포넌트 간 어떠한 값을 공유할 수 있게 해주는 기능입니다. 주로 전역적으로 필요한 값을 다룰 때 사용합니다. 컴포넌트간 props를 이용해서 인자를 전달하는 것이 아닌 다른 방식으로 값을 전달하는 방법이라고 보면 됩니다.
하지만 props로만 데이터를 전달하면 생기는 문제가 있습니다.
여러 컴포넌트를 거쳐서 연달아 데이터를 전달해야하는 경우 깊숙히 위치한 컴포넌트의 경우 불편하고 복잡합니다.(props drilling)
서버와 클라이언트 컴포넌트간 상태를 효과적으로 공유하기 위해서 context provider를 클라이언트 컴포넌트내에 설정하는 것이 좋습니다. 이는 서버 컴포넌트의 제한을 우회하고 클라리언트 상태를 쉽게 관리할 수 있습니다.
'use client';
import React, { createContext, useContext, useState } from 'react';
// ThemeContext를 생성하여 기본값을 'light'로 설정합니다.
const ThemeContext = createContext('light');
// ThemeProvider 컴포넌트는 테마 상태를 관리하고, 이를 하위 컴포넌트에 제공하는 역할을 합니다.
export function ThemeProvider({ children }) {
// theme 상태와 setTheme 함수를 useState를 사용하여 정의합니다.
const [theme, setTheme] = useState('dark');
// ThemeContext.Provider를 통해 하위 컴포넌트에 theme와 setTheme을 전달합니다.
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// UseThemeComponent 컴포넌트는 ThemeContext의 값을 사용하여 현재 테마를 표시하고, 테마를 변경할 수 있습니다.
function UseThemeComponent() {
// useContext를 사용하여 ThemeContext에서 theme과 setTheme을 가져옵니다.
const { theme, setTheme } = useContext(ThemeContext);
// 현재 테마를 화면에 표시하고, 버튼을 클릭하면 테마를 'light'로 변경합니다.
return (
<div>
<p>현재 테마: {theme}</p>
<button onClick={() => setTheme('light')}>테마 변경</button>
</div>
);
}
결론
Next.js는 현대 웹 개발에서 서버사이드렌더링(SSR)과 클라이언트사이드 렌더링(CSR) 사이의 균형을 맞추는 데 중요한 역할을 하고 있습니다. 두 렌더링 방식은 각각의 장점과 단점을 가지고 있으며, Next.js는 이 두가지 방식을 조합해서 더 나은 사용자 경험을 제공할 수 있는 좋은 플랫폼인 것 같습니다.
또 본인과 같은 개발자 학생입장에서는 이러한 개념에 대해서 잘 알고 자신의 프로젝트에 적용하면서 발전하는것이 엄청난 경쟁력이라고 생각합니다.
참고했던 정말 좋은 블로그 글:
Next.js에서 서버와 클라이언트 컴포넌트의 유연한 렌더링 조합 및 최적화 전략
Next.js 환경에서 서버와 클라이언트 컴포넌트를 효율적으로 조합하고 최적화하는 방법을 살펴보며 웹 애플리케이션의 성능과 사용자 경험을 극대화하는 전략을 소개합니다.
reactnext-central.xyz
'프론트엔드 기록 > 리액트' 카테고리의 다른 글
빈 화면 없이 자연스럽게 데이터 갱신하여 SSR UX 개선하기 (0) | 2025.03.03 |
---|---|
리액트 Flux 패턴이란?( vs MVC 모델 ) (2) | 2024.10.29 |
[Next.js 14] FSD 기능 관점 폴더 아키텍처에 대한 생각 (2) | 2024.10.04 |
리액트의 동작 원리, Virtual DOM (1) | 2024.09.12 |
NextJS 경로 가로채기 라우팅 (0) | 2024.09.02 |