배경
React Server Component/Action 변경점은 2024년 12월 5일에 출시한 React 19 중 가장 인상 깊은 부분이다. 어떤 방식으로 내부적인 갱신과 render 처리를 하는지 정리해보기 위하여 작성하였다.
주요 개념
Server Component/Action 개념은 next.js 경험이 있다면 직관적으로 이해할 것이다.
파일 최상단 "use server" 선언을 통해서 server component 구현을 할 수 있다. 이는 client 측에서 발생한 상호작용이 촉발하여 비롯한 연산을 서버에서 처리한다. 처리가 완료 되면 Payload 전송을 통해 client component("use client" 선언으로 시작하는 component) 일부분을 갱신(reconciliation)한다. Server Component/Action 기능을 통해서 Back-End 협업 중 가장 빈번한 API 구현 부담을 덜 수 있다. 직접 가져오면 되니까. 기존 서버가 JavaScript 기반 서버라면 주요 기능을 Server Component/Action 변환하여 같이 사용할 수도 있겠다.
⚠️ Hydration 개념과 관련이 없다.
Hydration(client 측에서 javascript 파일을 로드하여 event 등록 등을 통해 bootstrap 하는 과정) 발생은 오직 client component 안에서만 일어난다. 서버에서 보낸 Payload 정보를 바탕으로 회복(Reconciliation)한다.
영향
Form-Action
이번 버전에서 마찬가지로 새롭게 명명한 hook 중 하나인 useActionState 사용을 통해 Suspense 상태를 관리할 수 있다. 이는 Server Action 사용 시 아주 자연스럽게 영향을 받는다.
기존에 요긴하게 사용한 react-hook-form 라이브러리와 fetch 혹은 react-query 없이도 자연스럽게 연결해주며 loading state 선언을 별도로 할 필요 없이 간단하게 만들어준다. Suspense Fallback 구현도 아주 쉽게 할 수 있다.
아래 코드는 이의 예시이다
"use client"
import someServerAction from "@/actions"
function EnhencedComponent({usePromise}) {
const [actionState, action, isPending] = useActionState(async (data, formData) => { someServerAction }, {error: {message: ""}, success: ""});
const data = use(userPromise);
return <Suspense fallback={<div>Processing</div>}>
{data.someInfo}
<form action={action}>
<input ... />
<Button diabled={isPending} type="submit">
{isPending? "Processing...":"Submit"}
</Button>
{{actionState.error?.message}}
</form>
</Suspense>
}
이 코드에는 3가지 유리한 점이 있다.
- 상위 Component 통해서 넘겨받은 promise 상태를 그대로 사용(use)해서 fetch로 신경 쓰지 않고 render
- isLoading 없이 상태를 관리할 수 있다.
- 값 검증을 Server Action 하나로 끝낼 수 있다. 이는 client 에서 zod 같은 걸 사용하지 않아도 되고, 다시 말해서 불러와야할 라이브러리 크기가 줄어든다는 것이다.
Component 설계
아주 오래전, React 초기에는 component 설계를 할 때 Smart Container - Dumb Component 구조로 나누었다. 데이터를 가지고 있는 component, view 생성을 위한 JSX component 두 가지로 나누었다. 여러 가지 이유가 있지만, life cycle 마다 prop 혹은 event 발생 시 render 여부를 결정해줘야하는 class 기반 component 밖에 없었기 때문이다. 이후 Hook 및 Functional Component 등장으로 인해 이 구조는 사라졌다. hook 사용으로 데이터를 따로 가지고 있도록 지원했기 때문이다.
Server-Client 기준으로 component 구분을 나누면 어느 쪽에서 처리해야하는지가 명확하다. 또한 Server Action 처리를 통해 확실하게 강력한 컴퓨팅 자원이 필요할 때 얼마든지 서버를 사용할 수 있다는 점에서 그리고 Front-End 뿐만 아니라 데이터에 접근할 수 있다는 점이 매력적이다.
상태 관리
상태 관리를 위한 Store 사용을 하려면 어떻게 Server Component <-> Client Component 간 dispatch 및 동기화가 이루어지는지 궁금할 것이다.
예를 들어 Redux 환경이라면, 다음과 같이 초기화 해줘야한다.
'use client';
import { useRef } from 'react';
import { store } from './store';
import { setPosts } from './postsSlice';
export default function StoreInitializer({ data }) {
const initialized = useRef(false);
if (!initialized.current) {
store.dispatch(setPosts(data));
initialized.current = true;
}
return null;
}
import { db } from './db';
import StoreInitializer from './StoreInitializer'; // 클라이언트 컴포넌트
export default async function Page() {
const data = await db.posts.findMany(); // 서버 데이터 페칭
return (
<main>
<StoreInitializer data={data} />
<PostList />
</main>
);
}
PostList 내 로직에서는 redux 기존 방법대로 사용하면 된다.
마치며
자세히 알아보기 전에는 nuxt.js 혹은 next.js 등장으로 네트워크 경계가 모호해지는 것에서 부정적이었다. View-Data-Logic 분리를 위한 기존 Architecture 설계를 위한 노력에 반대하는 방향으로 생각했기 때문이다. 하지만 React 19 및 next.js 위에서는 render 책임이 어디에 있는지가 중요한 것이지, 여전히 보여주기 위한 view, 그리고 데이터와 그 처리를 위한 논리는 분리하고 있다.
서버-클라이언트를 통합한 Architecture 시도는 기존에도 있었다. Meteor.js 혹은 backbone.js, Angular2 같은 과거의 시도들과 비교해서 Full-Stack 도구를 노리는 react, 그리고 그 시도인 서버 컴포넌트, 액션은 이전의 한계를 능숙한 DX 제공을 통해 풀어내고 있다.
C# 웹 도구로 주어진 SOAP (Simple Object Access Protocol) 통신과 비슷하다는 생각이 들었다. 그 구현과 사용법은 상당히 다르지만 dispatch, Contract 같은 개념들이 떠올랐다. gRPC 같은 구현체가 web 기술에 자연스럽게 녹아드는 부분과 같은 감탄이 든다. 기술 발달로 인한 변주가 이렇게 다른 느낌을 줄지는 상상도 못했다.
'Front-End' 카테고리의 다른 글
| [React Native] Version Upgrade 후기 (0) | 2024.03.27 |
|---|---|
| 이직 회사 적응 안내서 -2- Front-End 분석 (0) | 2024.03.26 |
| [AngularJS] Bootstrap 부터 개념 대강 살펴보기 (0) | 2024.02.19 |