서버 사이드 렌더링 (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.tsx
와500.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
- 사용자와 관계없는 정적으로 결정된 페이지를 보여주고자 할 때 사용
getStaticPaths
와getStaticProps
는 같이 있어야 사용
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로 페이지를 렌더링한다.
}
getStaticPaths
에서 해당 페이지는 id를 각각 1, 2만 허용getStaticProps
는 1과 2에 대한 데이터 요청을 수행해props
로 반환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
getInitialProps
는 getStaticProps
나 getServerSideProps
가 나오기 전에 사용할 수 있었던 유일한 페이지 데이터 불러오기 수단이었다.
- 페이지의 루트 함수에 정저 메서드로 추가한다는 점과 props 객체를 반환하는 것이 아니라 바로 객체를 반환한다.
getInitialProps
는 라우팅에 따라서 서버와 클라이언 모두에서 실행 가능한 메서드다.
가급적이면 getStaticProps
나 getServerSideProps
를 사용하는 편이 좋다.
스타일 적용하기
전역 스타일
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