2023. 8. 31. 00:19ㆍNext.js/생활코딩

Server Componet VS Client Component

Next.js에서는 Server Component, Client Component라는 개념이 구분되어 있다. React 18 버전부터 서버 컴포넌트라는 개념이 추가가 되면서 Next.js가 그걸 가져와서 구분되어 진 것이다.
서버 컴포넌트를 안 써봤다면 지금까지 클라이언트 컴포넌트 방식으로 구현해왔을 것이다. 그리고 클라이언트 컴포넌트에서는 useState, useEffect, onClick, onChange와 같은 것들을 사용할 수 있는데 서버 컴포넌트에서 이런 것들을 사용하게 되면 에러가 발생하게 된다.
두 컴포넌트는 사용할 수 있는 api가 다르다는 것을 기억하면 좋다.
그리고 Next.js에서는 특별한 조치를 취하지 않는다면 서버 컴포넌트로 간주한다. 그럼 언제 서버 컴포넌트를 쓰고 언제 클라이언트 컴포넌트를 써야 할까?

어떤 정보를 표현하는데 사용자와 상호작용하지는 않는 부분에 대해서는 서버 컴포넌트로 만드는 게 유리하다. 사용자와 상호작용하지 않는 정보를 보여주는 컴포넌트가 있는데 그 컴포넌트 안에 사용자와 상호작용하는 버튼이 있다면 버튼만 새로운 컴포넌트로 만들어서 그것만 클라이언트 컴포넌트로 만드는 것이 더 유리하다는 것이다.
즉, 사용자 상호작용하는 기능은 클라이언트 컴포넌트, 정보를 단순히 보여주는 역할을 하는 것은 서버 컴포넌트가 유리하다는 큰 틀을 가지고 계속 진행해보자.
서버 컴포넌트는 아래와 같은 경우에 사용합니다.
- 사용자와 상호작용하지 않는 경우
- 백엔드에 엑세스하면서 보안적으로 위험한 정보를 주고 받는 경우
클라이언트 컴포넌트는 아래와 같은 경우 사용합니다.
- 서버 컴포넌트로 해결되지 않는 경우
- 사용자와 상호작용하는 경우
- useEffect, useState, onClick, onChange와 같은 API를 사용해야 하는 경우
- useRouter, useParams와 같은 nextjs의 client component API를 사용하는 경우
클라이언트 컴포넌트를 먼저 시도해보고, 클라이언트 컴포넌트의 비효율을 살펴본 뒤, 서버 컴포넌트로 전환하는 작업을 진행하자.
클라이언트 컴포넌트로 변환.
우선 서버에서 데이터를 가져오기 위해 useEffect에서 fetch를 실행하자. 그리고 이 데이터를 사용하기 위해 state를 사용하자. 그런데 useEffect 내에서 setState를 처리하면 에러가 발생한다.
에러 내용은 useState는 오직 클라이언트 컴포넌트에서만 동작한다는 것이다. 아까 말했듯 기본 컴포넌트는 서버 컴포넌트인데 useState와 같은 것들을 사용해서 그런 것이다.
그럼 이 코드를 클라이언트 컴포넌트로 바꿔주기 위해서는 어떻게 해야할까? 컴포넌트 파일 최상단에 "use client"라고 적으라고 에러에는 나와있다. 그런데 이번엔 다른 에러가 발생한다. use client로 클라이언트 컴포넌트로 바꿨더니 기존에 있는 metadata가 문제가 된다. metadata는 서버 컴포넌트에서만 사용되는 것이기 때문이다. 그런데 이걸 클라이언트 컴포넌트에서 사용했기 때문에 다시 에러가 난다. 그럼 이 metadata를 일단 주석 처리 해보자. 그럼 잘 동작하는 걸 볼 수 있다!
이제 가져온 데이터 state인 topics를 가지고 글 목록을 만들어보자. li 태그를 동적으로 주는 경우, 잘 동작하고 에러도 없다.
그런데 이 코드에서 아쉬운 점들이 몇 가지 있다.
// /src/app/layout.tsx
'use client';
import Link from 'next/link';
import './globals.css';
// import type { Metadata } from 'next';
import { useEffect, useState } from 'react';
// export const metadata: Metadata = {
// title: 'Web tutorials',
// description: 'Generated by byein',
// };
export default function RootLayout({ children }: { children: React.ReactNode }) {
const [topics, setTopics] = useState([]);
useEffect(() => {
fetch('http://localhost:9999/topics')
.then((resp) => resp.json())
.then((result) => {
setTopics(result);
});
}, []);
return (
<html>
<body>
<h1>
<Link href='/'>WEB</Link>
</h1>
<ol>
{topics.map((topic: { id: number; title: string; body: string }) => {
return (
<li key={topic.id}>
<Link href={`/read/${topic.id}`}>{topic.title}</Link>
</li>
);
})}
</ol>
{children}
<ul>
<li>
<Link href='/create'>Create</Link>
</li>
<li>
<Link href='/update/1'>Update</Link>
</li>
<li>
<input type='button' value={'delete'} />
</li>
</ul>
</body>
</html>
);
}
클라이언트 컴포넌트의 비효율성.
우선, useEffect를 이용해서 데이터 fetching하는 것이다. 서버에서 클라이언트로 데이터를 가져오는데 만약 서버가 상파울루에 있고 우린 서울에 있다면 데이터를 가져오는 데 시간이 오래 걸릴 것이다. 그럼 상당한 비효율이 발생한다. 그리고 disable javascript를 처리하면 컨텐츠는 표시되지 않는다. 정적인 부분은 잘 보이지만 useEffect와 같은 것들은 JS로 실행되지 않기 때문이다.
그리고 만약 데이터베이스에 접속을 한다면 데이터베이스 아이디, 패스워드를 이용해서 데이터베이스 서버에 접속한다. 여기서 아이디와 패스워드가 사용자에게 노출되면 안 되는데 보안적으로도 문제가 있을 수 있다.
서버 컴포넌트로 변환.
일단 JS를 다시 키고, 서버 컴포넌트로 다시 바꾸자. 서버 컴포넌트로 바꾸는 방법은 "use client"를 지우는 것이다. 서버 컴포넌트는 한 번 렌더링 돼서 한 번 렌더링 되면 그걸 클라이언트로 단순하게 보내주는 역할만 하면 되기 때문에 fetch를 사용할 때 useState, useEffect가 필요없다.
대신 일단 async로 함수를 선언해준다. 내부적으로 await라는 Promise 문법을 사용할 것이기 때문이다. 비동기적인 코드를 동기적으로 바꾸게 되면 에러 없이 잘 실행되는 걸 볼 수 있다.
이후 동작 과정은 fetch 메소드가 호출되고 이 실행이 끝날 때까지 await로 기다린다. 이게 끝나면 json으로 바꾸라는 명령이 전달되면서 이게 끝나면 글 목록을 동적으로 생성한다. 다음에 그 결과를 .next 폴더 내에 나름대로 저장해놓고 최종적인 정적인 내용만 클라이언트로 전송한다.
이 과정에서 JS 코드는 빼고 전송을 한다. 그럼 클라이언트로 JS를 전송하지 않아 용량이 작다. 그리고 우리가 접근하고 있는 서버가 같은 서버에 있는 주소라고 한다면 즉, layout.js의 js와 fetch에서 사용하는 주소의 api 서버가 같은 서버거나 가깝게 있다면 굉장히 빠르게 이 동작이 끝난다. 그리고 서버 쪽에서 렌더링을 끝내고 데이터를 보내기 때문에 JS를 끄고 새로고침해도 코드는 잘 동작한다. 서버쪽에서 동적으로 생성한 정적인 내용을 클라이언트로 전달했으니까 그렇다.
// /src/app/layout.tsx
import Link from 'next/link';
import './globals.css';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Web tutorials',
description: 'Generated by byein',
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const resp = await fetch('http://localhost:9999/topics');
const topics = await resp.json();
return (
<html>
<body>
<h1>
<Link href='/'>WEB</Link>
</h1>
<ol>
{topics.map((topic: { id: number; title: string; body: string }) => {
return (
<li key={topic.id}>
<Link href={`/read/${topic.id}`}>{topic.title}</Link>
</li>
);
})}
</ol>
{children}
<ul>
<li>
<Link href='/create'>Create</Link>
</li>
<li>
<Link href='/update/1'>Update</Link>
</li>
<li>
<input type='button' value={'delete'} />
</li>
</ul>
</body>
</html>
);
}
서버 컴포넌트의 좋은 점
- 간결한 코드: useEffect와 useState와 같은 훅을 사용하지 않아도 되므로, 코드가 더 간결하고 이해하기 쉬워집니다. 이로 인해 코드 유지 관리가 쉬워지고, 버그 발생 확률이 줄어들 수 있습니다.
- 빠른 데이터 엑세스: 데이터베이스와 같은 자원에 접근해야 하는 경우, 서버 컴포넌트는 서버와 데이터베이스가 가까운 위치에서 작동하므로, 더 빠른 속도로 필요한 데이터에 접근할 수 있습니다.
- 보안: 서버 컴포넌트는 클라이언트에 민감한 정보(예: 데이터베이스 비밀번호)를 전송하지 않습니다. 이로 인해, 필요한 작업을 안전하게 처리하면서 동시에 클라이언트의 보안을 유지할 수 있습니다.
- 향상된 성능: 서버 컴포넌트는 클라이언트로 JavaScript 코드를 전송하지 않습니다. 이는 전송되는 데이터의 양을 줄이고, 클라이언트의 부하를 줄임으로써 웹사이트의 전반적인 성능을 향상시키는데 도움이 됩니다.
참고자료
생활코딩 - Next.js 13
https://opentutorials.org/course/5098
Next.js 13 - 생활코딩
수업 개요 Next.js는 웹 애플리케이션을 빌드하고 배포하는 데 강력한 도구입니다. 이 도구를 활용하면 모던한 웹 앱을 빠르고 효율적으로 구축할 수 있습니다. 그럼 함께 미래의 웹 개발 패러다
opentutorials.org
'Next.js > 생활코딩' 카테고리의 다른 글
Next.js 13 - 12. 글생성 (0) | 2023.08.31 |
---|---|
Next.js 13 - 11. 글 읽기 (0) | 2023.08.31 |
Next.js 13 - 9. backend (0) | 2023.08.30 |
Next.js 13 - 8. css (0) | 2023.08.30 |
Next.js 13 - 7. 정적 자원 사용하기 (0) | 2023.08.30 |