[Next.js] server 컴포넌트와 client 컴포넌트로 분리하기
Next.js로 프로젝트를 진행하면서 리액트에서는 볼 수 없었던 에러를 발견하였다.
// app/login/page.tsx
'use client'
import { useTranslation } from 'react-i18next'
import { Button, PasswordTextField, TextField } from '@yeonsubaek/yeonsui'
import UserForm from '@/components/molecule/UserForm'
import { useRouter } from 'next/navigation'
import PageTitle from '@/components/atom/PageTitle'
import { ChangeEvent, useState } from 'react'
import { useSignIn } from '@/hooks/useUser'
import { useUserStore } from '@/stores/user'
function page() {
const router = useRouter()
const { setIsLoggedIn } = useUserStore()
const { t } = useTranslation('')
const { mutate } = useSignIn()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
mutate(
{
email,
password,
},
{
onSuccess: (res) => {
alert('로그인에 성공하였습니다.')
localStorage.setItem('userToken', res.user.email)
setIsLoggedIn(true)
router.replace('/')
},
onError: (error) => {
console.error(error)
},
}
)
}
return (
<div className="login">
<PageTitle>{t('user.login')}</PageTitle>
<UserForm buttonName={t('user.button.login')} onSubmit={onSubmit}>
<TextField
placeholder={t('user.form.email')}
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
/>
<PasswordTextField
placeholder={t('user.form.password')}
value={password}
onChange={(e: ChangeEvent<HTMLInputElement>) => setPassword(e.target.value)}
/>
</UserForm>
<div className="login-etc-buttons">
<Button variant="link">{t('user.button.findPassword')}</Button>
<Button variant="link" onClick={() => router.push('/join')}>
{t('user.button.joinUs')}
</Button>
</div>
</div>
)
}
export default page
위 코드는 login/page.tsx 에서 로그인 구현을 위한 기능이 모두 들어있는 페이지이다. VScode를 통해 코드를 살펴보면 아주 많은 빨간줄을 볼 수 있다.

React Hook "useUserStore" is called in function "page" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".
위 경고문을 요약하자면 "page" 함수 내에서는 리액트 훅을 사용할 수 없다는 것이다.
왜 사용하면 안되는지, 어떻게 해결하면 좋을 지 알아보자.
우선, 기본적으로 Next.js는 서버 컴포넌트를 사용한다. 이것은 서버 렌더링을 자동으로 구현할 수 있고, 필요한 경우에는 클라이언트 컴포넌트로 선택해 사용할 수 있다.
서버 컴포넌트를 사용하면서 얻을 수 있는 이점이 몇 가지 있다.
데이터 패칭할 때 걸리는 시간과 클라이언트가 요청해야 하는 횟수를 줄여 성능을 향상시킬 수 있다.
토큰과 API 키 같은 민감한 데이터와 로직을 클라이언트에 노출하지 않아 보안에 용이하다.
렌더링 결과를 캐시하여 후속 요청과 재사용이 가능하다.
클라이언트가 페이지를 렌더링하는 데 필요한 자바스크립트를 실행할 때까지 기다릴 필요 없이 사용자가 즉시 페이지를 볼 수 있도록 HTML을 생성한다.
렌더링된 HTML은 검색 엔진 봇이 페이지 색인을 생성하는 데 사용할 수 있어 검색 엔진 최적화에 도움을 준다.
렌더링 작업을 정크로 분할해 준비되는 대로 클라이언트로 스트리밍할 수 있어, 사용자는 서버에서 전체 페이지가 렌더링될 때까지 기다릴 필요 없이 페이지의 일부를 먼저 볼 수 있다.
Next.js 공식 문서에서 언제 서버/클라이언트 컴포넌트를 사용해야 하는 지 친절하게 정리해주었다.

뭐야, 그럼 page 함수에도 'use client' 붙여서 클라이언트 컴포넌트로 사용하면 되는 거 아니야? 라고 생각할 수 있지만, page 함수는 예외다.
Next.js는 page를 준비하고 사전 렌더링을 하는 방식을 갖고 있다. page 내에 있는 HTML을 먼저 준비하고 클라이언트 컴포넌트의 요소는 나중에 렌더링하는 것이다. 만약 page 함수가 클라이언트 컴포넌트라면 Next.js의 렌더링 방식을 무시해버리고, 그것의 이점을 가져올 수 없는 것이다.
그렇다면 page 함수 내에서 리액트훅이 사용되지 않도록 상세 컴포넌트로 리팩터링 할 필요가 있다.
import PageTitle from '@/components/atom/PageTitle'
import LoginForm from '@/components/organism/LoginForm'
import LoginButtons from '@/components/organism/LoginButtons'
function page() {
return (
<div className="login">
<PageTitle route="user.login" />
<LoginForm />
<LoginButtons />
</div>
)
}
export default page
이제 page 함수에서 리액트훅을 사용하지 않는 서버 컴포넌트로 거듭하며 에러를 제거하게 되었다.
![[JavaScript] 이미지 사이즈 압축해서 용량 줄이기](https://cdn.hashnode.com/res/hashnode/image/upload/v1731718582763/ed9e5687-ee04-44d6-ae39-8e5a809bdf73.png)
![[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기](https://cdn.hashnode.com/res/hashnode/image/upload/v1727921664973/6e3f32b0-2df4-44f9-bc12-69589dffbcb7.png)
![[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)](https://cdn.hashnode.com/res/hashnode/image/upload/v1725535323097/421ac5cd-a7af-401a-80f3-24b15fcd6087.png)