All Articles

Next.js의 Head 컴포넌트에 대해

Next.js로 사이드 프로젝트를 시작하면서 공식문서에서 시키는대로 create-next-app을 사용해서 프로젝트를 init했다. 13으로 버전업 되면서 차이가 생긴건지 다양한 옵션들이 생겼다.

create-next-app-init

eslint를 사용할지 말지, src directory를 추가할지 말지, app 디렉토리를 추가할지 말지 고를 수 있다. experimental이라길래 app은 제끼고, 나머지를 받아들여보았다.

자동으로 생성된 디렉토리를 확인하면서 파일을 보는데 index.tsx_document.tsx 모두 Head라는 컴포넌트가 있었다.

// index.tsx

import Head from "next/head";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      ...
    </>
  );
}
// _document.tsx

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      ...
    </Html>
  );
}

왜 굳이 import 경로가 다른 Head가 두개나 필요할까?

최근에 추천사를 작성하게 되면서 실전에서 바로쓰는 Next.js라는 책을 받았는데. 이 책에는

  1. next/head에 있는 Head는 오픈그래프 속성, 메타데이터를 정의할 때 사용하고,
  2. next/document에 있는 Head는 웹사이트의 모든 페이지에 공통으로 사용되는 코드가 있을 때 사용할 수 있고, 메타데이터를 정의하는 태그와 다르다고 설명되어있다.

1번은 알겠는데 2번은 잘 모르겠으니, 소스코드를 열어보려고 한다.

next/headHeadsrc/shared/lib/head.tsx에서 온다. 주석에는 <head>에 element를 inject하기 위해 사용된다고 설명되어있다.

// next/head

function Head({ children }: { children: React.ReactNode }) {
  const ampState = useContext(AmpStateContext);
  const headManager = useContext(HeadManagerContext);
  return (
    <Effect
      reduceComponentsToState={reduceComponents}
      headManager={headManager}
      inAmpMode={isInAmpMode(ampState)}
    >
      {children}
    </Effect>
  );
}

ReactNode들을 children으로 받아오고 Effect라는 태그로 감싸준다. Effect 컴포넌트는 useLayoutEffect hook을 사용해서 headManagerupdateHead라는 method를 실행시킨다. 이를 통해index.tsx에 있던 아래 태그들을 브라우저가 화면을 업데이트 시킬 때마다 defaultHead로 선언된 아래 두개의 meta tag와

export function defaultHead(inAmpMode = false): JSX.Element[] {
  const head = [<meta charSet="utf-8" />];
  if (!inAmpMode) {
    head.push(<meta name="viewport" content="width=device-width" />);
  }
  return head;
}

next/head에서 import한 Head의 children tag들을 <head>안에 children으로 넣어준다.

<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />

next/documentHeadsrc/pages/_document.tsx에서 온다. 주석을 보니 next/head와 가장 큰 차이는 next/document는 서버컴포넌트라는 것이다. 따라서 next/head처럼 useLayoutEffect등의 방식으로 헤더를 업데이트 할 수 없다. 따라서 클라이언트 사이드와 관계없는 tag들만 넣어주어야 한다.

따라서 화면에 해당하는 값을 보여줘야하는 <title>과 같은 값을 next/documentHead에 넣어주면 경고가 발생한다.

no-title-in-document

스크린샷에 있는 사이트에서 확인하면 아래 에러메세지를 확인할 수 있다.

Adding <title> in pages/_document.js will lead to unexpected results with next/head since _document.js is only rendered on the initial pre-render.

같인 이유로 <title>외에 화면의 크기와 관련있는 viewport option을 넣어도 경고메세지가 발생한다.

next/documentHead의 소스코드는, create-next-app으로 프로젝트를 처음 생성할 때 index.tsx에 적용할 style속성과 script태그들을 가지고있다. <title> 관련 경고메세지에서 본 것처럼 해당 Head는 최초 pre-render에 적용되기 때문에, SEO에 사용될만한 <meta> tag들을 열심히 심어두면 되겠다.

또 하나의 차이는 next/documentHead_document.tsx외 다른 파일에 넣으면 에러가 발생한다. 이것도 주의해야한다.

앞으로 계속 세팅을 하면서 궁금한 부분이나, 새롭거나 신기한 것을 발견하면 지속적으로 포스팅 해보도록 하겠다.

Published Feb 19, 2023

AI Enthusiast and a Software Engineer