본문 바로가기

Next.js

[NextJs] 서버 vs 클라이언트 컴포넌트: 최적의 선택을 위한 가이드

Next.js에서 App Router를 자주 사용하다 보면, 때때로 라이브러리 지원 문제나 프로젝트 마감 기한 때문에 서버 사이드 렌더링(Server-Side Rendering, SSR)을 고려하지 않고 'use client'를 무작정 사용하는 경우가 있습니다. 이러한 경험은 많은 개발자들이 공감할 수 있는 부분일 것입니다.

 

이번 글에서는 제가 서버 컴포넌트와 클라이언트 컴포넌트를 선택할 때 어떤 기준을 두고 사용하는지, 그리고 왜 서버 사이드를 우선적으로 고려해야 하는지에 대해 정리해 보려고 합니다. 각 컴포넌트의 특성과 장단점을 명확히 이해함으로써, 보다 효과적으로 Next.js의 기능을 활용할 수 있는 방법을 함께 살펴보겠습니다.

 

서버 사이드란 무엇일까요?

서버 사이드는 웹 애플리케이션의 비즈니스 로직과 데이터 처리를 서버에서 수행하는 방식을 의미합니다. 일반적으로 데이터베이스와의 연동, 사용자 인증 및 권한 권리 등이 있습니다.

 

장점으론 

1. 초기 로드 시간이 짧아 사용자에게 빠르게 콘텐츠를 제공할 수 있습니다.

2. SEO에 유리하여 검색 엔진이 콘텐츠를 쉽게 크롤링할 수 있습니다.

3. 서버에서 데이터를 미리 처리하므로 클라이언트의 리소스 소모를 줄일 수 있습니다.

 

저는 개발하면서 특히 3번의 장점이 크게 와닿았습니다. 프로젝트에서 카드 형태의 테이블에 이미지를 띄웠던 경험이 있습니다. 클라이언트에서 이미지를 처리할 경우 페이지에 들어왔을 때 스켈레톤 UI를 표시하거나 로딩 아이콘을 보여주지 않으면 가끔 이미지 로딩이 느려서 흰 카드가 보이는 경우가 있었습니다. 그러나 서버 사이드로 수정한 후에는 카드 형태 테이블에 이미지가 바로 표시되어 놀란 기억이 있습니다. 이러한 경험은 서버 사이드 렌더링의 유용성을 실감하게 해주었습니다.

 

클라이언트 사이드란 무엇일까요?

클라이언트 사이드는 사용자의 브라우저에서 실행되는 코드로, 사용자 인터페이스와 상호작용하는 데 집중합니다. 주로 JavaScript를 사용하여 웹 페이지의 동적 콘텐츠를 생성하고, 사용자와의 상호작용을 처리합니다.

 

장점으론

1. 사용자 경험(UX)을 향상시킬 수 있는 빠른 응답성과 동적인 인터페이스를 제공합니다.

 

2. 서버와의 상호작용 없이도 클라이언트에서 직접 데이터를 처리할 수 있어 네트워크 트래픽을 줄일 수 있습니다.

3. 다양한 라이브러리와 프레임워크(React, Vue.js)를 통해 복잡한 UI를 쉽게 구현할 수 있습니다.

 

저는 React Query를 사용하여 데이터 페칭을 하곤 했는데, 그때마다 클라이언트 사이드에서 데이터를 처리해야 하는 단점이 있었습니다. 또한, useState를 사용하면 클라이언트 사이드를 사용하는 것이었습니다. 이러한 방식으로 분리를 시키게 되면 props의 깊이가 깊어지는 문제가 발생하기도 했습니다. 이를 해결하기 위해 context API나 상태 관리 라이브러리를 활용하여 props를 간소화하고, 전역 상태를 관리하는 방식을 택했습니다.

 

프로젝트 적용 사례

import { useAxios } from "@/hooks/useAxios";
import { useRouter } from "next/navigation";

export const useGetCalculationHistoryList = (search: SearchProps) => {
  const router = useRouter();
  const axios = useAxios();
  const queryFunction = async (pageParam: number) => {
    const response = await axios.get(`/api/test/list`, {
      params: {
        pageNum: pageParam + 1,
        workName: search.workName,
        startDate: search.dateRange.from,
        endDate: search.dateRange.to,
        paymentStatusTypeList: search.paymentStatusTypeList,
      },
      paramsSerializer: {
        indexes: null,
      },
    });
    return response;
  };

 

  • Axios는 클라이언트 측에서 HTTP 요청을 처리하는 라이브러리입니다. 하지만, 이 코드에서 useAxios가 어떻게 구현되어 있는지에 따라 서버 컴포넌트에서도 사용할 수 있습니다. 서버에서 Axios를 사용해 API 요청을 처리하고, 그 결과를 클라이언트에 전달할 수 있습니다.

 

import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query";

const fetchResult = useInfiniteQuery({
    queryKey: ["GET", search],
    queryFn: ({ pageParam }) => queryFunction(pageParam),
    initialPageParam: 0,
    getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
      const totalCount = lastPage.data?.data?.itemCnt ?? 0;

      if (Math.ceil(totalCount / 10) <= lastPageParam + 1) {
        return null;
      }

      return lastPageParam + 1;
    },
    refetchOnWindowFocus: false,
    placeholderData: keepPreviousData,
  });
  if (fetchResult.isError) return null;
  if (fetchResult.isSuccess) {
    //성공했을때의 동작
  }
  return null;
};
  • React Query는 클라이언트 측 상태 관리 라이브러리로, 비동기 데이터 요청을 쉽게 처리할 수 있게 도와줍니다. 하지만, React Query는 클라이언트 컴포넌트에서 사용되는 것이 일반적입니다. 이 코드에서 React Query의 훅을 사용하는 것은 클라이언트 측에서 비동기 데이터 로딩을 관리하기 위한 것이지만, 서버 컴포넌트에서 사용되는 API 호출 및 로직을 담고 있기 때문에 use client 없이도 가능할 수 있습니다.

 

use client가 필요 없는 상황

  • 상태 관리와 데이터 페칭을 서버에서 처리하는 경우: 서버에서 데이터를 미리 처리하고 클라이언트에 전달하는 방식은 클라이언트의 초기 렌더링 성능을 높이고, UI를 깔끔하게 유지할 수 있습니다. 위의 경우, API 요청 및 데이터 처리가 서버에서 이루어지므로 클라이언트에서의 use client가 필요하지 않습니다.
  • 비즈니스 로직이 서버에서 실행될 수 있는 경우: 서버에서 데이터를 가져오고 그 결과를 기반으로 클라이언트에서 사용할 수 있는 형태로 가공하는 것이기 때문에, 클라이언트 컴포넌트로 전환할 필요가 없습니다.

 

느낀 점

앞으로는 서버 사이드를 보다 적극적으로 활용하여 초기 렌더링 성능을 개선하고, 클라이언트 사이드에서는 useEffect와 useState 등을 사용할 때 필요한 데이터만 가져오는 방식으로 효율성을 높일 계획입니다.