
🧐 의문점: Recoil을 Next.js에서 사용할 때는 Recoil을 따로 client component로 감싸서 사용해야한다고 한다. 왜 불편하게 한번 더 감싸야하는 걸까?
next 프로젝트에 recoil을 도입하려고 하고 있었다. 그래서 기존에 사용하던 방법대로 제일 상위 파일인 layout.tsx에 <RecoilRoot>를 선언해주었는데, createContext는 클라이언트 컴포넌트에서만 동작하니 'use client' directive를 적어달라고 오류가 뜨게 되었다. 그래서 구글링을 해보니 누군가의 깃허브에서 내가 겪은 문제에 대한 답을 보게 되었다.

이렇게 <RecoilRoot>를 'use client' directive를 선언한 클라이언트 컴포넌트에 담아 다시 layout.tsx에서 import 하라는 말이었다.
그런데 왜 이렇게 귀찮은 일을 해야하는지 의문이 들어 이유를 찾아보게 되었다.
검색해봐도 그냥 wrapper로 감싸면 된다고만 적혀있고 정확한 이유는 없었다.
다만 추측해보자면, createContext가 서버에서 동작하게 되면 값이 바뀔 때마다 서버가 이 context를 구독하는 컴포넌트에게 값을 전송해야 한다. 만약에 1초에 10000번 바뀌는 상태가 있다고 하면 이는 서버에 과부하를 일으킬 수 있는 심각한 문제다.
또한 서버 컴포넌트는 클라이언트와 달리 브라우저가 아닌 서버에서 실행되므로, 상호작용적인(상태를 사용하는)기능을 가질 필요가 없다. 그리고 데이터 처리, 로직 수행, 데이터베이스 연동 등의 역할은 서버에서 해야하기 때문에 확실하게 클라이언트와 서버의 역할을 나누려고 react hooks를 사용하지 못하도록 막은 걸 수도 있다.
아마 전자의 이유가 더 커서 createContext를 클라이언트 컴포넌트에서만 사용할 수 있게 제한해놓지 않았나 예상했다.
Recoil의 라이브러리 내부를 보면 core폴더에 Recoil_RecoilRoot.js.flow파일이 존재한다. 이 파일이 RecoilRoot가 선언된 파일이다. 여기서 어떻게 createContext를 사용하는지 살펴보자.(굳이 볼 필요는 없는데 개인적인 궁금증..)
▼createContext를 검색하면 이렇게 우리가 원래 사용하던 형태대로 선언되어 있다.

▼createContext를 담은 AppContext를 보면 RecoilRoot_INTERNAL함수의 return안에 포함되어 있다.

▼마지막으로 RecoilRoot_INTERNAL은 우리가 항상 사용하던 익숙한 이름인 RecoilRoot함수의 return안에 포함되어 있다.

이렇게RecoilRoot의 createContext가 사용되는 구조를 살펴보았다.
RecoilRoot가 우리가 늘 사용하던 대로 createContext가 선언되었다는 걸 확인했다.
그런데 next.js에서는 기본적으로 별다른 directive가 없다면 모든 컴포넌트는 서버 컴포넌트로 만들어진다. 앞에서 createContext가 선언된 파일인 Recoil_RecoilRoot.js.flow의 맨 위를 보면 'use client'는 어디에도 없다.▼

그러면 이 컴포넌트는 자동으로 서버 컴포넌트가 되는데, 앞에서 createContext가 서버 컴포넌트에 사용될 수 없는 이유를 설명했었다. 따라서 우리는 이 Recoil_RecoilRoot.js.flow에 선언된 RecoilRoot를 클라이언트 컴포넌트로 만들어야한다.
next.js에서 클라이언트 컴포넌트를 만드는 방법은 정말 간단하게도 파일 최 상단에 'use client'라고 적어주는 것이다. 이 RecoilRoot를 클라이언트 컴포넌트로 만들기 위해서는 클라이언트 컴포넌트인 Wrapper로 RecoilRoot 컴포넌트를 감싸면 된다.
'use client';
import { RecoilRoot } from 'recoil';
import React from 'react';
interface RecoilRootWrapperProps {
children: React.ReactNode;
}
export default function RecoilRootWrapper({
children,
}: RecoilRootWrapperProps) {
return <RecoilRoot>{children}</RecoilRoot>;
}
그리고 이 RecoilRootWrapper를 layout.tsx에서 호출해보자.
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='en'>
<body className={inter.className}>
<RecoilRootWrapper>
{children}
{...}
</RecoilRootWrapper>
</body>
</html>
);
}
▼ 실행시켜보니 정상 작동한다.

모든 상태관리 라이브러리에 'use client' directive가 있는 것은 아니다. 따라서 Recoil이 아닌 다른 라이브러리를 사용한다고 하더라도 context를 클라이언트 컴포넌트에 감싸서 사용해야할 수 있다.
next.js에서 클라이언트 컴포넌트로 서버 컴포넌트를 감싸면 서버 컴포넌트는 클라이언트 컴포넌트처럼 동작한다고 했다. 위에서 만든 RecoilRootWrapper는 클라이언트 컴포넌트이고 children에 서버 컴포넌트가 들어오면 이 서버 컴포넌트들이 다 클라이언트 컴포넌트로 동작하는 것이 아닐까?
위의 의문을 해결하기 위해 아래 예시 구조를 살펴보자.
layout의 children에는 page.tsx에서 선언한 컴포넌트가 들어간다. page.tsx에는 <Home>컴포넌트가 아래 코드처럼 선언되어 있다.
// ./app/page.tsx
import ServerComponent from '...';
import ClientComponent from '...';
const Home = () => {
return(
<div>
home !
</div>
);
};
export default Home;
이때 layout의 children에는 <Home>이 들어간다. 그러면 구조는 사실 아래와 같다.
// ./app/layout.tsx
// 보이는 코드
<RecoilRoot>
{children}
</RecoilRoot>
// 실제 코드
<RecoilRoot children={<Home/>}/>
실제 코드라고 주석이 적힌 코드를 보면 children prop으로 실제로 자식으로 들어올 page.tsx의 <Home>이 넘겨지고 있다. 이런 구조가 되면 <RecoilRoot>와 <Home>은 이들이 선언된 위치인 layout에서 렌더링되게 된다. 그리고 <RecoilRoot>로 <Home>이 호출된 결과를 children prop으로 전달하게 되는 것이다.
그러면 결국 <RecoilRoot>안에 <Home>가 하위 컴포넌트로 있는 것 처럼 보이지만 사실 둘은 같은 부모를 둔 자식 컴포넌트가 되므로, <RecoilRoot>이라는 클라이언트 컴포넌트 안의 <Home>서버 컴포넌트가 아니게 된다. 같은 부모인 layout안에서 <RecoilRoot>은 클라이언트 컴포넌트로써, <Home>은 서버 컴포넌트로써 각자 제 역할을 잃지않고 잘 수행할 수 있는 것이다.
이렇게 RecoilRoot안에 들어가는 모든 컴포넌트 들은 클라이언트 컴포넌트가 되는 것이 아닐까라는 의문은 children prop을 사용한 패턴의 동작 원리로 인해 말끔히 해결되었다.
또한 이런 children prop pattern은 불필요한 렌더링을 줄이기 위해서도 사용되기도 한다.
https://www.craftvalue.io/blog/react-children-prop-pattern
https://velog.io/@2ast/React-children-prop%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0feat.-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94
https://github.com/facebookexperimental/Recoil/issues/2082
틀린 내용이 있다면 편하게 말씀해주세요 !
좋은 정보 얻어갑니다, 감사합니다.