nextjs에는 이름 앞에 _ 이 붙는 특별한 컴포넌트들이 존재한다. _app · _document · _error 과 같은 컴포넌트들이 여기에 속하는데, 이들은 nextjs에서 기본적으로 제공하는 기능을 개발자의 필요에 따라 일부 변경해야하는 경우에 사용되며, 각각 페이지 초기화, html 문서 구조, 에러 페이지를 커스터마이징할 때 사용된다.
_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 에는 <Html>, <Head>, <Main>, <NextScript> 태그가 필수적으로 포함되어야 한다. 추가하고 싶은 내용은 head 태그 혹은 body 태그 내부에 설정하면 된다. 예를 들어 _app 컴포넌트의 예시에서 CDN으로 부터 로드했던 bootstrap 을 _document 태그에서 로드하는 코드는 다음과 같다.
next/head 및 next/document 의 <Head> 컴포넌트는 동일하지 않다. 전자의 경우 특정 페이지의 <head> 태그 내부의 정보를 지정할 때 사용되는데 비해, 후자는 모든 페이지를 대상으로 지정할 때 사용된다. 만약 개별적인 특성을 지니는 정보가 있다면, next/head의 <Head> 및 개별적인 컴포넌트를 이용하여 값을 설정하는게 맞다.
_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초 뒤에 메인 페이지로 이동하는 로직
위 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;
getInitialProps 을 통해 res 및 err을 받아온 후, 로직을 통해 정보가 포함된 객체 반환.
반환된 객체를 받아 페이지를 렌더링.
404 페이지 없이 _error 를 만들면 에러 코드 404 대한 에러 페이지 정적 최적화가 수행되지 않으므로, 가능하다면 _error을 만들기 이전에 404 및 500 페이지를 먼저 만드는 것이 추천된다.
결론 및 요약
_app.js 파일은 레이아웃, 상태 관리, 전역 css 등 페이지 전반적으로 사용될 요소를 지정할 때 사용된다.
_document.js 파일은 페이지 전반적으로 이용될 html 요소를 지정할 때 사용되는데, 서버측에서만 사용되는 정적 요소라 리액트 컴포넌트를 사용할 수 없으므로, 주로 <head> 태그 내부 내용을 설정하거나 <script> 태그를 이용하여 외부 코드를 로드하는 등의 제한적인 상황에서 사용될 수 있다.
_error.js는 에러 페이지를 커스터마이징할 때 사용되는데, 에러코드 404 및 500 에 대해서는 페이지를 개별적으로 생성할 수 있으며, _error 이전에 해당 페이지들을 먼저 생성하는 것이 권장된다.