
React Query를 사용하면서, CSR과 SSR의 경계에 대해 갑자기 궁금해지기 시작했다. useQuery 훅을 클라이언트에서 사용하려면, 해당 컴포넌트가 use client로 선언된 클라이언트 사이드 컴포넌트여야 한다. 그리고 그 컴포넌트 상위에는 QueryClientProvider로 감싸져 있어야 전역적으로 쿼리 클라이언트를 관리할 수 있다.
이때 QueryClientProvider는 일반적으로 use client를 사용하여 클라이언트 사이드에서 전역 상태를 관리하도록 설정된다. 그런데 QueryClientProvider가 app/layout.tsx와 같이 최상단에 use client로 감싸져 있으면, 결국 그 하위에 있는 모든 컴포넌트들이 클라이언트 사이드에서 동작하는 상황이 발생한다고 생각했다. 이 경우, Next.js의 SSR 기능이 제대로 작동하지 않거나, 서버 사이드 렌더링의 장점이 사라지는 것이 아닐까 하는 의문이 들었다.
그래서 이 의문을 풀기 위해 직접 실험을 해보기로 했다.
export default function ServerComponent({
children,
}: Readonly<{ children: React.ReactNode }>) {
console.log("SSR (서버)");
return (
<div>
<div>서버 컴포넌트</div>
{children}
</div>
);
}
"use client";
import { useEffect } from "react";
export default function ClientComponent({
children,
}: Readonly<{ children: React.ReactNode }>) {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
{children}
</div>
);
}
먼저 이렇게 서버컴포넌트와 클라이언트 컴포넌트를 만들었다.
Next.js 13부터는 console을 찍으면 서버컴포넌트에서 콘솔이 나올 때는 왼쪽에 Server라는 회색태그가 생기니 구분하기 쉽다.

app/page.tsx
export default function Home() {
return (
<ServerComponent>
<ClientComponent />
</ServerComponent>
);
}


당연한 결과이다. 서버 클라이언트의 하단에 클라이언트 컴포넌트가 있기 때문에 예상대로 동작한다.
app/page.tsx
export default function Home() {
return (
<ServerComponent>
<ServerComponent1 />
</ServerComponent>
);
}


서버 클라이언트의 하단은 use client를 선언하지 않은 이상, 서버 클라이언트로 렌더되기에 당연한 결과이다.
app/page.tsx
export default function Home() {
return (
<ClientComponent>
<ClientComponent1 />
</ClientComponent>
);
}


이것도 당연한 결과이다.
app/page.tsx
export default function Home() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}


하지만 여기선 좀 흥미로운 결과이다. 클라이언트 컴포넌트의 하위는 전부 클라이언트 컴포넌트로 렌더되는 줄 알았는데, 서버 컴포넌트는 문제없이 서버 사이드에서 렌더가 된다!!
클라이언트 컴포넌트는 브라우저가 JS코드를 런타임에 분석해서 렌더하는 것이 맞다.
하지만 클라이언트 컴포넌트가 props.children으로 서버 컴포넌트를 받게 되면, Next.js에서는 props로 받은 컴포넌트를 먼저 컴파일하고 부모인 클라이언트 컴포넌트를 나중에 렌더링하게 된다고한다.
export default function Home() {
return (
<ClientComponent> // => 브라우저상에서 두번째로 렌더링
<ServerComponent /> // => 첫번째로 컴파일
</ClientComponent>
);
}
하지만 클라이언트 컴포넌트안에 서버컴포넌트가 있다고 반드시 서버컴포넌트로 동작하는 것도 아니다!!
"use client";
import { useEffect } from "react";
import ServerComponent from "./ServerComponent";
export default function ClientComponent() {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
<ServerComponent />
</div>
);
}
이렇게 props로 받는 것이 아닌 클라이언트에서 직접적으로 서버컴포넌트를 즉시 return 하는 경우를 출력해보면...


예상과는 다르게 전부 클라이언트 사이드에서 로그가 찍힌다.
※ SSR (서버)라는 메시지는 컴포넌트 별 구분을 위한 차이일뿐, Server라는 회색태그가 없으므로 둘다 Client Side이다.
Next.js에서는 클라이언트 컴포넌트 내부에서 서버 컴포넌트를 직접 호출하는 것을 허용하지 않는다.
그 이유는 클라이언트 컴포넌트는 브라우저에서 실행되므로, 서버에서만 동작해야 하는 컴포넌트를 실행할 수 없기 때문이다.
💡 Next.js는 빌드 시 서버 컴포넌트가 클라이언트 컴포넌트 내부에서 직접 호출되었는지를 감지하고, 이를 일반 클라이언트 컴포넌트로 변환한다.
즉, Next.js는 "이건 클라이언트에서 실행될 수밖에 없겠군!"이라고 판단하고 자동으로 CSR로 변경해버린다.
"use client";
import { useEffect } from "react";
export default function ClientComponent({
children,
}: Readonly<{ children: React.ReactNode }>) {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
{children}
</div>
);
}
export default function Home() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}


ServerComponent => 서버사이드
ClientComponent => 클라이언트사이드
"use client";
import { useEffect } from "react";
import ServerComponent from "./ServerComponent";
export default function ClientComponent() {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
<ServerComponent />
</div>
);
}
export default function Home() {
return (
<ClientComponent />
);
}


ServerComponent => 클라이언트사이드
ClientComponent => 클라이언트사이드
"use client";
import React, { useEffect } from "react";
export default function ClientComponent({
serverComponent,
}: {
serverComponent: React.ReactNode;
}) {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
{serverComponent}
</div>
);
}
export default function Home() {
return <ClientComponent serverComponent={<ServerComponent />} />;
}


children과 마찬가지로 먼저 ServerComponent를 해석한 후에, ClientComponent를 클라이언트사이드에서 렌더링시키려고 하기 때문에, 정상적으로 동작한다.
ServerComponent => 서버사이드
ClientComponent => 클라이언트사이드
"use client";
import React, { useEffect } from "react";
export default function ClientComponent({
renderServerComponent,
}: {
renderServerComponent: () => React.ReactNode;
}) {
useEffect(() => {
console.log("CSR (클라이언트)");
}, []);
return (
<div>
<div>클라이언트 컴포넌트</div>
{renderServerComponent()}
</div>
);
}
export default function Home() {
return <ClientComponent renderServerComponent={() => <ServerComponent />} />;
}

renderProps는 Next.js에서 코드를 직렬화하는 과정에서 () => React.ReactNode 라는 타입을 JSX로 해석할 수 없어서 클라이언트에서 renderServerComponent를 실행해서 해석해야한다.
하지만 renderServerComponent는 서버컴포넌트를 렌더시키는 함수이기 때문에 이런 에러를 뱉는 것이다.
1️⃣ 클라이언트 컴포넌트 내부에서 서버 컴포넌트를 사용하려면 ReactNode 타입의 props로 전달해야 한다.
2️⃣ render props(즉, () => ReactNode) 방식은 Next.js 직렬화 제한 때문에 사용할 수 없다.
3️⃣ ReactNode(JSX)를 props로 직접 넘기면 서버에서 렌더링된 후 클라이언트에서 사용할 수 있다.
🚀 즉, ReactNode 방식 (를 직접 props or children 으로 넘기기)이 가장 올바른 방법!