본문 바로가기

[번역] The Next.js 핸드북

Flavio CopesThe Next.js Handbook을 번역한 글입니다.

 

The Next.js Handbook

I wrote this tutorial to help you quickly learn Next.js and get familiar with how it works. It's ideal for you if you have zero to little knowledge of Next.js, you have used React in the past, and you are looking forward diving more into the React ecosyste

www.freecodecamp.org


Next.js를 여러분들이 빨리 배우고, 어떻게 작동하는지 익숙해지는데 도움을 주기 위해 이 튜토리얼을 작성합니다.

 

이 글은 다음과 같은 상황의 여러분들에게 도움이 될 것입니다.

 

1. Next.js를 아예 모르거나 조금 아시는 분

2. 이전에 React를 써본 적이 있으신 분

3. React 생태계에 더 많이 알기 위해 뛰어드시는 분 (특히 서버 사이드 랜더링)

 

저는 Next.js가 웹 어플리케이션을 만드는 데 훌륭한 도구라는 걸 알게 되었으며, 이 글을 모두 읽은 여러분들 또한 제가 그랬던 것처럼 생각하기를 바랍니다. 또한 이 글이 여러분들이 Next.js를 학습하는데 도움이 될 것입니다!

 

노트: 여러분은 PDR / ePub / Mobi 버전을 다운로드받아 오프라인에서도 이 튜토리얼을 읽을 수 있습니다!

목차

  1. 소개
  2. Nextjs가 제공하는 주요 기능
  3. Next.js vs Gatsby vs create-react-app
  4. Next.js 설치하기
  5. SSR 동작을 소스코드에서 확인하기
  6. The app bundles
  7. 우측 하단의 아이콘의 정체는?
  8. React DevTools 소개 및 설치
  9. 사용가능한 또다른 디버깅도구
  10. 두번째 페이지 추가하기
  11. 두 페이지를 연결하기
  12. 라우터로 동적 콘텐츠 로드하기
  13. Prefetching
  14. router를 사용하여 선택된 링크 감지하기
  15. next/router 사용법
  16. getInitialProps()를 통해 컴포넌트에 데이터 보내기
  17. CSS
  18. custom 태그로 head 태그 옮기기
  19. wrapper 컴포넌트 추가하기
  20. API 라우팅
  21. 코드를 서버사이드 혹은 클라이언트 사이드에서 실행하기
  22. 운영 버전으로 배포하기
  23. now로 배포하기
  24. 앱 번들 파일 분석
  25. 지연 로딩 모듈 소개
  26. 더 공부해야 할 것들

 

 

1. 소개


리액트에 의해 움직이는 모던 자바스크립트 어플리케이션 개발은 기막히게 좋지만 여러분은 모든 컨텐츠 랜더링이 클라이언트 사이드에서만 일어난다는 문제를 깨닫게 됩니다.

 

우선 컨텐츠가 로드되기 전, 모든 자바스크립트를 로드하고 당신의 어플리케이션은 화면에 무엇을 보여줘야 할지 결정해야하기 때문에 화면이 사용자에게 보이기까지 엄청난 시간이 듭니다. 

 

두 번째로, 만약 당신이 공개으로 사용가능한 웹사이트를 개발한다면, 컨텐츠 SEO 문제를 겪게 될 것입니다. 검색 엔진은 자바스크립트 어플리케이션들을 실행하고 인덱싱하는 데에 성능이 좋지만, 검색엔진이 콘텐츠를 처리하게 하는것 대신에 우리가 직접 콘텐츠를 보낼 수 있다면 이보다 훨씬 더 뛰어날 것입니다.

 

이 두 문제를 해결하는 해결책은 서버 랜더링, 즉 static pre-rendering이라고 부르는 서버 랜더링(server rendering)입니다.

 

Next.js는 이 모든 것(server rendering)을 아주 쉽게 하는 하나의 리액트 프레임워크입니다. 하지만 서버 랜더링에 국한되지 않고, Next.js 개발자가 직접 리액트의 zero-configuration, single-command toolchain 기법으로도 홍보하고 있습니다.

 

Next.js는 당신이 더 쉽게 프론트엔드 리액트 어플리케이션을 만들 수 있도록 공통 구조를 제공하고, 서버 사이드 랜더링을 명백하게 처리합니다.

 

2. Next.js가 제공하는 주요 기능


다음은 Nextjs의 주요 기능입니다.

 

Hot Code Reloading

디스크에 어떤 변화가 감지 될 때 Next.js는 페이지를 재로드합니다.

 

Automatic Routing

pages 폴더에 있는 파일에 대해 어떤 URL이라도 파일 시스템에 매핑됩니다. 그리고 당신은 어떠한 환경 설정도 하지 않아도 됩니다 (물론 당신은 설정 옵션을 커스터마이징할 수 있습니다.) 

Single File Components

같은 팀에서 만들고 통합된 styeld-jsx를 사용하면, 해당 컴포넌트 영역 (스코프)에 스타일을 손쉽게 추가할 수 있습니다.

 

Server Rendering

당신은 클라이언트로 HTML을 보내기 전에 서버사이드에서 리액트 컴포넌트를 랜더링할 수 있습니다.

 

Ecosystem Compatibility

Next.js는 자바스크립트, Node, 리액트 생태계와 함께 잘 작동합니다.

 

Automatic Code Splitting

페이지들은 자신이 필요로 하는 자바스크립트와 라이브러리들만 랜더링 됩니다. 모든 app 코드를 포함한 하나의 싱글 자바스크립트 파일을 생성하는 대신, app은 Next.js에 의해 자동으로 몇몇의 다른 리소스들로 해체됩니다.

 

페이지를 로딩하면 오로지 해당 페이지에 필요한 자바스크립트를 로드합니다.

 

Next.js는 임포트된 리소스를 분석하여 이를 수행합니다.

 

예를 들어 하나의 화면만 Axios 라이브러리를 임포트하고 있다면, 그 페이지만 번들에서 Axis 라이브러리를 포함하고 있을 것입니다.

 

이는 첫 번째 페이지가 가능한 한 빠르게 로드될 수 있도록 보장하는 동시에 미래에 로드될 페이지가 클라이언트가 필요로 하는 자바스크립트를 보내줄 것입니다.

 

주목할만한 예외가 하나 있습니다. 만약 전체 사이트 화면의 최소한 절반에서 쓰는, 즉 주기적으로 쓰이는 임포트들은 메인 자바스크립트 번들로 이동됩니다.

 

Prefetching

두 개의 다른 페이지를 연결하는 Link 컴포넌트는 백그라운드에서 자동으로 페이지 리소스(코드 스플리팅에 의해 분실된 코드를 포함)를 사전 패치하는 prefetch 프로퍼티를 지원합니다.

 

Dynamic Components

당신은 자바스크립트 모듈과 리액트 컴포넌트를 동적으로 임포트할 수 있습니다.

 

Static Exports

Next.js는 next export 명령어를 통해 당신이 모든 정적 사이트를 당신의 앱에 export할 수 있게 해줍니다.

 

TypeScript Support

Next.js는 TypeScript로 작성되었고, 따라서 TypeScript를 지원합니다.

 

3. Next.js vs Gatsby vs create-react-app


Next.js, Gatsby, create-react-app은 어플리케이션을 시작하는데 사용가능한 멋진 도구들입니다.

 

이것들이 공통적으로 지닌 특성에 대해 먼저 짚어봅시다.

이들 모두 속을 들여다보면 모든 개발 경험을 작동하는  React 기반입니다.

또 다른 특성으로는 webpack과 이전에 우리가 지침대로 설정하곤 했던 webpack의 low level 모듈들을 추상화하고 있습니다.

 

create-react-app으로는 쉽게 서버 사이드 랜더링을 생성하는데 어렵습니다. Next.js 나 Gatsby만이 SEO, 속도 등과 관련한 것들을 제공하고 있습니다.

 

Next.js가 Gatsby보다 언제 더 나은가?

둘 모두 서버 사이드 랜더링을 지원하지만 방식은 다릅니다.

 

Gatsby를 활용한 최종 결과물은 서버가 없는 정적 사이트 생성기입니다. 여러분이 사이트를 빌드하면, Netlify와 같은 또다른 정적 호스팅 사이트 위에서 정적으로 빌드된 프로세스의 결과물을 배포하게 됩니다.

 

Next.js는 요청에 대한 응답을 서버 단에서 랜더할 수 있는 백엔드를 제공합니다. 따라서 여러분은 동적인 웹사이트를 만들 수 있으며, 이는 곧 Node.js를 실행할 수 있는 플랫폼 위에서 여러분의 사이트를 배포할 수 있다는 의미입니다.

 

Next.js또한 정적 사이트를 생성할 수 있지만, 저는 이것이 Next.js의 주요 활용 케이스라고는 말하지 않겠습니다.

 

만약 저의 목표가 정적 사이트를 빌드하는 것이었다면, 저는 Next.js와 Gatsby 중에 선택하는 데 고민이 많았을 것이고, 아마 Gatsby가 특히 많은 블로그 포스팅과 함께, 더 좋은 플러그인 생태계를 갖고 있을 것입니다.

 

Gatsby는 또한 여러분의 주관과 필요에 따라 선호할수도, 비선호할수도 있는 GraphQL을 바탕으로 하고 있다. 

 

4. Next.js 설치하기


Next.js를 설치하기 전에, 여러분은 Node.js 설치가 필요합니다.

 

Node.js 최신 버전을 설치하십시오. node -v 명령어를 터미널에서 실행하여 버전을 확인하여 https://nodejs.org/에 리스트업 된 최신 LTS 버전과 비교하세요.

 

Node.js 설치 후, 여러분은 npm 명령어를 사용할 수 있습니다.

 

이 단계까지 어떤 문제가 있으신 경우, 이전에 써둔 아래의 튜토리얼을 권합니다.

이제 여러분의 컴퓨터에 최신 버전의 Nodedhk npm 설치가 완료되었습니다!

Now that you have Node, updated to the latest version, and npm, we're set!

 

우리는 create-next-app, 그리고 지침대로 Next app을 설치하고 설정하는 기본적인 접근의 두 가지 경로로 Next.js를 설치할 수 있습니다.

 

Using create-next-app

create-react-app에 익숙한 독자라면, create-next-app은 이와 동일합니다.

create-next-app은 이름 그대로 React app 대신 Next app을 생성하는 차이밖에 없습니다.

 

여러분이 번들된 npx command와 관련한 Node.js 5.2 이상을 설치했을거라 가정합니다.

npx command는 우리가 JavaScript 명령어를 다우받아 실행하게 해주는 유용한 툴입니다.

npx create-next-app

위의 명령어는 어플리케이션 이름을 입력받아 폴더를 생성하여, react, react-dom, next를 포함한 필요한 모든 패키지들을 다운받는 동시에 package.json을 설정합니다.

 

그러면 여러분은 즉시 sample pp을 npm run dev로 실행할 수 있습니다.

 

다음은  http://localhost:3000의 결과입니다.

create-next-app은 Next.js application을 시작하는 데 권장하는 방법입니다. 여러분들에게 구조와 실행 가능한 샘플 코드들을 제공하기 때문입니다. 단지 기본 샘플 어플리케이션이라는 이유말고도 여러분은 --example 옵션을 사용하여https://github.com/zeit/next.js/tree/canary/examples에 저장된 몇몇 예제들을 사용할 수 있습니다.

npx create-next-app --example blog-starter

Manually create a Next.js app

마치 scratch에서 Next app을 만드는 것 같이 느껴지신다면 create-next-app을 사용하지 않을 수도 있습니다.

아래에서 그 방법을 설명하겠습니다.

 

원하는 경로에 빈 폴더를 생성하세요. 예를 들어 홈 경로에 다음의 명령어를 입력합니다.

mkdir nextjs
cd nextjs

그리고나서 여러분의 첫 Next 프로젝트 디렉토리를 생성하세요.

mkdir firstproject
cd firstproject

이제 npm 커맨드로 Node 프로젝트를 초기화(initialize)합니다.

npm init -y

-y는 기본 설정을 사용하여 샘플 package.json을 생성할 수 있는 npm 옵션입니다.

이제 Next와 React를 설치합니다.

npm install next react react-dom

프로젝트 폴더에는 아래의 두 파일과 node_modules/ 디렉토리가 존재해야 합니다.

선호하는 에디터로 프로젝트 폴더를 열어보세요. 제가 선호하는 에디터는 VS Code입니다.

에디터를 설치하셨다면, 여러분은 code . 명령어를 통해 터미널에서 현재 폴더를 에디터로 실행할 수 있습니다. (만약 명령어가 실행이 되지 않는다면 여기를 참고하세요.)

 

package.json을 열어보면 다음과 같이 쓰여있습니다.

{
  "name": "firstproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies":  {
    "next": "^9.1.2",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  }
}

이 중 script 영역을 다음과 같이 바꾸세요.

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

곧 사용할 Next.js 빌드 커맨드 next를 추가했습니다.

팁: "dev: next -p 3001"과 같이 사용하시면 예시처럼 포트를 3001로 변경하여 실행할 수 있습니다.

 

이제 pages 폴더를 생성하고, index.js 를 추가하세요.

여기서 첫번째 React 컴포넌트를 생성해봅시다. 우리는 index.js를 기본 export로 사용할 것입니다.

const Index = () => (
  <div>
    <h1>Home page</h1>
  </div>
)

export default Index

이제 터미널에서 npm run dev를 실행하여 Next 개발 서버를 시작하세요. 로컬호스트에서 포트 3000번으로 app이 빌드될것입니다.

http://localhost:3000를 열어보시면 다음과 같은 화면을 브라우저 상에서 볼 수 있습니다.

5. SSR 동작을 소스 코드에서 확인하기


이제 여러분의 어플리케이션이 우리가 예상한대로 작동하고 있는지 확인해봅시다. Next.js app이기 때문에 서버 사이드 랜더링되어야 합니다.

 

SSR은 Next.js의 주요 장점 중 하나입니다. 만약 여러분이 Next.js로 사이트를 만드셨다면 해당 사이트 내 페이지들은 서버 상에서 랜더링되어야 합니다. 즉 브라우저로 HTML 파일이 전달되어야 합니다.

 

SSR은 3가지 주요 이점이 있습니다.

  • 클라이언트에서 랜더링을 위해 React를 인스턴스화(instantiate)할 필요가 없기 때문에 사용자에게 사이트 속도가 빠르게 체감됩니다.
  • 검색 엔진은 client-side JavaScript를 실행할 필요 없이 페이지를 인덱싱할 것입니다. 구글이 실행하는 어떤 것은 공개적으로 더 느린 프로세스라는 것이 입증됩니다. 그리고 여러분은 ranking이 잘 되기를 원할 때 되도록이면 구글의 속도 개선을 추가로 작업해야 할 것입니다.
  • 여러분은 페이스북, 트위터에서 공유되는 이미지, 제목, 설명을 페이지의 원하는 부분에 추가할 때 유용한 소셜 미디어 메타 태그를 사용할 수 있습니다.

app의 소스를 봅시다. Chrome 상에서 마우스 오른쪽 클릭 후 View Page Source를 클릭하세요.

소스에서 여러분은 HTML body 안에서 app bundle들인 JavaScript 파일 뭉치로 감싸여진 <div><h1>Home page</h1></div> snippet을 볼 수 있습니다.

 

우리는 아무것도 설정할 필요가 없고, SSR이 이미 동작하고 있음을 확인했습니다.

 

React app은 클라이언트에서 실행될것이고, 클라이언트 사이드 랜더링을 사용하여 링크를 클릭하는 것과 같은 사용자 인터렉션을 실행할 것입니다. 하지만 페이지를 reload하면, 서버에서 re-load될 것입니다. Next.js를 사용하여 서버에서 렌더링된 페이지는 클라이언트에서 렌더된 것처럼 보이기 때문에 브라우저 상에서의 결과에서는 아무 차이도 없습니다.

 

6. The app bundles


페이지 소스를 보면 로드된 JavaScript 파일 뭉치를 볼 수 있습니다.

 

HTML formatter로 해당 코드를 입력해서 우리 같은 사람들이 더 이해하기 쉬운 형태로 만들어봅시다!

<!DOCTYPE html>
<html>

<head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1" />
    <meta name="next-head-count" content="2" />
    <link rel="preload" href="/_next/static/development/pages/index.js?ts=1572863116051" as="script" />
    <link rel="preload" href="/_next/static/development/pages/_app.js?ts=1572863116051" as="script" />
    <link rel="preload" href="/_next/static/runtime/webpack.js?ts=1572863116051" as="script" />
    <link rel="preload" href="/_next/static/runtime/main.js?ts=1572863116051" as="script" />
</head>

<body>
    <div id="__next">
        <div>
            <h1>Home page</h1></div>
    </div>
    <script src="/_next/static/development/dll/dll_01ec57fc9b90d43b98a8.js?ts=1572863116051"></script>
    <script id="__NEXT_DATA__" type="application/json">{"dataManager":"[]","props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","nextExport":true,"autoExport":true}</script>
    <script async="" data-next-page="/" src="/_next/static/development/pages/index.js?ts=1572863116051"></script>
    <script async="" data-next-page="/_app" src="/_next/static/development/pages/_app.js?ts=1572863116051"></script>
    <script src="/_next/static/runtime/webpack.js?ts=1572863116051" async=""></script>
    <script src="/_next/static/runtime/main.js?ts=1572863116051" async=""></script>
</body>

</html>

head 태그에 rel="preload" as="script" 태그로 미리 로드되도록(preloaded) 선언된 4개의 Javascript 파일이 있습니다.

  • /_next/static/development/pages/index.js (96 LOC)
  • /_next/static/development/pages/_app.js (5900 LOC)
  • /_next/static/runtime/webpack.js (939 LOC)
  • /_next/static/runtime/main.js (12k LOC)

이는 브라우저에게 정상적인 렌더링 흐름이 시작되기 전에 가능한한 빨리 이 파일들을 로드하라고 지시합니다.

 

그것들 없이는 script들은 추가 지연 이후에 로드될 것이며 페이지 로딩 성능에 영향을 미치게 됩니다.

 

그러고 나서 이 4개의 파일들은 body 끝에 /_next/static/development/dll/dll_01ec57fc9b90d43b98a8.js (31k LOC)와  페이지의 몇몇 기본 정보를 담은 JSON 데이터와 함께 로드 됩니다.

<script id="__NEXT_DATA__" type="application/json">
{
  "dataManager": "[]",
  "props": {
    "pageProps":  {}
  },
  "page": "/",
  "query": {},
  "buildId": "development",
  "nextExport": true,
  "autoExport": true
}
</script>

 

로드된 4개의 번들 파일들은 이전에 코드 스플리팅(code splitting)이라고 불리는 한 가지 특징을 수행합니다. 

 

index.js 파일은 / 경로의 index 컴포넌트에서 필요한 코드를 제공하며, 만약 더 많은 페이지들이 존재한다면, 페이지 수만큼의 번들이 더 많이 존재하고, 각 번들 파일은 해당 페이지가 필요할 때 로드되어 더 빠른 로딩 시간을 제공할 것입니다.

 

7. 우측 하단의 아이콘의 정체는?


페이지 우측 하단의 작은 번개 모양의 아이콘이 보이시나요?

 

마우스를 hover했을 때, "Prerendered Page"라고 뜹니다.

이 아이콘은 물론 development mode에서만 볼 수 있으며, 여러분에게 해당 페이지는 자동 정적 최적화의 자격을 갖추었다는 뜻입니다. 즉 기본적으로 호출 시간 때 fetch 되어야 할 데이터에 의존하지 않는다는 의미입니다.

또한 이 페이지는 npm run dev에 의한 빌드 시간에 정적 HTML 파일로서 빌드되고 prerender될 수 있다는 의미이기도 합니다.

 

Next는 page 컴포넌트에 추가되는 getInitialProps()의 부재로 이것을 결정할 수 있습니다.

 

이 경우, 페이지는 HTML 결과물을 생성하는 Node.js 서버를 거쳐서 동작하기보다는 정적인 HTML 파일로 다뤄지기 때문에 로딩 속도가 훨씬 빨라집니다.

 

번개 아이콘 다음, 혹은 사전렌더링되지 않은 페이지에서 대신 나타날 또다른 유용한 아이콘은 움직이는 작은 삼각형 아이콘입니다.

이것은 컴파일 지표(compilation indicator)로, 여러분이 페이지를 저장하면, Next.js는 hot code reloading이 자동으로 코드를 다시 로드하기 전에 어플리케이션을 컴파일합니다.

 

이것은 app이 이미 컴파일되었는지 결정하여 변경된 부분을 즉시 테스트할 수 있는 정말 멋진 방법입니다.

 

8. React Developer Tools 소개 및 설치


Next.js는 React 기반이라 필수적으로 설치해야하는 매우 유용한 도구를 하나 소개하겠습니다. 바로 React Developer Tools입니다.

 

Firefox와 Chrome에서 사용할 수 있는 React Developer Tools는 여러분이 React 어플리케이션을 점검하는데 사용할 수 있는 기본 도구입니다.

 

React Developer Tools(이하 React DevTools)는 Next.js에만 쓰이는 건 아니지만, 여러분이 React가 제공하는 모든 툴들에 익숙하지 않기 때문에 이것들을 소개하고 싶었습니다. 여러분이 이미 이것들을 알고있다고 가정하기 보다 디버깅 툴을 조금 사용해보는 것이 바람직합니다.

 

React DevTools는 빌드된 React 컴포넌트 트리와 함께 props, state, hooks 등의 정보를 확인할 수 있는 각 컴포넌트 inspector를 제공합니다.

 

React DevTools을 설치 후, 크롬의 마우스 오른쪽 클릭 > '검사' 클릭으로 일반 브라우저 개발도구를 열면 ComponentsProfiler라는 2개의 새로운 패널이 추가된 것을 확인할 수 있습니다.

 

여러분의 페이지 내 임의의 컴포넌트 위에 마우스 커서를 위치하면 브라우저는 해당 컴포넌트가 렌더링한 부분를 선택합니다.

 

트리 내의 어떤 컴포넌트를 클릭하면 우측 패널에서 부모 컴포넌트의 정보와 부모로부터 넘어온 props를 확인할 수 있습니다.

컴포넌트명 주위를 클릭함으로써 여러분은 손쉽게 탐색할 수 있습니다.

 

개발자 도구 툴바에 위치한 눈 모양 아이콘을 클릭하여 DOM 엘리먼트의 정보를 확인할 수 있으며, 마우스 커서 아이콘과 함께 있는 첫번째 아이콘을 사용하면 브라우저 UI 안의 엘리먼트와 함께 바로 해당 엘리먼트의 React 컴포넌트를 확인할 수 있습니다.

 

벌레 모양의 아이콘을 사용하여 콘솔에 컴포넌트 데이터를 확인할 수도 있습니다.

콘솔에 데이터를 출력하면, 어떤 엘리먼트에 오른쪽 클릭 후 "Store as a global variable"을 누를 수 있어 상당히 편리합니다. url props를 예로 들어 확인해보겠습니다. 콘솔에서 temp1 식별자로 임시 변수에 저장된 것을 콘솔에서 확인할 수 있습니다.

또한 Next.js 개발 모드에서 자동으로 로드되는 Component 패널 내 Source Maps을 사용하여 <> 코드를 클릭하여 Source 패널로 이동하여 컴포넌트 소스코드를 확인할 수 있습니다.

Profiler 탭을 사용하면 앱 내 사용자 인터렉션을 기록하여 무슨 일이 일어났는지를 보여줄 수 있습니다. 

 

적어도 2개의 컴포넌트가 존재해야 인터렉션이 발생하기 때문에 아직은 여러분에게 예시를 보여줄 수 없습니다.

 

아직 소개하지 못한 도구가 하나 있지만 나중에 언급하도록 하겠습니다.

크롬 기반의 스크린샷을 보여드렸지만 파이어폭스또한 동일한 방식으로 React Developer Tools가 작동합니다.

9. 사용가능한 또다른 디버깅 도구


Next.js 어플리케이션을 빌드할 때 유용한 React DevTools말고도 Next.js 앱을 디버깅할 수 있는 다른 2가지 방법을 강조드리고 싶습니다.

 

첫번째 방법은 명시적으로 console.log()를 포함한 모든 Console API를 사용하는 것입니다. Next 앱은 브라우저 콘솔 혹은 npm run dev로 Next 앱을 실행한 터미널에서 log문을 출력할 것입니다.

 

특히, 특정 URL에 접속하거나, 새로고침 버튼을 클릭했을 때 페이지가 서버에서 로드되면, 콘솔 로그는 터미널에서 나타날 것입니다.

 

반대로 마우스 클릭으로 인한 다음 페이지 이동 시의 콘솔 로그는 브라우저에서 출력될것입니다.

 

로그가 보이지 않아 당황스럽지 않게 기억하세요.

 

또다른 기본적인 디버깅 도구는 debugger문입니다. debugger문을 컴포넌트에 추가하면 해당 부분에서 브라우저가 페이지를 렌더링하는 것을 멈출것입니다.

브라우저 디버거가 멈춘 때부터 값을 검사하고 앱을 한줄 씩 실행할 수 있어 아주 유용합니다.

 

VS Code 디버거로 서버사이드 코드를 디버그할 수도 있습니다. 이 방법은 여기에 설명해두었습니다.

 

10. 두 번째 페이지 추가하기


이제 여러분은 Next.js 앱을 개발할 때 유용한 도구들을 잘 이해헀습니다. 이제 우리가 만든 첫번째 앱을 벗어나는 부분부터 계속 개발합시다.

이 웹사이트에 두번째 페이지인 블로그를 만들어보겠습니다. /blog 경로로 접근할 수 있도록 설정하고, 잠시동안은 index.js 컴포넌트처럼 간단한 정적 페이지로 두겠습니다. 

새 파일을 저장한 후, npm run dev 프로세스는 재시작할 필요 없이 페이지를 렌더링하고 있습니다.

 

http://localhost:3000/blog URL로 접속하시면 새 페이지를 볼 수 있습니다.

터미널은 다음과 같이 보일 것입니다.

보이는 바와 같이 /blog 경로는 단지 파일명에 따라 결정되며, 해당 파일은 pages 폴더 내에 있어야 합니다.

 

/hey/ho 페이지를 생성한다면 그 페이지는 http://localhost:3000/hey/ho경로로 접근할 수 있습니다.

 

파일 내의 컴포넌트 이름은 URL 의 목적과는 상관이 없습니다.

 

페이지 소스코드를 보시면, 서버에서 로드될 때 번들파일 중 하나인 /_next/static/development/pages/blog.js로 보이고, 홈페이지의 /_next/static/development/pages/index.js와는 다를것입니다. 자동 코드 스플리팅 덕분에 우리는 홈페이지를 제공하는 번들파일을 필요로 하지 않습니다. 단지 블로그 페이지와 관련한 번들파일만 필요합니다.

또한 blog.js에서 익명의 함수를 export하는 것도 가능합니다.

export default () => (
  <div>
    <h1>Blog</h1>
  </div>
)

혹은 화살표 함수가 아닌 문법을 선호하신다면 다음과 같이 export합니다.

export default function() {
  return (
    <div>
      <h1>Blog</h1>
    </div>
  )
}

 

11. 두 페이지를 연결하기


이제 우리는 index.js와 blog.js 2개의 페이지가 있으니, link를 소개할 차례입니다.

 

보통의 HTML에서 페이지 간 링크는 a 태그를 사용합니다.

<a href="/blog">Blog</a>

Next.js에서는 이렇게 하지 않습니다.

 

왜냐고요? 물론 기술적으로는 할 수 있지만, 이것은 웹이고 웹에서의 모든 것은 결코 깨지지 않기 때문입니다.

(이것이 우리가 여전히 <marquee> 태그를 사용하는 이유입니다.)

 

하지만 Next를 사용하는 데의 가장 주요한 이점은 페이지가 한번 로드되면, 클라이언트 사이드 렌더링에 의해 다른 페이지로의 전환이 매우 빠르다는 것입니다.

 

만약 평범한 a 링크를 사용하게 되면,

const Index = () => (
  <div>
    <h1>Home page</h1>
    <a href='/blog'>Blog</a>
  </div>
)

export default Index

이제 DevTool을 열어서 Network 패널을 확인해보세요. http://localhost:3000/ 를 처음 로드하면 우리는 로드된 모든 번들파일을 받게 됩니다.

이제 Network 패널을 클리어하지 않기 위해 "Preserve log"를 누르고 "Blog" 링크를 클릭해보면, 다음과 같은 일이 일어납니다.

우리는 서버에서 모든 자바스크립트를 다시 받게 됩니다! 하지만... 우리는 이미 받은 자바스크립트 파일에 대해서는 다시 받을 필요가 없습니다. 우리는 단지 새로운 페이지인 blog.js 페이지 번들 하나만 필요합니다.

 

이러한 문제를 해결하기 위해, Next에서 제공하는 컴포넌트인 Link를 사용합니다.

 

다음을 import 하세요.

import Link from 'next/link'

그리고나서 링크를 다음과 같이 감싸세요.

import Link from 'next/link'

const Index = () => (
  <div>
    <h1>Home page</h1>
    <Link href='/blog'>
      <a>Blog</a>
    </Link>
  </div>
)

export default Index

이제 이전에 여러분이 했던 것을 재시도해보면, 즉 blog 페이지로 이동하면 blog.js 번들만 로드되는 것을 볼 수 있습니다.

그리고 tab 위의 브라우저 스피너가 보이지도 않을만큼 이전보다 페이지 로드가 훨씬 빠릅니다.

 

하지만 보는 것처럼 URL이 변경됨을 확인할 수 있습니다. Link는 브라우저의 History API와 동일하게 작동합니다. 

 

Link는 작동 중 클라이언트 사이드 렌더링입니다.

 

만약 여러분이 뒤로가기 버튼을 누르면 어떻게 될까요? 브라우저는 여전히 이전의 가동 중인 index.js 번들을 보유하고 있기 때문에 아무것도 로드되지 않고, /index 라우터를 로드할 준비를 합니다. 이 모든 것은 자동입니다!

 

12. 라우터로 동적 콘텐츠 로드하기


이전 챕터에서, 홈에서 블로그로 어떻게 link를 설정하는지 배웠습니다.

 

블로그는 Next.js에서 훌륭한 사용 예시로, 이제 블로그 포스트들을 추가하는 법을 배워보겠습니다.

 

블로그 포스트들은 동적 URL로 예를 들어 "Hello World"라는 제목의 포스트는 /blog/hello-world 경로로 설정될 것입니다. "My second post"라는 제목의 포스트는 /blog/my-second-post로 설정될 것입니다.

 

이 콘텐츠는 동적이고, 데이터 베이스 혹은 마크다운 파일 등 외부 저장소에서 가져올 수도 있습니다.

 

Next.js는 동적 콘텐츠를 dynamic URL에 기반하여 제공하고 있습니다.

 

[ ] 문법으로  동적 페이지를 생성하는 동적 URL을 만들 수 있습니다.

 

구체적으로 어떻게 만드는지 알아보겠습니다.

 

pages/blog/[id].js 파일을 추가합니다. 이 파일은 위에서 예시로 들었던 /blog/hello-world/blog/my-second-post 등의 /blog/ 경로 아래의 동적 URL들을 처리할 것입니다.

 

파일명에서, 대괄호 안의 [id]는 동적인 어떤 것이든 router쿼리 파라미터로서 id 파라미터에 담길 수 있습니다.

 

이것은 꽤 많은 것들을 한번에 설명한 것입니다.

 

router는 무엇일까요?

 

router는 Next.js에서 제공하는 라이브러리로 next/router에서 임포트 받을 수 있습니다.

import { useRouter } from 'next/router'

임포트한 useRouter를 쓰면 사용하는 router 객체를 초기화합니다.

 

const router = useRouter()

해당 router 객체를 정의한 후, 우리는 이것으로부터 정보를 추출할 수 있습니다.

 

특히 router.query.id [id].js로 인해 생성된 URL에서 동적인 부분을 얻을 수 있습니다.

 

동적인 부분은 마찬가지로 post-[id].js처럼 URL의 한 부분이 될 수 있습니다.

 

이제 실습으로 이 모든 것들을 적용해봅시다.

 

pages/blog/[id].js파일을 생성하고, 다음과 같이 입력합니다.

import { useRouter } from 'next/router'

export default () => {
  const router = useRouter()

  return (
    <>
      <h1>Blog post</h1>
      <p>Post id: {router.query.id}</p>
    </>
  )
}

이제 http://localhost:3000/blog/test 경로로 이동해보면 다음과 같은 페이지가 보일 것입니다.

이 id 파라미터를 사용하여 예컨대 데이터베이스에 저장된 포스트 목록에서 특정 포스트를 가져올 수 있습니다.

간단한 샘플로 프로젝트 root 폴더에 posts.json 파일을 생성합니다. 

{
  "test": {
    "title": "test post",
    "content": "Hey some post content"
  },
  "second": {
    "title": "second post",
    "content": "Hey this is the second post content"
  }
}

이제 posts.json 파일을 임포트하고 id 키값으로 포스트를 검색할 수 있습니다.

import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () => {
  const router = useRouter()

  const post = posts[router.query.id]

  return (
    <>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </>
  )
}

페이지를 리로드하면 다음과 같이 보일 것입니다.

하지만 이 화면 대신에 여러분은 콘솔과 브라우저에서 에러 메세지를 보게 됩니다.

왜 의도했던 화면이 나오지 않는걸까요?

 

렌더링 도중 컴포넌트를 초기화할 때, 데이터는 아직 해당 컴포넌트에 존재하지 않기 때문입니다.

 

다음 장에서 getInitialProps로 컴포넌트에 데이터를 대입하는 방법을 배우겠습니다.

 

지금은 if (!post) return <p></p>로 JSX를 반환하기 전 임시로 확인하는 구문을 추가합니다.

import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () => {
  const router = useRouter()

  const post = posts[router.query.id]
  if (!post) return <p></p>

  return (
    <>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </>
  )
}

이제 정상적으로 작동할 것입니다. 맨 처음 컴포넌트가 router.query.id 데이터 없이 렌더링됩니다. 렌더링이 끝난 후, Next.js는 query 값을 업데이트하고 페이지는 올바른 정보를 보여줍니다.

 

소스코드를 보면, 빈 <p> 태그가 HTML에 존재합니다.

이처럼 SSR을 실행하는데 실패하여 처음에 언급했던 사용자의 체감 로딩 시간과 SEO, 공유 기능을 저하하는 문제를 곧 해결하겠습니다.

 

pages/blog.js에 포스트들을 나열한 다음에 블로그 예제를 마치겠습니다.

import posts from '../posts.json'

const Blog = () => (
  <div>
    <h1>Blog</h1>

    <ul>
      {Object.entries(posts).map((value, index) => {
        return <li key={index}>{value[1].title}</li>
      })}
    </ul>
  </div>
)

export default Blog

next/link에서 Link를 임포트한 후 포스트들의 반복문에 사용하여 각 포스트들에 링크를 걸 수 있습니다.

import Link from 'next/link'
import posts from '../posts.json'

const Blog = () => (
  <div>
    <h1>Blog</h1>

    <ul>
      {Object.entries(posts).map((value, index) => {
        return (
          <li key={index}>
            <Link href='/blog/[id]' as={'/blog/' + value[0]}>
              <a>{value[1].title}</a>
            </Link>
          </li>
        )
      })}
    </ul>
  </div>
)

export default Blog

 

13. Prefetching


이전에 Link 컴포넌트로 두 페이지를 연결하는 링크를 생성하는 법을 배웠습니다.

또한 Link 컴포넌트를 사용하면 Next.js가 프론트엔드 라우팅을 하는 걸 분명히 확인하였습니다. 

따라서 사용자가 링크를 클릭하면, 프론트엔드는 새로운 client/server 요청과 응답을 트리거하지 않고 일반적인 웹페이지가 그렇듯이 새로운 페이지를 띄웁니다.

 

이외에 여러분이 Link를 사용할 때 Next.js가 제공하는 또다른 이점이 있습니다.

 

<Link>로 감싼 어떤 html 요소는 viewport에 나타납니다. 즉, 웹사이트 사용자에게 보여질 수 있다는 뜻입니다.

Next.js는 이 링크가 지역 링크인 한해서 해당 요소를 가리키는 URL를 프리패치하는데 이 점은 사용자가 원하는 화면을 더욱 빠르게 보여질 수 있도록 합니다.

 

이 이점은 오로지 운영 모드(production)에서만 발생합니다. 운영 모드에 대해서는 나중에 더 깊게 다뤄보겠습니다.

따라서 프리패치를 확인하기 위해 여러분이 npm run dev로 실행했던 어플리케이션을 멈추고, npm run build로 운영 번들파일들을 컴파일하고 npm run start로 실행해야합니다.

 

개발자 도구의 Network 패널을 보시면 페이지가 로드될 때 화면 상 보이는 부분에 존재하는 모든 링크들은 로드 이벤트가 발생하자마자 프리패치하기 시작하는 것을 확인할 수 있습니다.

즉, 페이지가 완전히 로드될 때 프리패치가 트리거되고, DOMContentLoaded 이벤트가 완료된 후에 발생합니다.

 

viewport 내의 다른 Link 태그들은 사용자가 스크롤을 할 때 prefetch되기 시작합니다.

 

prefetch는 브라우저가 Save-Data HTTP 헤더를 보내지 않는 이상,  자동으로 Wifi, 3g+와 같은 가장 빠른 인터넷 연결로 이루어집니다.

 

또한 Link 컴포넌트의 props로 prefetch를 false로 설정하면 개별 Link마다 prefetch 설정을 해제할 수 있습니다.

<Link href="/a-link" prefetch={false}>
  <a>A link</a>
</Link>

 

14. router를 사용하여 선택된 링크 감지하기


링크를 사용하는 데 있어 매우 중요한 한가지 특징이 있습니다. 바로 현재 URL이 무엇인지를 결정하는 것인데 특히 클래스명을 선택된 링크에 할당하는 것을 말합니다. 따라서 우리는 이것을 다른 링크들과 구분하기 위해 따로 지정해야 합니다.

 

예를 들어 당신의 사이트 제목에 이 클래스명을 사용할 수 있어 특히 유용합니다.

 

Next.js의 next/link에서 제공하는 기본 Link 컴포넌트는 이것을 자동으로 해주진 않습니다.

따라서 우리가 직접 Link 컴포넌트를 만들고 컴포넌트 폴더 아래에 Link.js로 저장한 후 기본 next/link 대신에 이 Link.js 컴포넌트를 임포트할 수 있습니다.

 

직접 만든 Link 컴포넌트에서 우리는 먼저 react에서 React와 next/link의 Link, next/router에서 제공하는 useRouter hook을 임포트할 것입니다.

 

그리고 현재 경로명이 컴포넌트의 href prop과 동일한지 결정하고, 동일하다면 선택된 클래스명을 컴포넌트의 children으로 추가할 것입니다.

 

마지막으로 React.cloneElement()를 사용하여 갱신된 클래스를 children으로 리턴합니다.

 

아래의 코드는 이 기능을 수행하는 자체 Link 컴포넌트입니다.

import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'

export default ({ href, children }) => {
  const router = useRouter()

  let className = children.props.className || ''
  if (router.pathname === href) {
    className = `${className} selected`
  }

  return <Link href={href}>{React.cloneElement(children, { className })}</Link>
}

 

15. next/router 사용법


여러분은 이미 앞에서 Next.js 앱에서 Link 컴포넌트를 사용하여 코드로 라우팅을 다루는 법을 배웠습니다.

 

이 방법은 JSX 내에서 라우팅을 관리하는 데 유용하지만 여러분은 때때로 프로그램에서 라우팅이 변경되도록 해야 할 필요가 있습니다.

 

이 경우, next/router 패키지에서 제공하는 Next.js Router를 사용하여 push() 메소드를 호출하여 해결할 수 있습니다.

 

다음은 라우터에 접근하는 예시입니다.

import { useRouter } from 'next/router'

export default () => {
  const router = useRouter()
  //...
}

useRouter()를 임포트해서 라우터 객체를 정의하고 나면 라우터의 메소드들을 사용할 수 있습니다.

 

이 라우터는 클라이언트에 존재하는 라우터로, 메소드들은 반드시 프론트엔드 쪽 코드에서만 사용해야 합니다. 이것을 보장하는 가장 쉬운 방법은 함수를 useEffect() 리액트 훅으로 감싸거나 리액트의 stateful 컴포넌트의 componentDidMount() 내에서 사용하는 방법입니다.

 

앞으로 가장 많이 사용하게 될 메소드들은 push()prefetch()입니다.

 

push()는 프론트엔드에서 프로그램이 URL을 변경시키도록 하는 함수입니다.

router.push('/login')

prefetch()는 프로그램이 URL을 prefetch하도록 하는데 13장에서 다뤘던 prefetching을 자동으로 처리하는 Link 태그가 없을 때 유용합니다.

router.prefetch('/login')

아래는 전체 예시입니다.

import { useRouter } from 'next/router'

export default () => {
  const router = useRouter()

  useEffect(() => {
    router.prefetch('/login')
  })
}

이 뿐만 아니라 라우터 변경 이벤트를 감지하는 데에 라우터를 사용할 수도 있습니다.

 

16. getInitialProps()를 통해 컴포넌트에 데이터 보내기


12장의 예시에서 JSON 파일에서 데이터를 가져오려고 할 때 컴포넌트가 클라이언트에서 어떤 데이터를 필요로 하기 때문에 동적으로 post 페이지를 생성하는 데에 문제가 있었습니다. 

import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () => {
  const router = useRouter()

  const post = posts[router.query.id]

  return (
    <>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </>
  )
}

우리는 다음의 에러를 보았습니다.

이 문제를 어떻게 해결할 수 있을까요? 그리고 동적 라우터를 어떻게 SSR로 동작하도록 할 수 있을까요?

 

이 문제를 해결하기 위해 컴포넌트 바로 뒤에 getInitialProps()라는 특수한 함수를 추가하여 props가 존재하는 컴포넌트를 만들어야 합니다.

 

먼저 컴포넌트를 만듭시다.

const Post = () => {
  //...
}

export default Post

그리고 함수를 추가합니다.

const Post = () => {
  //...
}

Post.getInitialProps = () => {
  //...
}

export default Post

이 함수는 객체를 argument로 받는데 몇가지 속성들을 포함하고 있습니다. 특히, 지금 우리가 관심있는 속성은 query 객체로, 이전에 포스트 id를 가져오기 위해 사용했던 값입니다.

 

따라서 객체 구조 분해 할당 문법으로 query 객체를 사용해봅시다.

Post.getInitialProps = ({ query }) => {
  //...
}

이제 getInitialProps()로부터 포스트를 리턴할 수 있습니다.

Post.getInitialProps = ({ query }) => {
  return {
    post: posts[query.id]
  }
}

그리고나서 useRouter 임포트문을 제거하고 Post 컴포넌트로 넘겨준 props 속성 내의 post로 데이터를 가져올 수 있습니다.

import posts from '../../posts.json'

const Post = props => {
  return (
    <div>
      <h1>{props.post.title}</h1>
      <p>{props.post.content}</p>
    </div>
  )
}

Post.getInitialProps = ({ query }) => {
  return {
    post: posts[query.id]
  }
}

export default Post

이제 view source에서 보실 수 있듯이 에러 없이 동작하고 예상한대로 SSR로 동작 할것입니다.

getInitialProps 함수는 앞의 실습대로 Link 컴포넌트를 사용하여 새 페이지를 탐색할 때, 서버사이드와 클라이언트 사이드 양쪽 모두에서 실행됩니다.

 

getInitialProps 는 query 객체 뿐만 아니라 다음의 다른 속성들을 컨텍스트에서 가져옵니다.

 

  • pathname: URL 경로
  • asPath: query를 포함한 브라우저에서 보이는 실제 URL

예를 들어 http://localhost:3000/blog/test를 호출할 때, 각각 다음과 같이 정의됩니다.

  • /blog/[id]
  • /blog/test

또한 서버사이드 랜더링의 경우, 다음의 arguments를 사용할 수 있습니다.

  • req: HTTP 요청 객체
  • res: HTTP 응답 객체
  • err: 에러 객체

req와 res는 Node.js 개발을 하셨다면 익숙할 것입니다.

 

17. CSS


Next.js에서는 어떻게 리액트 컴포넌트에 스타일을 적용할까요?

 

스타일과 관련된 여러분이 선호하는 어떤 라이브러리든 사용할 수 있습니다.

 

하지만 많은 사람들이 Next.js로 개발할 떄 styled-jsx를 사용했기 때문에 Next.js 내부에 styled-jsx가 내장되어 있습니다.

 

또한 styled-jsx는 CSS가 적용된 컴포넌트에만 영향을 미쳐 유지보수가 뛰어난 이른바 스코핑된(scoped) CSS를 제공하는데 꽤 좋은 라이브러리입니다.

 

제 생각엔 여러분이 처음 CSS를 작성하는데 있어 styled-jsx는 좋은 접근법입니다. 추가적인 라이브러리 혹은 복잡한 전처리기를 제외하고 말이죠.

 

Next.js 내의 리액트 컴포넌트에 CSS를 추가하기 위해 JSX 문법 내에 다음으로 시작하는 블록을 추가합니다.

<style jsx>{`

그리고나서 다음과 같이 닫습니다.

`}</style>

이 생소한 블록안에 여러분이 평상시에 .css 파일에서 했던 것처럼 일반 CSS를 작성할 수 있습니다.

<style jsx>{`
  h1 {
    font-size: 3rem;
  }
`}</style>

JSX 문법 안에서는 다음처럼 작성합니다.

const Index = () => (
  <div>
	<h1>Home page</h1>
	<style jsx>{`
		h1 {
		    font-size: 3rem;
		}
	`}</style>
  </div>
)

export default Index

블록 안에서 특정 변수를 채워넣어 값을 동적으로 변경할 수 있습니다. 예를 들어 size 속성이 부모 컴포넌트로 전달되고 styled-jsx 블록에 사용한다고 가정합시다.

const Index = props => (
  <div>
	<h1>Home page</h1>
	<style jsx>{`
		h1 {
		    font-size: ${props.size}rem;
		}
	`}</style>
  </div>
)

전역에 CSS를 적용하고 싶을 때, 컴포넌트로 영역을 지정하지 말고, global 키워드를 style 태그에 추가합니다.

<style jsx global>{`
body {
  margin: 0;
}
`}</style>

Next.js 컴포넌트 내부에 외부 CSS 파일을 임포트하고 싶을 때에는, @zeit/next-css를 먼저 설치해야 합니다.

npm install @zeit/next-css

그리고나서 프로젝트 최상단 경로에 아래의 내용으로 next.config.js라는 설정 파일을 만드세요.

const withCSS = require('@zeit/next-css')
module.exports = withCSS()

Next app을 재시작하면, 여러분이 컴포넌트 혹은 자바스크립트 라이브러리를 임포트하듯이 CSS를 임포트할 수 있습니다.

import '../style.css'

@zeit/next-sass 라이브러리를 대신 사용하여 여러분은 SASS 파일을 직접 임포트할 수도 있습니다.

 

18. custom 태그로 head 태그 옮기기


어떤 Next.js 페이지 컴포넌트로부터 여러분은 페이지 header로 정보를 추가할 수 있습니다.

 

이것은 다음의 상황에서 편리합니다.

  • 페이지 제목을 커스터마이징하고 싶을 때
  • meta 태그를 변경하고 싶을 때

모든 컴포넌트 내부에 여러분은 next/head로부터 Head 컴포넌트를 임포트하여 컴포넌트 JSX 결과에 포함시킬 수 있습니다.

import Head from 'next/head'

const House = props => (
  <div>
    <Head>
      <title>The page title</title>
    </Head>
    {/* the rest of the JSX */}
  </div>
)

export default House

여러분이 페이지의 <head> 구역에 나타내길 원하는 어떠한 HTML 태그라도 추가할 수 있습니다.

 

Next.js가 컴포넌트를 마운팅할 때, Next.js는 Head 내의 태그들이 페이지의 헤더에 포함시킵니다. 마찬가지로 Next.js가 컴포넌트를 언마운팅할 때, Next.js는 해당 태그들을 제거합니다. 

 

19. 19. wrapper 컴포넌트 추가하기


대부분의 페이지들은 거의 비슷해보입니다. 크롬으로 열면, 공통 기본 레이어가 있고, 여러분은 그 안의 것들을 수정하고 싶을 겁니다.

 

네비게이션 바가 있고, 그 안에 실제 컨텐츠가 있습니다.

 

Next.js 에서는 이것을 어떻게 구현할까요?

 

두 가지 방법이 있습니다. 먼저 components/Layout.js를 만들어서 고차 컴포넌트(Higher Order Component)를 사용하는 방법을 소개하겠습니다.

export default Page => {
  return () => (
    <div>
      <nav>
        <ul>....</ul>
      </hav>
      <main>
        <Page />
      </main>
    </div>
  )
}

이 안에 여러분은 각 컴포넌트들을 임포트해서 헤더나 사이드바로 불러올 수 있고, CSS 또한 필요 시 추가할 수 있습니다.

 

그리고 다음과 같이 모든 페이지에서 이것을 사용할 수 있습니다.

import withLayout from '../components/Layout.js'

const Page = () => <p>Here's a page!</p>

export default withLayout(Page)

하지만 고차 컴포넌트를 사용하는 방법은 여러분이 getInitialProps()를 사용할 필요가 없는 아주 간단한 경우에서만 동작합니다. 

 

왜일까요?

 

getInitialProps()은 page 컴포넌트에서만 호출할 수 있기 때문입니다. 하지만 만약 당신이 고차 컴포넌트인 withLayout()을 페이지에 export하고 싶다면, Page.getInitialProps()는 사용할 수 없습니다. withLayout.getInitialProps()도 마찬가지입니다.

 

여러분의 코드 복잡도를 불필요하게 증가하는 것을 방지하기 위해, 다른 접근이 필요합니다. 바로 props를 활용하는 겁니다.

 

export default props => (
  <div>
    <nav>
      <ul>....</ul>
    </hav>
    <main>
      {props.content}
    </main>
  </div>
)

 

그리고 이제 다음과 같이 페이지에서 사용할 수 있습니다.

import Layout from '../components/Layout.js'

const Page = () => (
  <Layout content={(
    <p>Here's a page!</p>
  )} />
)

단지 content 속성에 컴포넌트 JSX를 작성하는 이 방법으로 이제 page 컴포넌트에 getInitialProps()를 사용할 수 있습니다.

import Layout from '../components/Layout.js'

const Page = () => (
  <Layout content={(
    <p>Here's a page!</p>
  )} />
)

Page.getInitialProps = ({ query }) => {
  //...
}

 

20. API 라우팅


웹페이지로서 브라우저에 페이지로 뜨는 페이지 라우터 외에 Next.js에서 API 라우터도 만들 수 있습니다.

 

즉, Next.js에 의해 fetch 요청을 통해 JSON 형태로 저장된 데이터를 프론트엔드에 전송하거나 받을 수 있어 매우 흥미로운 특징입니다.

 

API 라우팅은 /pages/api/ 폴더 아래에 위치하며 /api endpoint에 매핑됩니다.

 

이 특징은 어플리케이션을 만들 때 매우 유용합니다.

 

이 라우터를 통해 우리는 Node.js 코드를 작성할 수 있습니다.

 

여러분은 프론트엔드에서 백엔드로 매우 매끄럽게 전환할 수 있으며, 이 특징은 곧 패러다임의 전환입니다.

 

/pages/api/comments.js 파일이 있고, 이 파일의 목적은 블로그 게시물의 댓글들을 JSON 형태로 반환하는 거라고 가정합시다.

 

그리고 comments.json 파일에 댓글 목록들이 저장되어 있습니다.

[
  {
    "comment": "First"
  },
  {
    "comment": "Nice post"
  }
]

여기 클라이언트로 댓글 목록을 반환하는 샘플 코드가 있습니다.

import comments from './comments.json'

export default (req, res) => {
  res.status(200).json(comments)
}

이 코드는 GET 요청으로 /api/comments URL로 listen하며, 브라우저를 통해 이 API를 호출해볼 수 있습니다.

API 라우터는 동적 API 라우터를 생성하는 [ ] 문법을 이용하여 마치 페이지와 같이 동적 라우팅또한 사용할 수 있습니다.

 

예를 들어 /pages/api/comments/[id].js와 같은 형태로 특정 게시물 id의 댓글들을 반환하는 API를 작성할 수 있습니다.

 

req.query 객체에서 id 값을 찾아 [id].js 의 id 값에 넘겨주는 방식으로 동작할 수 있습니다.

import comments from '../comments.json'

export default (req, res) => {
  res.status(200).json({ post: req.query.id, comments })
}

 

다음은 위의 코드가 실행되었을 때의 결과입니다. 

동적 페이지에서, 여러분은 next/router에서 useRouter를 임포트해서 const router = useRouter()를 통해 라우터 객체를 얻었습니다. 그리고나서 router.query.id를 사용하여 최종적으로 id 값을 얻을 수 있었습니다.

 

서버 사이드에서는 이 과정은 더 쉽습니다. query가 request 객체에 포함되어 있기 때문입니다.

 

POST 요청으로 API를 호출해도, 위의 코드는 동일하게 작동합니다. 모두 default export를 거치게 되어있습니다.

 

POST와 GET 요청 및 다른 PUT, DELETE와 같은 다른 HTTP 메소드를 구분하기 위해서는 req.method 값을 살펴봐야 합니다.

export default (req, res) => {

  switch (req.method) {
    case 'GET':
      //...
      break
    case 'POST':
      //...
      break
    default:
      res.status(405).end() //Method Not Allowed
      break
  }
}

우리가 이미 살펴봤던 req.queryreq.method외에도, req.cookies를 참조하여 쿠키값에 접근하거나 req.body로 요청 body에도 접근할 수 있습니다.

 

이 내면에는 이 모든 기능은 Next.js 팀에서 만든 Micro라는 비동기 HTTP 마이크로서비스에 의해 동작하고 있습니다. 

 

따라서 더 많은 기능성을 추가하기 위해 모든 마이크로 미들웨어를 여러분의 API 라우터에 활용할 수 있습니다.

 

 

21. 코드를 서버사이드 혹은 클라이언트 사이드에서 실행하기


페이지 컴포넌트의 코드는 window 속성을 확인하여 클라이언트 혹은 서버 사이드에서만 실행할 수 있습니다.

 

이 속성은 브라우저에만 존재하기 때문에 확인을 통해 구분할 수 있습니다.

if (typeof window === 'undefined') {

}

위의 if문 블록에 서버에서만 동작하는 코드를 작성할 수 있습니다.

 

비슷하게, 클라이언트 사이드 코드를 다음과 같이 확인하여 실행할 수 있습니다.

if (typeof window !== 'undefined') {

}

JS  Tip: 우리는 위의 코드에서 typeof 연산자를 사용했는데 undefiend를 달리 감지할 수 없기 때문입니다. window is not defined 런타임 에러가 발생하기 때문에 if (window === undefined)로 undefined를 감지할 수 없습니다.

 

빌드 타임 최적화 관점에서 Next.js는 번들파일로부터 위의 확인하는 코드를 제거하기도 합니다. 클라이언트 사이드 번들 파일은 if (typeof window === 'undefined') {} 블록으로 감싼 내용은 포함하고 있지 않습니다.

 

22. 운영 버전으로 배포하기


앱을 배포하는 과정은 항상 튜토리얼의 마지막입니다.

 

하지만 이 튜토리얼에서는 조금 이르게 이 과정을 소개합니다. Next.js로 만든 앱 배포는 매우 쉬워 지금 충분히 할 수 있기 때문입니다. 그 후에 더 복잡한 주제를 다뤄보도록 합시다.

 

4장의 "Next.js 설치하기"에서 package.json 스크립트 영역에 아래의 3줄을 추가하라고 했던 것 기억하시나요?

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

지금까지는 npm run dev를 사용하여 로컬환경에서 설치된 node_modules/next/dist/bin/next의 next 커맨드를 호출했습니다.

 

이 커맨드로 디버깅 시 매우 유용한 source maps과 hot code reloading이 제공되는 개발 서버를 실행했습니다.

 

npm run build를 실행하면 build 플래그를 넘겨 웹사이트를 빌드시킵니다. 그리고나서 npm run start로 start 플래그를 넘겨 운영 앱이 시작됩니다.

 

이 두 명령어는 여러분이 성공적으로 운영 버전의 사이트를 로컬에서 배포하는데 사용해야 하는 명령어입니다.

 

운영 버전의 앱은 잘 최적화되어있으며 최종 사용자들에게는 유익하지 않을 hot code reloading이나 source maps과 같은 부수적인 것들을 가지고 있지 않습니다.

 

이제 다음 명령어를 사용하여 운영 배포를 생성해봅시다.

npm run build

명령어를 실행하면 해당 화면에서 어떤 경로들을 출력합니다.

이는 /와 /blog는 정적 HTML로 미리 랜더링 되고,반면에 /blog/[id]는 Node.js 백엔드에서 처리될 것임을 의미합니다.

 

그후에 npm run start를 실행하여 운영 서버를 로컬에서 실행합니다.

npm run start

http://localhost:3000에 접속하면 운영버전으로 배포된 앱을 로컬환경에서 확인할 수 있습니다.

 

23. now로 배포하기


이전 장에서 여러분은 Next.js 어플리케이션을 로컬 환경에서 배포했습니다.

 

이제 다른 사람들이 접근할 수 있도록 실제 웹 서버에 앱을 어떻게 배포할까요?

 

가장 간단한 방법 중 하나는 같은 Next.js 오픈소스 프로젝트 팀인 Zeit이 만든 Now 플랫폼 위에 Next 앱을 배포하는 것입니다.

 

Node.js 앱과 정적 웹 사이트 등을 배포하는 데 Now를 사용할 수 있습니다.

 

Now를 사용하면 앱의 배포와 배급 과정을 매우, 매우 간단하고 빠르게 수행할 수 있으며 Node.js 뿐만 아니라 Go, PHP, Python 등의 다른 언어들도 지원합니다.

 

여러분은 Now를 "cloud"와 같은 개념으로 이해할 수도 있습니다. 실제 앱이 어디에 배포될 것인지는 모르지만, URL을 통해 앱에 접근할 수 있기 때문입니다.

 

Now는 현재 100GB 호스팅, 매일 1000개의 serverless 함수 호출, 매월 1000개의 빌드, 매월 100GB 이내의 대역폭, 한 개의 CDN 서버를 제공하는 무료 정책이 존재하여 무료로 사용할 수 있습니다.

 

더 많은 정보를 원한다면 Now 가격 정책 페이지에서 확인할 수 있습니다.

 

Now를 사용하여 배포하기 위해 가장 권장하는 방법인 공식 Now CLI를 사용합니다.

npm install -g now

설치 후 다음을 실행하세요.

now login

로그인을 시도하면 앱은 여러분에게 이메일을 묻습니다.

 

만약 아직 가입하지 않았다면 https://zeit.co/signup 페이지에서 계정을 만들고 계속하세요.

 

이메일 입력이 완료되었다면 Next.js 프로젝트 최상위 폴터에서 다음을 실행합니다.

now

그러면 앱은 Now 클라우드에서 즉시 배포될것이고 고유한 앱 URL을 받습니다.

now 프로그램을 실행하면, 앱은 now.sh 도메인 아래의 임의의 URL로 배포됩니다.

 

3개의 다른 URL들을 이미지 상의 결과에서 확인할 수 있습니다.

왜 이렇게 많을까요?

 

첫번째 URL은 배포를 확인할 수 있는 URL입니다. 우리가 앱을 배포할 때마다 이 URL은 매번 변경될 것입니다.

 

이 URL을 통해 여러분은 프로젝트 코드를 수정하고 나서 즉시 테스트해볼 수 있으며 테스트가 끝난 후 now를 다시 실행하면 됩니다.

다른 두 개의 URL은 변경되지 않습니다. 그 중 첫번째 URL은 임의의 URL이며, 두 번째 URL은 현재 프로젝트 폴더, 계정명, now.sh로 구성된 기본값입니다.

 

해당 URL로 접속하면 운영 버전으로 배포된 앱을 확인할 수 있습니다.

Now 설정을 변경하여 여러분이 원하는 도메인이나 서브 도메인명으로 사이트를 배포할수도 있지만 지금은 다루지 않겠습니다.

 

24. 앱 번들 파일 분석


Next는 자체적으로 코드 번들을 분석한 결과를 우리에게 제공합니다.

 

package.json 파일의 script 영역에 다음의 새 명령어 3개를 추가합니다.

"analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"

전체 코드입니다.

{
  "name": "firstproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "analyze": "cross-env ANALYZE=true next build",
    "analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
    "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^9.1.2",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  }
}

그리고나서 다음의 두 패키지를 설치합니다.

npm install --dev cross-env @next/bundle-analyzer

프로젝트 최상단 경로에 next.config.js를 생성하고, 다음의 내용을 입력하세요.

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})

module.exports = withBundleAnalyzer({})

이제 새로 추가한 명령어를 실행하여 분석을 시작합니다.

npm run analyze

명령어를 실행하면 브라우저에서 두 페이지가 뜹니다. 하나는 클라이언트 번들이고, 다른 하나는 서버 번들입니다.

이것은 놀랍도록 유용합니다.

 

번들 파일에서 어떤 파일이 가장 많은 용량을 차지하는지 검사하고, 더 작은 번들파일을 시각적으로 더 쉽게 확인하기 위해 사이드 바에서 특정 번들을 제외하고 볼수도 있습니다.

 

25. 지연 로딩 모듈 소개


번들 분석을 시각화 할수 있어 우리는 앱을 더욱 쉽게 최적화 할 수 있어 매우 편리합니다.

 

Moment 라이브러리를 블로그 게시물에 로드할 필요가 있다고 가정합시다.

npm install moment

위의 명령어를 실행하여 프로젝트에 moment 라이브러리를 설치합니다.

 

이제 /blog와 /blog/[id] 이 두 경로에 우리가 필요로 하는 기능을 구현합니다.

 

/blog/[id].js에 다음을 구현합니다.

import moment from 'moment'

...

const Post = props => {
  return (
    <div>
      <h1>{props.post.title}</h1>
      <p>Published on {moment().format('dddd D MMMM YYYY')}</p>
      <p>{props.post.content}</p>
    </div>
  )
}

저는 예제로 오늘 날짜만을 추가했습니다.

 

npm run analyze 실행 결과에서 볼 수 있듯이 이제 블로그 게시물 페이지 번들에 Moment.js를 포함했습니다.

/blog/[id]에 붉은색으로 표시된 부분이 보이시나요? 우리가 방금 Moment.js를 추가했던 부분입니다!

 

약 1kB에서 350kB까지 증가했습니다. 다소 큰 비용입니다.

 

그리고 349kB가 늘어난 이유는 바로 Moment.js 라이브러리의 자체 용량때문입니다.

 

이제 클라이언트 번들을 시각화한 페이지에서 더 큰 번들은 page 가 되었음을 확인할 수 있습니다.  이전에는 매우 작은 번들이었습니다. 그리고 코드의 99%는 Moment.js가 차지하고 있습니다.

블로그 게시물을 로드할 때마다 클라이언트로부터 이 모든 코드를 가져올 것입니다. 매우 이상적이지 않은 방법입니다.

 

한가지 해결책은 아마도 더 작은 사이즈의 라이브러리를 찾는 것입니다. Moment.js는 경량화된 라이브러리로 알려져 있지 않기 때문입니다.

(특히 locales까지 즉시 포함되어 있습니다.)

 

하지만 예제의 편의를 위해 우리가 Moment.js를 사용해야 한다고 가정해봅시다.

 

대신 할 수 있는 방법은 Moment 코드를 번들로 따로 분리하는 것입니다.

 

컴포넌트 단계에서 Moment를 임포트하기보다 getInitialProps내에 비동기로 임포트하도록 합니다. 그리고 나서 연산한 값을 컴포넌트에 보내줍니다.

 

여러분은 getInitialProps()가 반환하는 객체에 복잡한 객체를 반환할 수 없다는 걸 기억하세요. 따라서 날짜를 계산하여 반환해야 합니다.

import posts from '../../posts.json'

const Post = props => {
  return (
    <div>
      <h1>{props.post.title}</h1>
      <p>Published on {props.date}</p>
      <p>{props.post.content}</p>
    </div>
  )
}

Post.getInitialProps = async ({ query }) => {
  const moment = (await import('moment')).default()
  return {
    date: moment.format('dddd D MMMM YYYY'),
    post: posts[query.id]
  }
}

export default Post

await 임포트 후의 .default()라는 특이한 호출이 보입니다. .default()는 동적 임포트 내에서 기본 export를 참조하기 위해 필요합니다. (https://v8.dev/features/dynamic-import)

 

이제 다시 npm run ananlyze를 실행하면 다음과 같은 결과를 보게 됩니다.

/blog/[id] 번들이 다시 작아졌고, Moment는 자체 번들 파일로 이동되어 브라우저에서 분리된 채로 로드됩니다.

 

26. 더 공부해야 할 것들


Next.js에 대해 더 알아야 할 것들이 많습니다. 이 튜토리얼에서는 로그인을 통한 사용자 세션을 관리하는 법, serverless, 데이터베이스를 처리하는 법 등을 다루고 있지 않습니다.

 

이 안내서는 여러분께 모든 것을 가르치기보다는 단계적으로 Next.js가 지닌 힘을 소개하는 것이 목표입니다.

 

추천드리는 다음 단계는 Next.js 공식 도큐먼트를 정독하고 여기서 다루지 않은 더 많은 기능과 특징을 발견하는 것입니다.

또한 매우 잘 되어있는 Next.js 플러그인에서 소개하는 추가 기능들을  살펴보는 것또한 추천드립니다.

 

 

트위터: @flaviocopes

웹사이트: flaviocopes.com

 

 

노트: 여러분은 이 튜토리얼을 PDF/eub/Mobi로 다운받아 오프라인에서도 읽을 수 있습니다!