본문 바로가기

javascript/nextjs

[nextjs] _app · _document · _error

 nextjs에는 이름 앞에 _ 이 붙는 특별한 컴포넌트들이 존재한다. _app · _document · _error 과 같은 컴포넌트들이 여기에 속하는데, 이들은 nextjs에서 기본적으로 제공하는 기능을 개발자의 필요에 따라 일부 변경해야하는 경우에 사용되며, 각각 페이지 초기화, html 문서 구조, 에러 페이지를 커스터마이징할 때 사용된다. 

 모든 파일은 pages/ 폴더 바로 안에 위치한다.


_app

https://nextjs.org/docs/advanced-features/custom-app

 _app.js 파일은 모든 페이지 컴포넌트에 대해 적용되어야 하는 요소를 초기화할 때 사용된다. 만약 해당 파일이 존재한다면, 모든 페이지 컴포넌트에 대해 _app 파일에 정의된 여러 설정들이 적용된다. 예컨대 모든 페이지에 적용되는 요소 ( 상태 관리, 레이아웃, 공통되는 css 파일 혹은 js 파일의 로딩 등 ) 가 존재하는 경우, 이러한 요소를 설정하면 된다.

공식 문서에서는 다음과 같은 상황에서 _app을 사용할 수 있다고 제시한다.

  • 페이지를 변경할 때 레이아웃이 유지되는 경우
  • 페이지를 탐색할 때 상태(state)을 유지하는 경우
  • componentDidCatch을 통해 사용자 정의 오류를 처리하는 경우
  • 페이지에 추가 데이터를 삽입하는 경우 ( getInitialProps만 가능 )
  • 전역 CSS의 추가

 

import '../styles/globals.css' // 전역적으로 css 파일을 가져온다!
import type { AppProps } from 'next/app'
import Head from 'next/head'
import { NextPage } from 'next'

const MyApp : NextPage<AppProps> = ({ Component, pageProps }) => {
  return <>
    <Head>
      {/* bootstrap 코드 */}
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" 
            rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
            crossOrigin="anonymous" /> 
    </Head>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
            crossOrigin="anonymous" defer></script>
    <Component {...pageProps} />
  </>
}

export default MyApp

 _app의 컴포넌트는 AppProps 인터페이스를 인자로 가진다. 이때 AppProps 인터페이스는 2개의 프로퍼티를 가진다.

  • Component : 현재 활성화된 페이지를 의미한다. 
  • pageProps : 페이지 컴포넌트가 인자로 받는 프로퍼티로, data fetching(get~ 메서드)을 통한 props을 포함한다.

 

 서버측에서 렌더링을 수행할 수 있는 nextjs 특성상 context나 redux을 이용한 전역 상태 관리가 필요한 경우가 많지는 않겠지만, 필요한 경우 _app.js 파일 내부에서 Provider을 제공하여 전역적인 상태 관리를 수행할 수 있을 것이다.

 nextjs 공식 튜토리얼에서 사용하는 예시는 전역 css 파일을 지정하는 것이다. _app 파일 내에서 css 파일을 import 구문을 통해 가져오도록 설정하면 모든 페이지 컴포넌트에서 사용할 수 있다.

 bootstrap 등 CDN을 통해 사용자 측에서 로딩할 수 있는 파일들도 동일하게 설정할 수 있다. 통상적인 html 파일에서 CDN을 경유하여 파일을 로딩하는 경우 css 파일은 <link> 태그를, js 파일은 <script> 태그를 이용한다. 이러한 틀과 동일한 방식으로 파일을 로딩할 수 있다.


_document

https://nextjs.org/docs/advanced-features/custom-document

 일반적으로 지정되어 있는 html 문서의 구조나 클래스 등을 커스터마이징할 때 사용되는 파일이다. 기본적인 _document 에서의 html 문서 구조는 다음과 같다.

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

export default function Document() {
  return (
    <Html lang='kr'>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

_document 에는 <Html>, <Head>, <Main>, <NextScript> 태그가 필수적으로 포함되어야 한다. 추가하고 싶은 내용은 head 태그 혹은 body 태그 내부에 설정하면 된다. 예를 들어 _app 컴포넌트의 예시에서 CDN으로 부터 로드했던 bootstrap 을 _document 태그에서 로드하는 코드는 다음과 같다.

export default function Document() {
    return (
        <Html lang='kr'>
            <Head>
                <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
                    rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
                    crossOrigin="anonymous" />
            </Head>
            <body>
                <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
                    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
                    crossOrigin="anonymous" defer></script>
                <Main />
                <NextScript />
            </body>
        </Html>
    )
}

_document 을 사용할 때 유의해야 하는 점이 몇개 존재한다.

  1. next/head 및 next/document 의 <Head> 컴포넌트는 동일하지 않다. 전자의 경우 특정 페이지의 <head> 태그 내부의 정보를 지정할 때 사용되는데 비해, 후자는 모든 페이지를 대상으로 지정할 때 사용된다. 만약 개별적인 특성을 지니는 정보가 있다면, next/head의 <Head> 및 개별적인 컴포넌트를 이용하여 값을 설정하는게 맞다.
  2. _document 컴포넌트는 서버측에서만 사용되는 static 파일의 성격을 가지므로, 각종 컴포넌트를 사용할 수 없다. 예를 들어 _document 에서 스크립트 로딩 최적화를 목적으로 <script> 대신 next/script 의 <Script> 컴포넌트를 사용하려고 하면 다음과 같은 문제가 발생한다.  
    Script 컴포넌트를 사용하려고 하면 에러메시지가 발생한다.
    _document 내부에서는 해당 파일을 사용할 수 없다는 이야기인데, 기본적으로 next/script는 리액트 컴포넌트이며 클라이언트 측에서 사용되는데 이를 서버측에서 사용하려고 해서 오류가 발생하는 것이다

서버측의 코드만을 접근할 수 있는 문제 때문에 사실 _document을 사용해야만 하는 경우는 거의 없다. 공식 문서에서 renderPage 및 getInitialProps 메서드를 이용하여 styled-jsx을 사용하는 방법을 제시하고 있기는 하지만, 이 역시도 그냥 css module 등을 사용한다고 하면 없는 기능이다. 리액트 18을 위해서 사용 자제를 권고하고 있기도 하다.

 따라서 모든 페이지가 공유하는 <head> 태그 내 메타 데이터 등을 지정하고 싶은 경우 정도밖에 _document의 사용처가 없는데, 이마저도 Layout 컴포넌트 및 _app을 이용하여 대체할 수 있으므로 현재 시점에서 이 컴포넌트를 사용해야만 하는 이유는 그다지 없다.


_error

사용자의 실수나 서버측 문제 등으로 인해 발생하는 에러에 대한 페이지를 커스터마이징할 때 사용된다. 에러 중 특히 많이 발생하는 404(존재하지 않는 페이지 요청), 500(서버측 오류 발생) 의 경우, 전용 페이지를 지정할 수 있다.

404

pages/404.js 파일 내부에 지정한다.

import { useRouter } from "next/router";
import { useEffect, useState } from "react";

export default function Custom404() {
    const router = useRouter();
    const [num, setNum] = useState(5);

    useEffect(() => {
        if (num <= 0) {
            router.replace('/');
        }
        const t = setTimeout(() => {
            setNum(prev => prev - 1);
        }, 1000);

        return () => {
            clearTimeout(t);
        }
    }, [num, router]);

    return (<>
        <h1>404 Error!</h1>
        <p>remain time : {num}</p>
    </>);
}
// 5초 뒤에 메인 페이지로 이동하는 로직

 

500

pages/500.js 파일 내부에 지정한다.

export default function Custom500() {
  return <h1>500 - Server-side error occurred</h1>
}

 

 위 2가지 에러 상황 이외에 대응되는 에러 메시지를 보여주고 싶을 수도 있다. 이 경우 _error.js 파일을 이용한다. 기본적으로 404 · 500 페이지와 _error 페이지가 함께 있는 경우, 404 · 500 페이지가 렌더링된다. 

 에러 페이지는 다음과 같이 생성할 수 있다.

import { NextPage } from "next";
import {ErrorProps} from 'next/error';

interface AppErrorProps extends ErrorProps {
    statusCode : number;
} // 에러 코드를 받기 위한 목적의 인터페이스.

const Error: NextPage<AppErrorProps> = ({statusCode} : AppErrorProps) => {
    return (
        <p>
            {statusCode 
                ? `Error Code : ${statusCode}`
                : `An Error occured on client`
            }
        </p>
    )
}

Error.getInitialProps = ({res, err}) => {
    const statusCode = res ? res.statusCode : err ? err.statusCode! : 404;
    return {
        statusCode
    }
} // 클라이언트의 요청에 따른 에러를 반환. 

export default Error;

 

  1. getInitialProps 을 통해 res 및 err을 받아온 후, 로직을 통해 정보가 포함된 객체 반환.
  2. 반환된 객체를 받아 페이지를 렌더링.

 404 페이지 없이 _error 를 만들면 에러 코드 404 대한 에러 페이지 정적 최적화가 수행되지 않으므로, 가능하다면 _error을 만들기 이전에 404 및 500 페이지를 먼저 만드는 것이 추천된다.


결론 및 요약

  • _app.js 파일은 레이아웃, 상태 관리, 전역 css 등 페이지 전반적으로 사용될 요소를 지정할 때 사용된다.
  • _document.js 파일은 페이지 전반적으로 이용될 html 요소를 지정할 때 사용되는데, 서버측에서만 사용되는 정적 요소라 리액트 컴포넌트를 사용할 수 없으므로, 주로 <head> 태그 내부 내용을 설정하거나 <script> 태그를 이용하여 외부 코드를 로드하는 등의 제한적인 상황에서 사용될 수 있다.
  • _error.js는 에러 페이지를 커스터마이징할 때 사용되는데, 에러코드 404 및 500 에 대해서는 페이지를 개별적으로 생성할 수 있으며, _error 이전에 해당 페이지들을 먼저 생성하는 것이 권장된다.