Server Component 실전에서 써먹기 (1편 - 서버컴포넌트 잡기술)

Sming·2023년 6월 6일
53
post-thumbnail

😎😎 인기만점 서버 컴포넌트 😎😎

요즘 프론트엔드 부분에서 가장 큰 이슈 중 하나인 서버 컴포넌트는 대부분 들어보셨을겁니다. 하지만 실제로 서버 컴포넌트production에서 운영중인 회사도 많이 없을 뿐더러 규모가 큰 프로젝트에서 서버 컴포넌트를 사용한 사례가 크게 없는듯 하여 이번에 실제로 서버 컴포넌트를 운영까지 반영하며 겪었던 이슈와 그를 어떤 방식으로 해결하였고 올라간 복잡도를 어떻게 간편화 할 것 인지에 대해서 한번 알아보겠습니다.

🚗 server component로 인한 코드 작성법 변경 - 관심사의 분리

가장 먼저 알아볼 것은 server component로 인해 nextjs 코드 작성의 변경점입니다.

서버 컴포넌트의 등장으로 인해서 nextjs를 작성할때 복잡도가 올라갔다고 많이 얘기를 합니다.

실제로 사용을 해보면 서버 컴포넌트로 이용하려는데 몇가지 요인때문에 이용을 못하거나 server component에서는 useState, useEffect, 그리고 browser api 등을 이용하지 못하 client component내부에 server component를 import못하는 문제등으로 인해서 굉장히 코드를 작성하기 힘들어집니다.

이에 대해서는 서버 컴포넌트단점고려사항만을 모은 추가 게시물을 작성할 예정입니다.


하지만 다른관점으로 바라보면 저는 이 서버 컴포넌트의 존재가 관심사의 분리로서의 어느정도 강제성을 주는 이점이 존재한다고 생각합니다.

react-query가 나온 원인 중 하나인 redux-saga, redux-thunk 같이 redux라는 하나의 스토어에서 클라이언트, 서버 데이터를 모두 다루는 것을 서버와 클라이언트 데이터에 대한 관심사를 분리하기 위해 나왔던 것 처럼 말이죠.

이제 react-query가 맡던 서버 상태에 대한 관리는 서버 컴포넌트에서 모두 처리하며 그 데이터가 그리는 것 마저 서버에서 처리하기 떄문에 client component는 정말 서버 상태가 존재하지 않는 순수 client 에 대한 것만 처리하게 됩니다.


저는 다음과 같은 폴더 관리 방법으로 서버 컴포넌트클라이언트 컴포넌트를 분리하였습니다.

먼저 도메인 별로 관리할 폴더를 나눈뒤 그 내에서 server, client를 분리하여 컴포넌트를 관리하도록 하였습니다.

ex)

- shop
  - server
    - shopList (server component)
  - client
    - shopItem (client component)

🌧 서버 컴포넌트에 외부 클라이언트 로직 부여하기

다음 알아볼 방법은 서버 컴포넌트에 외부 클라이언트 로직을 부여하기 입니다.

과연 이게 어떤 상황일까요?

예를 들어 특정 listserver component로 그렸다고 가정하겠습니다. 그런데 그 list를 swiper로 적용하고 싶다면?, 혹은 intersection observer를 통하여 무한 스크롤 혹은 특정 지점에 감지되었을때 동작을 하고 싶다면?

서버 컴포넌트에서는 이러한 클라이언트 로직을 이용할 수 없기때문에 개발에 지장을 줄 수 있습니다.

그래서 제가 생각한 방법은 간단합니다. 기존 클라이언트 로직을 부모 컴포넌트에 넘기는 것이죠.

마치 SuspenseErrorBoundary를 이용할때 로딩과 에러에 대한 로직을 부모 컴포넌트에 위임하는것으로 관심사를 분리한것처럼 말이죠.

// server component

export async function List() {
  const listData = await getListData();

  return (
      <nav className="categoryMenu">
        <ul className="list">
          <GnbFallback />
          {gnbData?.map((datum, idx) => (
            <GnbItem key={idx} {...datum} />
          ))}
          <GnbMoreButton />
        </ul>
        <GnbCloseButton />
      </nav>
  );
}

여기서 ListLogicWrapper 이라는 client component 를 하나 만든 후 서버 컴포넌트에 감싸주면 됩니다.

그러면 ListLogicWrapper의 내부를 볼까요?

const ListLogicWrapper = ({ children }: { children: React.ReactNode }) => {
  const [isExpanded, _] = useGnbExpanded(); // list가 펴질지 안펴질지 처리하는 훅
  useFixedGnb(); // list가 특정 위치에 도달할시 intersection observer를 통하여 fixed 처리하는 훅
  useGnbDimmed('.list'); // list가 펼쳐질 경우 외부화면을 dimmed 처리하는 로직

  // gnb 펼칠지, 안펼칠지 확인
  useEffect(() => {
    const target = document.querySelector('.categoryMenu');

    if (isExpanded) {
      target?.classList.add('active');
      return;
    }

    target?.classList.remove('active');
  }, [isExpanded]);

  return <>{children}</>;
};

일단 로직별로 하나하나 Wrapper를 만들면 오히려 jsx를 읽는데 불편하다고 생각되었기때문에 단순히 이 컴포넌트의 로직을 처리한다는 의미로 ListLogicWrapper와 내부에서 custom hook을 이용하여 로직에 대한 관심사를 분리하여 처리하였습니다.

그래서 하나의 wrapper에 list가 펴질지 안펴질지 처리, list가 특정 위치에 도달할시 intersection observer를 통하여 fixed 처리, list가 펼쳐질 경우 외부화면을 dimmed 처리등의 로직이 존재하는 것이죠.

문제점

여기서 제가 생각하는 하나의 문제점이 있습니다. client component와 server component간의 ref를 이용하여 서로 전달받을 수 없다는것. 그렇기 때문에 react에서 안티패턴중 하나인 real-dom에 직접 접근을 하여 처리를 해야합니다.

그리고 현재는 회사에서 퍼블리셔팀이 따로 존재하기에 pure css를 이용하고 있는데 이것이 후에 css-in-js로 변경되었을때 어떻게 관리하면 좋을지에 대해서도 따로 알아봐야 될 내용입니다.

⛵️ 여러개의 서버컴포넌트 이용

다음은 하나의 클라이언트 컴포넌트에 여러개의 서버컴포넌트를 부르고 싶을 때 입니다.

사실 이건 간단한데요.

따로 nextjs docs에도 없고 여러분들이 typescript를 이용하실때 보통 사용하는 children 타입을 쓰면 type error가 나오기 때문에 몰랐을 가능성이 있습니다.

{children}: {children: React.ReactNode}

사실 children은 배열형태로 오기때문에 서버컴포넌트를 이용할떄는 children[0], children[1]과 같이 직접 요소에 접근하여서 사용하시면 됩니다.

<ClientComponent>
  <FirstServerComponent />
  <SecondServerComponent />
</ClientComponent>
// ClientComponent.tsx

export const ClientComponent = ({children}: {children: React.ReactNode[]}) => {
  return (
    <div>
      {children[0]} // FirstServerComponent
      {children[1]} // SecondServerComponent
    </div>
  )
}
profile
딩구르르

11개의 댓글

comment-user-thumbnail
2023년 6월 6일

답글 달기
comment-user-thumbnail
2023년 6월 8일

문제점에서 언급해주신 대로 real dom을 접근해서 수정하는 것은 지양해야 되고 client component에서 처리하는 것이 적절해보이는데 그럼에도 여기에서 server 컴포넌트를 사용하는 이유가 있나요?

1개의 답글
comment-user-thumbnail
2023년 6월 10일

아진짜감사합니다^^...애먹고있었는데 진짜진짜진짜진짜감사합ㄴ디ㅏ!!!!!!!!!!!!!!

답글 달기
comment-user-thumbnail
2023년 6월 14일

와 이거 너무 상세한 게시물이네요. 저는 코딩에 대해 잘 모르지만 이것이 저를 흥미롭게 만들었습니다. 코딩을 해볼까요? 시작하는 것은 매우 힘들지만 시간이 지날수록 쉬워진다고 들었습니다.

1개의 답글
comment-user-thumbnail
2023년 6월 30일

궁금한게있습니다. 리스트를 불러올때 필터링하는 기능까지 추가한다면 여기서부턴 서버컴포넌트에서 fetch 할수없는건가요? 예를들면 낮은가격순 높은가겨순 등등 필터를 state로 관리한다고 했을때요!

1개의 답글
comment-user-thumbnail
2023년 8월 10일

안녕하세요! 좋은 글 감사합니다!
혹시 다른 주제의 외람된 질문이지만 서버컴포넌트내에서 회원정보 처리는 어떤식으로 하고 계신지 여쭈어봐도 될까요 ?
저는 현재 서버컴포넌트내에서 cookies객체를 통해 fetch로 액세스 토큰과 리프레시 토큰을 백단으로 보내서 인증을 해보려고했는데, 현재 쿠키가 안보내지는 상황이어서요..!
혹시 작성자님께선 어떤식으로 구현하고 계신지 여쭙고 싶습니다 !

1개의 답글