서버 사이드 렌더링 (3)

서버 사이드 렌더링 (3)

모던 리액트 Deep Dive: 리액트의 핵심 개념과 동작 원리부터 Next.js까지, 리액트의 모든 것 - 04장 서버 사이드 렌더링 (3)

3. Next.js 톺아보기

Next.js 란?

Next.js는 Vercel이라는 미국 스타트업에서 만든 리액트 기반 서버 사이드 렌더링 프레임워크다.

Next.js의 페이지 구조는 디렉터리 구조가 URL로 변환되는 것은 react-page에서 구현해 놓은 기능으로, Next.js도 디렉터리 기반 라우팅을 서비스한다.

Next.js 시작하기

Next.js는 create-next-app을 제공해 프로젝트를 생성할 수 있다.

npm create-next-app@latest --ts

package.json

package.json에는 프로젝트 구동에 필요한 모든 명령어 및 의존성이 포함돼 있다.

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint . --fix",
  },
  "dependencies": {
    "next": "13.1.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "4.9.5"
  },
  "devDependencies": {
    "@titicaca/eslint-config-triple": "^5.0.0",
    "@titicaca/prettier-config-triple": "^1.0.2",
    "eslint": "^8.38.0",
    "eslint-config-next": "13.1.6",
  }
}
  • next : Next.js의 기반이 되는 패키지
  • eslint-config-next : Next.js 기반 프로젝트에서 사용하도록 만들어진 ESLint 설정

next.config.js

next.config.js는 Next.js 프로젝트의 환경 설정을 담당하다.

/** @type {import('next').NextConfig} */
const nextConfig = {
	reactStricMode: true,
	swcMinfy: true
}

moudle.exports = nextConfig

@type으로 시작하는 주석은 자바스크립트 파일에 타입스크립트의 타입 도움을 받기 위해 추가된 코드다.

  • reactStrictMode : 리액트의 엄격 모드와 관련된 옵션이다.
  • swcMinfy : 바벨의 대안이라고 볼 수 있다. 바벨 보다 빠르며 그 이유는 러스트(Rust)라는 완전히 다른 언어로 작성했으며, 병렬로 작업을 처리했기 때문이다.

pages/_app.tsx

pages 폴더가 src에 있거나 혹은 프로젝트 루트에 있어도 동일하게 작동한다.

_app.tsx 그리고 내부에 있는 default export로 내보낸 함수는 애플리케이션의 전체 페이지의 시작점이다.

  • 에러 바운더리를 사용해 애플리케이션 전역에서 발생하는 에러 처리
  • reset.css 같은 전역 CSS 선언
  • 모든 페이지에 공통으로 사용 또는 제공해야 하는 데이터 제공

최초에는 서버 사이드 렌더링을, 이후에는 클라이언트에서 _app.tsx의 렌더링이 실행된다.

pages/_document.tsx

_document.tsx가 없어도 실행에 지장이 없는 파일이다.

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

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
  • _app.tsx가 애플리케이션 페이지 전체를 초기화하는 곳
  • _document.tsx는 애플리케이션의 HTML을 초기화하는 곳

_app.tsx와 다음과 같은 차이점이 있다.

  • <html> 이나 <body>에 DOM 속성을 추가하고 싶다면 _document.tsx를 사용한다.
  • _document.tsx는 무조건 서버에서 실행된다. 이벤트 핸들러를 추가하는 것은 불가능하며 이벤트 추가는 hydrate의 몫이다.
  • next/head는 페이지에서 사용할 수 있으며, SEO에 필요한 정보나 title 등을 담을 수 있다. 또한 next.document<Head/> 내부에서는 <title/>을 사용할 수 없다.
  • getServerSideProps, getStaticProps 등 서버에서 사용 가능한 데이터 불러오기 함수는 여기에서 사용할 수 없다.

참고로 _document.tsx에서만 할 수 있는 또 한 가지 작업은 바로 CSS-in-JS의 스타일을 서버에서 모아 HTMl로 제공하는 작업이다.

pages/_error.tsx

_error.tsx 파일은 기본으로 생성해 주는 파일은 아니며, 없더라도 실행하는 데 지장이 없다.

에러 페이지는 클라이어트에서 발생하는 에러 또는 서버에서 발생하는 500에러를 처리할 목적으로 만들어졌다.

  • Next.js 프로젝트 전역에서 발생하는 에러를 처리하고 싶다면 사용
import { NextPageContext } from 'next'

function Error({ statusCode }: { statusCode: number }) {
  return (
    <>
      {statusCode ? `서버에서 ${statusCode}` : '클라이언트에서'} 에러가
      발생했습니다.
    </>
  )
}

Error.getInitialProps = ({ res, err }: NextPageContext) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : ''
  return { statusCode }
}

export default Error

pages/404.tsx

기본으로 생성해 주는 파일은 아니며 없는 경우에는 Next.js에서 제공하는 기본 404 페이지를 제공한다.

404.tsx 파일은 404 페이지를 정의할 수 있는 파일이다.

import { useCallback } from 'react'

export default function My404Page() {
  const handleClick = useCallback(() => {
    console.log('hi') // eslint-disable-line no-console
  }, [])
  return (
    <h1>
      페이지를 찾을 수 없습니다. <button onClick={handleClick}>클릭</button>
    </h1>
  )
}

pages/500.tsx

  • _error.tsx500.tsx가 모두 있다면 500.tsx가 우선적으로 실행된다.
  • 500.tsx_error.tsx 페이지가 없다면 Next.js에서 제공하는 패이지를 볼 수 있다.
import { useCallback } from 'react'

export default function My500Page() {
  const handleClick = useCallback(() => {
    console.log('hi') // eslint-disable-line no-console
  }, [])

  return (
    <h1>
      (500페이지) 서버에서 에러가 발생했습니다.{' '}
      <button onClick={handleClick}>클릭</button>
    </h1>
  )
}

pages/index.tsx

Next.js는 react-pages처럼 라우팅 구조는 다음과 같이 /pages 디렉터리를 기초로 구성되며, 각 페이지에 있는 default export로 내보낸 함수가 해당 페이지의 루트 컴포넌트가 된다.

개발자가 자유롭게 명칭을 지정해 만들 수 있는 페이지다.

  • /pages/index.tsx : 웹사이트의 루트
  • /pages/hello.tsx : /pages가 생략되고, 파일명이 주소
  • /pages/hello/world.tsx : 디렉토리의 깊이만큼 주소를 설정할 수 있다.
  • /pages/hello/[greeting]/tsx : []의 의미는 여기에 어떠한 문자도 올 수 있다는 뜻
  • /pages/hello/[...props].tsx : 전개 연산자와 동일하게 작동한다. [...props] 값은 props라는 변수에 배열로 온다.

서버 라우팅과 클라이언트 라우팅의 차이

Next.js는 서버 사이드 렌더링을 수행하지만 동시에 싱글 페이지 애플리케이션과 같이 클라이언트 라우팅 또한 수행한다.

next/link는 Next.js에서 제공하는 라우팅 컴포넌트이며 <a> 태그와 비슷한 동작을 한다.

  • <a>는 서버에서 렌더링을 수행하고, 클라이언트에서 hydrate하는 과정에서 한 번 더 실행된다.
  • next/link는 클라이언트에서 필요한 자바스크립트만 불러온 뒤 라이팅하는 클라이언트 라우팅/렌더링 방식으로 작동한다.

Next.js는 사용자가 빠르게 볼 수 있는 최초 페이지를 제공한다는 점과 싱글 페이지 애플리케이션의 장점인 자연스러운 라이팅이라는 두 가지 장점을 모두 살리기 위해 이러한 방식으로 작동한다.

  • <a> 대신 <Link>
  • window.location.push 대신 router.push

페이지에서 getServerSideProps를 제거하면 어떻게 될까?

  • getServerSideProps가 있는 빌드 서버 사이드 런타임 체크가 되어 있다. 빌드 시 λ 기호로 표시 서버 사이드에서 렌더링되는 페이지

  • getServerSideProps가 없는 빌드 빌드 크기가 약간 줄었으며 서버 사이드 렌더링이 필요없는 정적인 페이지로 분류된다. 빌드 시 기호로 표시 빌드 시점에서 미리 만들어도 되는 페이지

pages/api/hello.tsx

서버의 API를 정의하는 폴더다.

라우팅 구조는 페이지와 동일, /api라는 접두사가 붙는다.

  • 서버 요청을 주고 받는다.
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

interface Data {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>,
) {
  res.status(200).json({ name: 'John Doe' })
}

default export로 내보낸 함수가 실행된다.

  • 서버에서 내려주는 데이터를 조합해 BFF(backend-for-frontend) 형태로 활용
  • 풀스택 애플리케이션을 구축하고 싶을 때, 혹은 CORS 문제를 우회하기 위해 사용

Data Fetching

Next.js에서 서버 사이드 렌더링을 지원을 위한 몇 가지 데이터 불러오기 전략을 Data Fetching이라 한다.

  • pages/의 폴더에 있는 라우팅이 되는 파일에서만 사용 가능
  • 예약어로 지정되어 반드시 정해진 함수명으로 export를 사용해 함수를 파일 외부로 내보내야 한다.

getStaticPaths와 getStaticProps

  • 사용자와 관계없는 정적으로 결정된 페이지를 보여주고자 할 때 사용
  • getStaticPathsgetStaticProps는 같이 있어야 사용
import { GetStaticPaths, GetStaticProps } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
	return {
		paths: [{ params: { id: '1' }}, { params: { id: '2' }}],
		fallback: false,
	}
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
	const { id } = params
	
	const post = await fetchPost(id)
	
	return {
		props: { post },
	}
}

export default function Post({ post }: { post: post }) {
	// post로 페이지를 렌더링한다.
}
  1. getStaticPaths에서 해당 페이지는 id를 각각 1, 2만 허용
  2. getStaticProps는 1과 2에 대한 데이터 요청을 수행해 props로 반환
  3. Post는 이 결과를 바탕으로 페이지를 렌더링한다.
  • 두 함수를 사용하면 빌드 시점에 미리 데이터를 불러운 다음에 정적인 HTML 페이지를 만들 수 있다.

getServerSideProps

getServerSideProps는 서버에서 실행되는 함수이며 해당 함수가 있다면 무조건 페이지 진입 전에 이 함수를 실행한다.

  • 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환할 수도, 혹은 다른 페이지로 리다이렉트시킬 수도 있다.
  • Next.js는 꼭 서버에서 실행해야 하는 페이지로 분류해 빌드 시에도 서버용 자바스크립트 파일을 별도로 만든다.
import type { GetServerSideProps } from 'next'

export default function Post({ post }: { post: Post }) {
	// 렌더링
}

export const getServerSideProps: GetServerSideProps = async (context) => {
	const {
		query: { id = '' }
	} = context
	const post = await fetchPost(id.toString())
	return {
		props: { post }
	}
}

context.query.id를 사용하면 /post/[id]와 같은 경로에 있는 id 값에 접근할 수 있다. 이 값을 이용해 props를 제공하면 페이지의 Post 컴포넌트에 해당 값을 제공해 이 값을 기준으로 렌더링을 수행할 수 있다.

Next.js의 서버 사이드 렌더링은 getServerSideProps의 실행과 함께 이뤄지며, 이 정보로 페이지를 렌더링하는 과정이 바로 서버 사이드 렌더링이다.

서버에서만 실행되기 때문에 다음과 같은 제약이 있다.

  • window.document와 같이 브라우저에서만 접근할 수 있는 객체에는 접근할 수 없다.
  • API 호출 시 /api/some/path와 같이 protocol과 domain 없이 fetch 요청을 할 수 없다. 자신의 호스트를 유추할 수 없기 때문이다. 반드시 완전한 주소를 제공해야 fetch가 가능하다.
  • 여기에서 에러가 발생한다면 500.tsx와 같이 미리 정의해 둔 에러 페이지로 리다이렉트된다.

컴포넌트 내 DOM에 추가하는 이벤트 핸들러 함수와 useEffect와 같이 몇 가지 경우를 제외하고는 서버에서 실행될 수 있다.

서버 사이드 렌더링은 루트 컴포넌트부터 시작해 모든 컴포넌트로 실행해 완성하므로 클라이언트에서만 실행 가능한 변수, 함수, 라이브러리 등은 서버에서 실행되지 않도록 별도로 처리해야 한다.

사용자가 매 페이지를 호출할 때마다 실행되고, 이 실행이 끝나기 전까지는 사용자에게 어떠한 HTML도 보여줄 수 없다.

  • getServerSideProps에는 반드시 해당 페이지를 렌더링하는 데 있어 중요한 역할을 하는 데이터만 가져오는 것이 좋다.
  • getServerSideProps에서 해당 조건에 따라 다른 페이지로 보내고 싶다면 redirect를 사용할 수 있다.

getInitialProps

getInitialPropsgetStaticPropsgetServerSideProps가 나오기 전에 사용할 수 있었던 유일한 페이지 데이터 불러오기 수단이었다.

  • 페이지의 루트 함수에 정저 메서드로 추가한다는 점과 props 객체를 반환하는 것이 아니라 바로 객체를 반환한다.

getInitialProps는 라우팅에 따라서 서버와 클라이언 모두에서 실행 가능한 메서드다.

가급적이면 getStaticPropsgetServerSideProps를 사용하는 편이 좋다.

스타일 적용하기

전역 스타일

CSS React이라, 불라는 이른바 브라우저에 기본으로 제공되고 있는 스타일을 초기화하는 등 애플리케이션 전체에 공통으로 적용하고 싶은 스타일이 있다면 _app.tsx를 활용하면 된다. _app.tsx에 필요한 스타일을 import로 불러오면 애플리케이션 전체에 영향을 미칠 수 있다.

// 적용하고 싶은 글로벌 스타일
import '../styles.css'

// 혹은 node_module에서 바로 꺼내올 수도 있다.
import 'normalize.css/normalize.css'
  • _app.tsx에서만 제한적으로 적용

컴포넌트 레벨 CSS

Next.js에서는 컴포넌트 레벨의 CSS를 추가할 수 있다. [name].module.css와 같은 규칙만 준수하면 된다.

  • 클래스명과 겹쳐서 스타일을 충돌이 일어나지 않도록 고유한 클래스명을 제공
  • 페이지와 다르게 컴포넌트 레벨 CSS는 어느 파일에서건 추가할 수 있다.
import styles from './Button.module.css'

export function Button() {
	return (
		<button class={styles.alert}
			버튼
		</button>
	)
}

SCSS와 SASS

sass와 scss도 css를 사용했을 때와 마찬가지로 동일한 방식으로 사용한다.

npm install --save-dev sass

설치하면 별도의 설정 없이 바로 동일하게 스타일을 사용할 수 있다.

scss에서 제공하는 variable을 컴포넌트에서 사용하고 싶다면 export 문법을 사용하면 된다.

// primary 변수에 blue라는 값을 넣는다
$ primary: blue:

:export {
	primary: $primary
}

export function Button() {
	return (
		<button class=
			버튼
		</button>
	)
}

CSS-in-JS

자바스크립트 내부에 스타일시트를 삽입하는 CSS-in-JS 방식이다.

비록 CSS와 비교했을 때 CSS-in-JS가 코드 작성의 편의성 이외에 실제로 성능 이점을 가지고 있는지는 논쟁거리로 남아있지다

  • CSS 구문이 자바스크립트 내부에 있다는 것은 확실히 프론트엔드 개발자에게 직관적이고 편리하게 느껴진다.

CSS-in-JS 라이브러리

  • styled-jsx
  • styled-components
  • Emotion
  • Linaria

© 2021. All rights reserved.

Powered by Hydejack v9.1.6