Skip to main content

Command Palette

Search for a command to run...

[Next.js] server 컴포넌트와 client 컴포넌트로 분리하기

Updated
3 min readView as Markdown

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는 서버 컴포넌트를 사용한다. 이것은 서버 렌더링을 자동으로 구현할 수 있고, 필요한 경우에는 클라이언트 컴포넌트로 선택해 사용할 수 있다.

서버 컴포넌트를 사용하면서 얻을 수 있는 이점이 몇 가지 있다.

  1. 데이터 패칭할 때 걸리는 시간과 클라이언트가 요청해야 하는 횟수를 줄여 성능을 향상시킬 수 있다.

  2. 토큰과 API 키 같은 민감한 데이터와 로직을 클라이언트에 노출하지 않아 보안에 용이하다.

  3. 렌더링 결과를 캐시하여 후속 요청과 재사용이 가능하다.

  4. 클라이언트가 페이지를 렌더링하는 데 필요한 자바스크립트를 실행할 때까지 기다릴 필요 없이 사용자가 즉시 페이지를 볼 수 있도록 HTML을 생성한다.

  5. 렌더링된 HTML은 검색 엔진 봇이 페이지 색인을 생성하는 데 사용할 수 있어 검색 엔진 최적화에 도움을 준다.

  6. 렌더링 작업을 정크로 분할해 준비되는 대로 클라이언트로 스트리밍할 수 있어, 사용자는 서버에서 전체 페이지가 렌더링될 때까지 기다릴 필요 없이 페이지의 일부를 먼저 볼 수 있다.

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 함수에서 리액트훅을 사용하지 않는 서버 컴포넌트로 거듭하며 에러를 제거하게 되었다.

More from this blog

시맨틱하게 다이얼로그 만들기

웹에서 양식을 제출하기 위한 다이얼로그를 작성할 때 시맨틱 태그를 무시하고 div 태그를 활용해 css로 스타일링을 하는 경우가 많다. 이렇게 작성할 경우, 동작에는 전혀 무리가 없지만 하지 않아도 될 스타일링 작업을 하는 데 시간을 쏟을 수 있고 웹접근성 측면에도 좋지 않다. 웹에서 다이얼로그는 모달, 대화상자, 알림창, 검사기와 같은 상호작용 가능한 컴포넌트를 말한다. HTML에서는 dialog 라는 태그를 사용하면 핵심적인 다이얼로그의 기...

Jan 13, 20262 min read

[JavaScript] 이미지 사이즈 압축해서 용량 줄이기

이미지를 받아서 저장하는 기능을 구현하고 며칠이 지난 후 한 가지 이슈를 발견하였다. 바로 이미지 업로드가 되지 않는 아주 치명적인 문제였다! 분명 테스트도 수 없이 진행하고 배포한 후에도 잘만 동작했는데 갑자기 이런 일이 왜 벌어진걸까? 문제 해결의 원인을 찾기 위해 다양한 방법을 시도하였다: 갤럭시의 갤러리에 있는 이미지 업로드 갤럭시의 카메라로 직접 찍어서 업로드 아이폰의 갤러리에 있는 이미지 업로드 아이폰의 카메라로 직접 찍어서 ...

Nov 16, 20242 min read
[JavaScript] 이미지 사이즈 압축해서 용량 줄이기

[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기

웹 개발을 하면서 한 번쯤 겪어볼 수 있는 이슈가 있다. A 사용자가 이미지를 업로드하면 B 사용자가 그 이미지를 확인하고, 재업로드를 요청하는 플로우다. A 사용자가 새로운 이미지를 업로드 했는데도 B 사용자는 여전히 처음 이미지를 볼 수 있다. 이 문제는 서버 측에서 "이미지 경로는 그대로 유지하고 이미지를 덮어씌운다"는 구조와 관련이 있다. 브라우저는 URL이 동일하면 캐싱된 이미지를 가져와 네트워크 요청을 줄인다. 따라서 새로운 이미지가...

Oct 3, 20242 min read
[JavaScript] 이미지 수정 시 브라우저 캐싱 방지하기

[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)

평소 한땀한땀 기능을 만들어나가는 걸 좋아해서 유명한 라이브러리들을 사용해보지 않았는데, 회사에서 프로젝트를 진행하며 다양한 라이브러리를 사용해보았다. 그 중 React-Hook-Form을 사용하며 평소에 리액트로 폼을 작성하며 겪은 불편한 점을 많이 해소했다. 코드 길이, 예외 처리, 렌더링 등 다양한 관점에서 개선할 수 있게 되었다. 오늘은 다양한 폼 상태 관리 방법을 비교하여 React-Hook-Form의 장점을 알아보자. useState...

Sep 5, 20243 min read
[React] 렌더링 관점에서 보는 react-hook-form (feat. useRef)

Untitled Publication

53 posts