본문으로 건너뛰기

Next.js 프로젝트 회고 및 서버 컴포넌트와 클라이언트 컴포넌트

· 약 3분
고현림
프론트엔드 | 블록체인 Developer

개요

최근 실시간 주식 데이터 프로젝트를 진행할 때 Next.js 15.3.2를 사용해서 개발을 진행한 적이 있습니다. 이 때 돌이켜보면 NEXT.js가 유행하니깐 사용을 했었는데 왜 사용했고 이점을 보지 못한 부분이 많이 아쉬웠습니다. 이를 회고하고 어떻게 하면 수정할 수 있을지에 대해서 고민을 해보았습니다.

개념 공부와 수정 사항 제안

런타임

런타임이 무엇인지 알아보았고, NEXT.js에서 런타임은 아래와 같다는 것을 알 수 있었습니다:

  • 런타임은 기본적으로 사용자가 웹페이지를 열거나, 코드가 실제로 실행되었을 때를 의미합니다. 혹은 서버에서 SSR이 일어날 때도 해당이됩니다.
  • 서버 런타임 환경
    • NEXT.js 서버에서 실행되는 것읠 의미합니다.
    • 서버 런타임의 실행 위치는 Node.js 혹은 Edge가 있습니다.
  • 클라이언트 런타임 환경
    • 사용자의 브라우저에서 실행되는 것을 의미합니다.
"use client";

import { useEffect } from "react";

const ClientComponent = () => {
useEffect(() => {
console.log("브라우저에서 실행이 됩니다.");
}, []);

return <>client component</>;
};
const Page = async () => {
const response = await fetch("api url", {
cache: "no-store",
});

const data = await response.json();

return <div>{data.length}개의 데이터</div>;
};

해당 코드에서의 클라이언트 컴포넌트는 클라이언트 런타임 환경이고 즉 브라우저에서 실행이 되는 코드입니다. 하지만 Page 컴포넌트를 보게 되면 해당 코드는 서버에서 먼저 실행이 되고 이후 HTML 결과를 브라우저에 전달을 해줍니다.

이처럼 서버 컴포넌트와 클라이언트 컴포넌트는 실행되는 위치가 다르고 이에 따라 적절하고 사용해주는 것이 중요합니다.

이전 내가 코드를 작성했던 로직 및 변경 사항 고민

실시간 주식 데이터 그래프를 시각화 하는 과정에서 모든 로직을 use client로 설정하여 구현하였습니다. 하지만 이렇게 하게 되면

  1. NEXT.js의 강점인 초기 렌더링 속도
  2. SEO 및 포퍼먼스

이 2개의 강점을 가져오지 못한다는 점을 알게 되었습니다. 하지만 use client를 사용하지 않으면 서버 컴포넌트는 reChart 라이브러리와 호환이 되지 않는다는 것을 알게 되었습니다. 그럼에도 해당 분기를 통해 처리를 하게 된다면 javascript 파일의 크기를 줄일 수 있어, 충분히 가치가 있을 것이라도 판단을 하였습니다.

따라서 전체적인 흐름을 아래와 같이 구성해보았습니다.

[1] 사용자가 페이지에 접근

[2] 기존에 클라이언트 컴포넌트에서 시간을 처리하던 것을 서버 컴포넌트로 가져와 데이터를 처리

- 열려 있음: 직전 데이터 1개만 fetch
- 닫혀 있음: 지난 데이터 목록 fetch

[3] 장이 열려있을 때와 닫혀있을 때 그래프를 분기하여 처리합니다.

[4] 클라이언트 컴포넌트에서 실시간 데이터 요청 시작 (useSWR, 3초마다)

수정 방향

Page 코드 수정

  1. 이전 Page 소스코드
import { notFound } from "next/navigation";
import { StockGraph } from "@/components/stock/StockGraph";

const StockDetailPage = async ({
params,
}: {
params: Promise<{ id: string }>,
}) => {
const { id } = await params;
if (!id) return notFound();

// ...

return (
<div className="p-8">
<StockGraph symbol={id} />
</div>
);
};

export default StockDetailPage;
  1. 변경된 Page 소스 코드
  • GraphWrapper 컴포넌트를 만들어서 관리해주도록 하였습니다.
import { notFound } from "next/navigation";
import { StockGraph } from "@/components/stock/StockGraph";

const StockDetailPage = async ({
params,
}: {
params: Promise<{ id: string }>,
}) => {
const { id } = await params;
if (!id) return notFound();

// ...

return (
<div className="p-8">
<GraphWrapper symbol={id} />
</div>
);
};

export default StockDetailPage;

GraphWrapper 서버 컴포넌트 추가 제작

  • GraphWrapper 서버 컴포넌트를 만들고 해당 서버 컴포넌트에서 장 열림여부와 가져와야하는 데이터 및 그래프를 분기하도록 하였습니다.
  • 이렇게 하게 되면 StaticStockGraph에서는 불필요한 로직 등이 제거되어 클라이언트 컴포넌트로 실행이 되기 떄문에 자바스크립트 파일 크기를 줄일 수 있다는 점이 있습니다.
const GraphWrapper = ({symbol} : {symbol: string}) => {
const marketStatus = isMarketOpen();
let data;

if (marketStatus) {
data = await getLastKnownStock(symbol);
return <LiveStockGraph symbol={id} initialData={data} />
} else {
data = await getPreviousStockGraph(symbol);
return <StaticStockGraph data={data} />
}
};

회고를 진행하면서 느낀 점

단순하게 생각해보면 Wrapper 컴포넌트를 만들고 분기한 거 말고 화려한 비즈니스 로직이 들어가있지는 않았습니다.

하지만 NEXT.js에서 서버 컴포넌트와 클라이언트 컴포넌트가 무엇인지 이해하고 제가 작성한 코드를 처음으로 분석해보았던거 같습니다. 특히 NEXT.js의 런타임 관점에서 코드를 바라보고 왜 NEXT.js에서 서버 컴포넌트를 사용하는지 그리고 클라이언트 컴포넌트에 너무 많이 의존하는 코드가 작성되는 것 역시 지양되는 것이 좋다고 생각을 하였습니다.

이번을 계기로 기준을 명확히 하여 작업을 진행하는 작업이 정말 중요하다는 것을 배운거 같습니다.

참고 자료

NEXT.js 런타임 종류