Next.js 문제 해결 - 클라이언트 컴포넌트에 컴포넌트 넘기기
2024. 10. 6.
얼마 전 업데이트로 서버 컴포넌트에서 클라이언트 컴포넌트로 컴포넌트를 props로 넘길 수 없게 되었다.
서버 컴포넌트에서 클라이언트로 함수를 props로 넘기면 Server Action이라는 기능을 사용하려던 것으로 해석되기 때문이다.
목차
수정 전 코드
page.tsx
'use server';
import { notFound } from 'next/navigation';
import { ComponentType } from 'react';
import { ClientComponent } from './ClientComponent';
interface Params {
params: Record<'slug', string>;
}
export default async function Post({ params }: Params) {
const MDXContent = await (async () => {
let result: ComponentType;
try {
result = await import('./data/' + params.slug + '.js');
} catch {
throw notFound();
}
return result;
})();
return <ClientComponent MDXContent={MDXContent} />;
}
ClientComponent.tsx
'use client';
import { ComponentType } from 'react';
export interface ClientComponentProps {
MDXContent: ComponentType;
}
export function ClientComponent({ MDXContent }: ClientComponentProps) {
// ...
return (
<div>
{/* ... */}
<MDXContent />
</div>
);
}
매우 그럴 듯한 코드다. 별 문제 없어 보인다.
하지만 빌드 시 이런 오류가 발생한다.
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or maybe you meant to call this function rather than return it.
수정 후 코드
page.tsx
'use server';
import { notFound } from 'next/navigation';
import { ComponentType } from 'react';
import { ClientComponent } from './ClientComponent';
interface Params {
params: Record<'slug', string>;
}
export default async function Post({ params }: Params) {
const MDXContent = await (async () => {
let result: ComponentType;
try {
result = await import('./data/' + params.slug + '.js');
} catch {
throw notFound();
}
return result;
})();
return <ClientComponent MDXContent={<MDXContent />} />;
}
ClientComponent.tsx
'use client';
import { ReactElement } from 'react';
export interface ClientComponentProps {
MDXContent: ReactElement;
}
export function ClientComponent({ MDXContent }: ClientComponentProps) {
// ...
return (
<div>
{/* ... */}
{MDXContent}
</div>
);
}
변경사항 별 거 없다.
page.tsx.diff
--- before/page.tsx
+++ after/page.tsx
@@ -20,5 +20,5 @@
return result;
})();
- return <ClientComponent MDXContent={MDXContent} />;
+ return <ClientComponent MDXContent={<MDXContent />} />;
}
ClientComponent.tsx.diff
--- before/ClientComponent.tsx
+++ after/ClientComponent.tsx
@@ -1,9 +1,9 @@
'use client';
-import { ComponentType } from 'react';
+import { ReactElement } from 'react';
export interface ClientComponentProps {
- MDXContent: ComponentType;
+ MDXContent: ReactElement;
}
export function ClientComponent({ MDXContent }: ClientComponentProps) {
@@ -11,7 +11,7 @@
return (
<div>
{/* ... */}
- <MDXContent />
+ {MDXContent}
</div>
);
}
그냥 컴포넌트였던 것을 리액트 엘리먼트로 바꿔서 넘겼을 뿐이다.
넘길 컴포넌트의 props가 가변이라면?
그럼 매우 귀찮아진다.
MyPropsContext.ts
"use client";
import { createContext } from "react";
export interface MyProps {
blah: string;
something: number;
}
export const MyPropsContext = createContext<MyProps | undefined>(undefined);
우선 context를 하나 만들고,
page.tsx
'use server';
import { notFound } from 'next/navigation';
import { ComponentType } from 'react';
import { ClientComponent } from './ClientComponent';
interface Params {
params: Record<'slug', string>;
}
export default async function Post({ params }: Params) {
const DynamicContent = await (async () => {
let result: ComponentType;
try {
result = (await import('./data/' + params.slug + '.tsx')).default;
} catch {
throw notFound();
}
return result;
})();
return <ClientComponent dynamicContent={<DynamicContent />} />;
}
ClientComponent.tsx
"use client";
import { ReactElement, useMemo, useState } from "react";
import { MyPropsContext } from "./MyPropsContext";
export interface ClientComponentProps {
dynamicContent: ReactElement;
}
export function ClientComponent({ dynamicContent }: ClientComponentProps) {
const [blah, setBlah] = useState("");
const [something, setSomething] = useState(0);
const contextValue = useMemo(() => ({ blah, something }), [blah, something]);
return (
<div>
<input
placeholder="blah"
value={blah}
onChange={(e) => setBlah(e.target.value)}
/>
<button onClick={() => setSomething((n) => n + 1)}>
Increase something (currently {something})
</button>
<MyPropsContext.Provider value={contextValue}>
{dynamicContent}
</MyPropsContext.Provider>
</div>
);
}
<DynamicContent />를 context provider에 넣고,
data/test.tsx
"use client";
import { useContext } from "react";
import { MyProps, MyPropsContext } from "../MyPropsContext";
function Test({ blah, something }: MyProps) {
return (
<div>
<p>blah: {blah}</p>
<p>something: {something}</p>
</div>
);
}
export default function ActualComponent() {
const context = useContext(MyPropsContext);
return context && <Test {...context} />;
}
그리고 매우 귀찮지만, 각 파일에서 그 context를 사용하는 컴포넌트로 감싸주면 되기는 한다.
실제로 동작하는 예시는 여기에서 확인할 수 있다.
Next.js