요즘 프론트엔드 부분에서 가장 큰 이슈 중 하나인 서버 컴포넌트
는 대부분 들어보셨을겁니다. 하지만 실제로 서버 컴포넌트
를 production에서 운영중인 회사도 많이 없을 뿐더러 규모가 큰 프로젝트에서 서버 컴포넌트
를 사용한 사례가 크게 없는듯 하여 이번에 실제로 서버 컴포넌트
를 운영까지 반영하며 겪었던 이슈와 그를 어떤 방식으로 해결하였고 올라간 복잡도를 어떻게 간편화 할 것 인지에 대해서 한번 알아보겠습니다.
가장 먼저 알아볼 것은 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)
다음 알아볼 방법은 서버 컴포넌트에 외부 클라이언트 로직을 부여하기
입니다.
과연 이게 어떤 상황일까요?
예를 들어 특정 list
를 server component로 그렸다고 가정하겠습니다. 그런데 그 list
를 swiper로 적용하고 싶다면?, 혹은 intersection observer를 통하여 무한 스크롤
혹은 특정 지점에 감지
되었을때 동작을 하고 싶다면?
서버 컴포넌트에서는 이러한 클라이언트 로직을 이용할 수 없기때문에 개발에 지장을 줄 수 있습니다.
그래서 제가 생각한 방법은 간단합니다. 기존 클라이언트 로직을 부모 컴포넌트에 넘기는 것이죠.
마치 Suspense
와 ErrorBoundary
를 이용할때 로딩과 에러에 대한 로직을 부모 컴포넌트에 위임하는것으로 관심사를 분리한것처럼 말이죠.
// 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>
)
}
문제점에서 언급해주신 대로 real dom을 접근해서 수정하는 것은 지양해야 되고 client component에서 처리하는 것이 적절해보이는데 그럼에도 여기에서 server 컴포넌트를 사용하는 이유가 있나요?
와 이거 너무 상세한 게시물이네요. 저는 코딩에 대해 잘 모르지만 이것이 저를 흥미롭게 만들었습니다. 코딩을 해볼까요? 시작하는 것은 매우 힘들지만 시간이 지날수록 쉬워진다고 들었습니다.
궁금한게있습니다. 리스트를 불러올때 필터링하는 기능까지 추가한다면 여기서부턴 서버컴포넌트에서 fetch 할수없는건가요? 예를들면 낮은가격순 높은가겨순 등등 필터를 state로 관리한다고 했을때요!
캬