Event handlers cannot be passed는 서버 컴포넌트가 onClick 함수를 클라이언트 속성으로 넘긴 직렬화 오류입니다. 버튼 파일로 나누고 주문 번호만 넘기며, 서버 함수만 예외입니다. useState 지시자 오류와 줄을 가릅니다. Next.js, use client, 프론트, 한국 1인 개발. 2026년 8월 경계 문서.
Event handlers cannot be passed to Client Component props는 훅 실수가 아니라, 서버 컴포넌트가 onClick 함수를 클라이언트 속성으로 넘기다 직렬화가 거절된 줄입니다. 버튼만 'use client' 파일로 옮기고, 서버에서는 주문 번호만 넘기세요.
앱 라우터 페이지는 기본이 서버입니다. 그 파일에서 함수를 넘기면 런타임이 그 속성을 가리키며 멈춥니다. 예외는 'use server'로 표시한 서버 함수입니다.
useState를 서버 파일에서 부른 문장과는 다릅니다. 그 줄은 지시자가 없는 훅이고, 이번 줄은 함수를 경계 너머로 보낸 직렬화입니다.
서버가 함수를 클라이언트로 못 넘긴다
경계 너머로 가는 것은 값이 아니라 함수라서 멈춥니다.
넥스트 경계 가이드는 두 규칙을 나눕니다. 코드는 import를 통해 클라이언트 번들로 들어가고, 데이터는 속성으로 건너며 직렬화되어야 합니다. 그래서 서버 컴포넌트에서 클라이언트 컴포넌트로 함수를 넘기면 던진다고 적습니다. onClick은 그 예입니다. 에러는 문제 속성 옆에 function이라고 표시합니다.
서버 컴포넌트는 브라우저에서 그 함수를 다시 실행할 자바스크립트를 보내지 않습니다. 클릭 리스너는 브라우저에 있어야 하므로, 서버가 만든 함수 객체를 JSON처럼 실어 나를 방법이 없습니다. 페이지 전체를 클라이언트 파일로 바꾸기 전에, 어느 속성이 함수인지 에러가 가리키는 이름부터 보면 범위가 작아집니다.
에러가 가리키는 속성 | onClick={function}처럼 찍힌 이름만 클라이언트 파일 안으로 옮기면 됩니다. 페이지의 데이터 조회까지 옮길 필요는 없습니다.
서버 페이지는 값을 넘기고, 클릭 함수는 클라이언트 파일 안에 둔다
버튼만 파일로 쪼개기
핸들러는 클라이언트 파일 안에서 만듭니다.
서버 페이지는 주문을 읽고 orderId만 넘깁니다. 버튼 파일 첫 줄에 'use client'를 두고, onClick은 그 파일의 함수로 둡니다. 자식도 클라이언트여야 함수 속성을 받을 수 있습니다. 부모만 서버이고 자식만 클라이언트인 채로 onClick={handle}을 넘기면 같은 에러가 남습니다.
페이지 최상단에 지시자를 붙이면 그 파일이 import하는 모듈까지 클라이언트 번들로 딸려 옵니다. 조회와 비밀 키가 그 그래프에 있으면 브라우저로 내려갑니다. 버튼, 토글, 입력처럼 이벤트가 있는 조각만 분리하는 편이 번들이 작습니다. 서버에서 만든 리액트 요소는 직렬화되므로, children으로 서버 트리를 클라이언트 슬롯에 넣는 패턴은 그대로 허용됩니다.
넥스트는 'use server'로 표시한 서버 함수가 참조로 경계를 지난다고 적습니다. 타입만 봐서는 일반 함수와 구분이 안 되어, 타입스크립트 플러그인은 속성 이름이 action이거나 Action으로 끝날 때만 함수 속성을 허용하고 나머지는 표시합니다. 런타임은 서버 함수가 아닌 함수가 넘어오면 여전히 거절합니다.
클라이언트에서 서버 변경이 필요하면 액션 파일에 서버 함수를 두고, 버튼의 onClick 안에서 그 함수를 호출합니다. 서버 페이지가 액션을 속성으로 넘길 수도 있습니다. 변경 데이터 문서의 예는 updateItemAction을 클라이언트 폼의 action에 연결합니다. 클릭마다 서버 왕복이 생기므로, 화면 열림만 바꾸는 토글은 서버 함수로 만들지 않습니다.
클릭 안에서 서버 함수 호출
// app/orders/actions.js
'use server';
export async function markPaid(orderId) {
// 서버에서만 실행. 비밀 키는 이 파일에 둔다.
return { ok: true, orderId };
}
// pay-button.js
'use client';
import { markPaid } from '../actions';
export default function PayButton({ orderId }) {
return (
<button type="button" onClick={() => markPaid(orderId)}>
결제 완료
</button>
);
}
useState 지시자 에러와 다른 줄
훅 문장과 함수 속성 문장은 고치는 파일이 다릅니다.
콘솔
의미
고치는 곳
useState는 클라이언트에서만
서버 파일이 훅을 호출
훅이 있는 파일에 use client
Event handlers cannot be passed
함수 속성이 경계를 넘음
핸들러를 클라이언트 파일 안으로
함수는 클라이언트로 직접 전달 불가
서버 함수 표시 없는 함수
use server 이거나 값만 전달
Dynamic server usage, cookies
정적 생성 중 요청 API
동적 렌더 또는 쿠키를 요청 밖으로
Hydration failed
첫 HTML과 브라우저 렌더가 다름
첫 렌더 값을 서버와 맞추기
지시자를 이미 붙였는데도 함수 속성 에러가 남으면, 그 함수가 아직 서버 부모에서 만들어져 내려온 것입니다. 클라이언트 파일끼리 함수를 주고받는 것은 둘 다 브라우저 그래프 안이라 허용됩니다. 조상 중 서버가 그 함수를 만든 경우만 거절입니다. 한국에서 넥스트 앱을 혼자 올리는 경우, 공용 버튼 컴포넌트만 클라이언트로 두고 페이지에서 onClick을 넘기다가 이 줄을 만납니다.
넘겨도 되는 값
문자열, 숫자, 평범한 객체, 서버 함수 참조, 렌더된 요소는 넘어갑니다.
날짜 객체나 클래스 인스턴스, 맵, 심벌은 직렬화에서 또 걸릴 수 있으니 문자열이나 숫자로 바꿉니다. 사용자 레코드 전체를 넘기면 비밀번호 해시까지 브라우저 페이로드에 실릴 수 있습니다. 버튼에 필요한 필드만 고릅니다. 서버 전용 모듈을 클라이언트 파일이 import하면 경계 에러가 한 줄 더 납니다. 그 모듈은 서버 페이지와 서버 함수 쪽에 둡니다.
에러 바운더리는 렌더 중 예외를 잡고, 이벤트 핸들러 안의 예외는 잡지 않는다고 넥스트 에러 처리 문서가 적습니다. 클릭 함수 안의 실패는 클라이언트에서 직접 잡아 상태에 넣어야 화면에 남습니다. 경계를 고친 뒤 버튼이 조용히 아무 일도 안 하면, 그 핸들러의 예외를 따로 보세요.