TechFeedTechFeed
Frontend

useState, use client, 서버 컴포넌트 | 훅을 넣었는데 빨간 줄이면?

앱 라우터에서 useState가 막히면 그 파일은 아직 서버 컴포넌트입니다. 훅을 쓰는 파일 맨 위에 use client를 붙이거나 버튼만 쪼갭니다. Invalid hook call, 하이드레이션과 자리를 나눕니다. 넥스트, 리액트, 서버 컴포넌트, 카카오 로그인, 프론트엔드, 1인 개발자 기준. 2026년 9월 Next.js, React 공식 문서.

by

앱 라우터에서 useState가 막히면 그 파일은 아직 서버 컴포넌트입니다. 훅을 쓰는 파일 맨 위, 임포트보다 앞에 'use client'를 붙이거나, 클릭이 있는 칸만 다른 파일로 쪼개세요.


넥스트 앱 폴더에서 카카오 로그인 버튼을 페이지에 바로 넣다가 needs useState 빨간 줄을 보셨을 겁니다. 페이지는 기본이 서버라 클릭 상태를 못 들고, 훅은 브라우저 쪽에서만 살아 있습니다.


공식 문서도 경계를 파일 단위로 긋습니다. 페이지 전체에 지시자를 붙이면 글 목록까지 클라이언트로 내려가니, 문의 버튼처럼 클릭이 있는 칸만 따로 빼는 편이 안전합니다. 근거는 넥스트 use client 문서리액트 use client 문서에 있습니다.


이 빨간 줄은 훅이 잘못된 걸까

훅이 틀린 게 아니라 그 파일이 서버 컴포넌트입니다. 앱 폴더의 페이지와 레이아웃은 지시자가 없으면 서버에서 먼저 그려지고, useStateuseEffect는 그 자리에서 쓸 수 없습니다.


메시지가 길게 이어집니다. 훅은 클라이언트 컴포넌트에서만 되고, 부모 중에 use client가 찍힌 파일이 없으니 기본값인 서버로 본다는 뜻이에요. 문법 오류가 아니라 실행 자리가 어긋난 겁니다.


제가 12개 사이트를 넥스트 앱 라우터로 돌리며 가장 자주 본 장면은 로그인 페이지에 버튼을 바로 넣은 경우입니다. 페이지는 글을 읽고 메타를 붙이는 서버 자리인데, 버튼이 클릭 상태를 들고 있으려다 같은 빨간 줄이 납니다. 훅 규칙을 어긴 줄과 자리를 나누세요. 조건 안에서 훅을 호출했거나 리액트 복사본이 두 개인 경우는 중복 리액트 훅 글 쪽입니다.


먼저 기억할 것 | 앱 폴더는 기본이 서버입니다. 훅, 클릭, 브라우저 저장소가 필요하면 그 파일만 클라이언트로 표시하세요. 페이지 전체를 클라이언트로 바꾸는 건 나중 선택입니다.


페이지 맨 위에 지시자를 붙이면

페이지 전체에 붙이면 목록과 데이터 조회까지 클라이언트로 내려갑니다. 빨간 줄은 사라지지만, 서버에 둬야 할 조회와 비밀 값까지 같이 넘어갑니다.


넥스트 문서는 지시자를 모든 파일에 반복하지 말라고 합니다. 서버 컴포넌트가 직접 그리는 입구 파일에만 찍으면 됩니다. 그 파일이 내보내는 컴포넌트가 클라이언트 입구가 되고, 그 아래로 이어지는 임포트는 같은 번들에 들어갑니다.


레이아웃에 붙이면 범위가 더 넓어집니다. app/layout.js 맨 위에 지시자를 두면 그 아래 페이지가 통째로 클라이언트 트리에 붙습니다. 제가 예전에 문의 폼 때문에 루트 레이아웃에 지시자를 넣었다가, 글 목록 조회가 브라우저로 내려간 적이 있습니다. 폼만 고치려다 사이트 전체가 무거워진 케이스예요.


붙인 자리빨간 줄같이 내려가는 것
훅이 있는 작은 파일사라짐그 버튼과 모달만
app/login/page.js사라짐로그인 페이지 전체
app/layout.js사라짐그 레이아웃 아래 모든 페이지
안 붙임그대로없음. 서버 유지

넥스트 앱 라우터에서 서버 페이지와 클라이언트 버튼을 파일로 나눈 모니터 화면
훅은 작은 파일에만 두고, 페이지는 서버로 남겨 둔다

버튼만 다른 파일로 빼는 순서

클릭이 있는 컴포넌트만 새 파일로 빼고 지시자를 그 파일 맨 위에 둡니다. 페이지는 서버로 남기고, 버튼을 임포트하면 됩니다.


순서는 짧습니다. 훅과 onClick이 있는 줄을 새 파일로 옮기고, 그 파일 첫 줄에 'use client'를 씁니다. 페이지는 데이터를 읽어 제목을 그린 뒤 그 버튼을 끼워 넣습니다. 서버가 클라이언트를 자식으로 두는 조합은 공식 문서가 권하는 방식입니다.


한국에서 카카오 로그인을 붙일 때도 같습니다. 인가 주소로 보내는 클릭만 클라이언트 파일이면 되고, 콜백에서 코드를 받아 세션을 만드는 칸은 서버 라우트나 서버 액션에 둡니다. 폼 제출 중 버튼을 잠그는 훅은 이중 제출 글과 같이 보면 자리가 더 분명해집니다.


페이지는 서버, 버튼만 클라이언트
// app/login/KakaoButton.js 'use client' import { useState } from 'react' export default function KakaoButton() { const [pending, setPending] = useState(false) return ( <button disabled={pending} onClick={() => { setPending(true) window.location.href = '/api/auth/kakao' }} > 카카오 로그인 </button> ) } // app/login/page.js | 서버 유지 import KakaoButton from './KakaoButton' export default function LoginPage() { return ( <main> <h1>로그인</h1> <KakaoButton /> </main> ) }

지시자 위치 | 파일 맨 위, 임포트보다 앞, 소문자 'use client'입니다. 주석이나 빈 줄은 앞에 둘 수 있지만, import 다음에 두면 지시자로 안 읽힙니다.


서버에 둘 것과 클라이언트에 둘 것

조회와 비밀 값은 서버, 클릭과 훅은 클라이언트입니다. 한 페이지 안에서 둘을 섞을 때는 파일 경계로 나눕니다.


넥스트 조합 패턴 문서는 역할을 이렇게 나눕니다. 서버는 데이터를 읽고, 토큰 같은 비밀을 지키며, 큰 라이브러리를 브라우저에 안 내려 보냅니다. 클라이언트는 클릭, 입력, 브라우저 저장소, 화면 크기를 다룹니다. 둘 다 필요한 화면은 서버 페이지가 클라이언트 조각을 자식으로 둡니다.


자식으로 children을 넘기는 패턴도 자주 씁니다. 클라이언트 껍데기가 열고 닫기만 하고, 무거운 본문은 서버가 그려 그 안에 넣는 식입니다. 제가 글 상세에서 쓰는 목차 접기도 껍데기만 클라이언트입니다. 본문 HTML은 서버가 그립니다.


할 일둘 자리이유
디비에서 글 읽기서버 페이지, 서버 액션연결 문자열이 브라우저에 안 나감
카카오 로그인 클릭클라이언트 파일onClick과 대기 상태가 필요
문의 폼 제출 중 잠금클라이언트 파일훅이 필요
제출 처리, 메일 발송서버 액션, 라우트비밀 키와 검증
localStorage 테마클라이언트 파일서버에 그 객체가 없음

테마처럼 브라우저 값을 첫 화면에 바로 그리면 하이드레이션이 갈라집니다. 그 칸은 이 글의 지시자 문제가 아니라 하이드레이션 미스매치 글입니다. 지시자를 붙인 뒤에 날짜가 깜빡이면 그쪽으로 가세요.


서버에서 글을 읽고 클라이언트 버튼만 브라우저로 내려보내는 개발 책상
조회는 서버, 클릭은 작은 클라이언트 파일로 나눈다

카카오 로그인 버튼에서 막힌 자리

카카오 버튼 onClick을 페이지에 직접 넣으면 같은 빨간 줄이 납니다. 인가 주소로 보내는 클릭만 클라이언트 파일로 옮기면 페이지는 서버로 남습니다.


제가 막혔던 줄은 이랬습니다. app/login/page.js 안에서 useState로 대기 표시를 켜고, 클릭하면 카카오 인가 URL로 보내려 했습니다. 로컬 터미널은 빌드가 되고, 브라우저만 개발 오버레이를 띄웠습니다. 페이지를 서버로 두고 버튼을 쪼개자 오버레이가 사라졌습니다.


콜백 라우트는 그대로 서버입니다. 카카오가 돌려주는 코드, 토큰 교환, 세션 쿠키는 브라우저 훅이 필요 없습니다. 클릭 칸과 콜백 칸을 한 파일에 섞지 마세요. 쿠키 전달이 막히면 지시자가 아니라 세임사이트 칸입니다.


페이지에 훅을 넣으면 터지는 줄
// app/login/page.js ← 기본은 서버. 여기서 훅을 쓰면 빨간 줄 import { useState } from 'react' export default function LoginPage() { const [pending, setPending] = useState(false) // needs useState return ( <button onClick={() => setPending(true)}> 카카오 로그인 </button> ) }

대소문자와 지시자 위치를 먼저 보라

지시자는 소문자, 임포트보다 앞입니다. 한 글자만 달라도 서버로 남아서 같은 빨간 줄이 납니다.


깃허브 넥스트 토론에도 'use Client'처럼 C만 대문자로 썼다가 지시자로 안 읽힌 사례가 있습니다. 따옴표는 작은따옴표나 큰따옴표 둘 다 되지만, 철자는 소문자 use client여야 합니다. 파일 중간에 넣거나 임포트 아래에 두면 번들러가 지시자로 안 봅니다.


서드파티 캐러셀처럼 패키지 안에 지시자가 없는 컴포넌트를 서버 페이지에 바로 넣어도 같은 계열의 줄이 납니다. 공식 문서의 처방은 얇은 래퍼 파일을 하나 만들고, 그 파일 맨 위에 지시자를 붙인 뒤 다시 내보내는 것입니다. 패키지를 고칠 수 없을 때 쓰는 칸입니다.


지시자 없는 패키지를 감싸기
// app/components/Carousel.js 'use client' export { Carousel } from 'acme-carousel' // app/gallery/page.js ← 서버 페이지에서 래퍼만 임포트 import { Carousel } from '../components/Carousel' export default function GalleryPage() { return <Carousel /> }
파일 맨 위 지시자 위치를 확인하는 코드 편집기 화면이 있는 책상
지시자는 임포트보다 앞, 소문자 use client만 읽힌다

함수 props는 다른 빨간 줄이다

함수 props 에러는 직렬화 문제라 use client를 더 붙여도 안 풀립니다. 서버가 클라이언트로 넘기는 값은 리액트가 직렬화할 수 있는 형태여야 합니다.


리액트 문서는 문자열, 숫자, 날짜, 평범한 객체, 배열, 그리고 서버 액션은 넘길 수 있다고 합니다. 일반 함수, 클래스, 심볼은 못 넘깁니다. 그래서 서버 페이지에서 onClick={handleClick}을 클라이언트 버튼에 주면, 훅 에러가 아니라 함수를 직접 넘길 수 없다는 줄이 뜹니다.


고치는 칸이 다릅니다. 클릭 함수는 클라이언트 파일 안에 두세요. 서버에서 실행해야 하는 저장이라면 서버 액션으로 만들고, 그 액션만 넘깁니다. 서버 액션 자리와 폼 검증은 서버 액션 글을 보면 됩니다. 훅 에러와 직렬화 에러를 한 처방으로 묶지 마세요.


다른 줄과 섞지 말 것 | needs useState는 파일 경계입니다. Invalid hook call은 리액트 복사본 또는 훅 규칙입니다. 함수를 넘길 수 없다는 줄은 직렬화입니다. 화면이 갈라지면 하이드레이션입니다.


참고 자료


내부 연계: 중복 리액트 훅, 하이드레이션 미스매치, 폼 상태 훅, 서버 액션, 에러와 not-found


인용한 동작과 옵션은 2026년 9월 공개 문서 기준입니다.


자주 묻는 질문

페이지에 use client를 붙이면 끝나나요?

빨간 줄은 사라집니다. 다만 그 페이지의 조회와 마크업까지 클라이언트로 내려갑니다. 클릭이 있는 칸만 새 파일로 빼면 페이지는 서버로 남습니다. 레이아웃에 붙이면 아래 페이지가 통째로 따라가니 더 넓게 번집니다.


use Client라고 대문자로 써도 되나요?

안 됩니다. 철자는 소문자 use client입니다. C만 대문자여도 지시자로 안 읽어 파일이 서버로 남고, 같은 훅 에러가 다시 납니다. 임포트 아래에 둔 경우도 지시자로 안 봅니다.


서버 페이지가 클라이언트 버튼을 임포트해도 되나요?

됩니다. 그게 권장 조합입니다. 서버가 데이터를 읽고, 클라이언트 입구를 자식으로 두면 됩니다. 반대로 클라이언트 파일이 서버 전용 모듈을 임포트하면 다른 에러가 납니다. 비밀 키와 fs는 서버 파일에 두세요.


onClick을 서버에서 만들어 넘기면 되나요?

일반 함수는 안 됩니다. 리액트가 서버에서 클라이언트로 함수를 직렬화하지 못합니다. 클릭 함수는 클라이언트 파일 안에 두고, 서버에서 실행할 저장만 서버 액션으로 넘기세요. 이 줄은 needs useState와 처방이 다릅니다.


패키지 캐러셀을 넣어도 같은 줄이 나요.

패키지 안에 지시자가 없으면 넥스트가 그걸 서버로 봅니다. 얇은 파일에 use client를 붙이고 다시 내보내면 됩니다. 공식 문서의 서드파티 래퍼 패턴입니다. 패키지 소스를 고칠 필요는 없습니다.


Invalid hook call과 같은 에러인가요?

아닙니다. 이 줄은 앱 폴더 기본값이 서버라서 훅 자리가 없는 상태입니다. Invalid hook call은 리액트 복사본이 두 개이거나 훅 규칙을 어긴 줄입니다. 패키지가 리액트를 따로 들고 있으면 중복 리액트 글을 보세요.


앱 라우터의 이 빨간 줄은 훅이 있는 파일이 아직 서버라는 뜻입니다. 지시자는 소문자로 그 파일 맨 위에 두고, 페이지 전체가 아니라 클릭 칸만 쪼개면 대부분 풀립니다. 관련 글: 중복 리액트 훅, 하이드레이션 미스매치, 폼 상태 훅.


useStateuse client서버 컴포넌트클라이언트 컴포넌트Next.jsReact앱 라우터카카오 로그인프론트엔드지시자개발자

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Frontend

이어서 읽어보기

전체 토픽 둘러보기