Next.js 공식문서 파보기

Sheryl Yun·2022년 6월 27일
0

Next.js

목록 보기
3/6
post-thumbnail

React에서 해결해야 할 문제들

React로 웹 애플리케이션을 처음부터 만들려면 다음과 같은 것들을 고려해야 한다.

  • 코드는 웹팩과 같은 번들러로 묶여서 바벨과 같은 컴파일러로 변환되어야 한다.
  • 코드 스플리팅과 같은 배포 최적화를 해야 한다.
  • 몇몇 페이지에는 성능과 검색엔진 최적화를 위한 정적인 pre-rendering이 필요하다.
  • 서버 사이드 렌더링이나 클라이언트 사이드 렌더링이 필요할 수 있다.
  • 서버 사이드 코드를 작성해서 React와 데이터 store를 연결할 수 있다.

위의 문제들을 모두 해결하면서 적절한 추상화가 이루어져 있고 훌륭한 개발자 경험(DX)를 보유한 프레임워크가 바로 Next.js이다.

Next.js 소개

Next.js는 리액트 프레임워크이다. Next.js는 위에서 말한 React의 모든 문제점들을 해결해준다.

Next.js의 내장 기능들

  • 직관적인 페이지 기반 라우팅 (+ 동적 라우팅)
  • 페이지 단위의 Pre-rendering (정적 생성(SSG)과 서버 사이드 렌더링(SSR))
  • 자동 코드 스플리팅으로 더 빠른 페이지 로딩 가능
  • 최적화된 prefetching(데이터를 미리 받아옴)으로 클라이언트 사이드 라우팅 가능
  • 내장 CSS와 Sass 지원, 모든 CSS-in-JS 라이브러리 지원
  • 빠른 새로고침 속도
  • 서버리스 함수로 API endpoint를 만들기 위한 API routes
  • 높은 확장성

API Routes

Next.js에서는 API를 개발하는 방법을 제공한다.
pages/api 폴더 안에 있는 파일들은 모두 /api/* 경로와 매칭되고 page 대신 API endpoint로 사용된다.
또한 서버 사이드에 번들되기 때문에 클라이언트쪽의 번들 사이즈를 늘리지 않는다.

API 예시 코드

pages/api/user.js 경로의 API route는 200번 상태코드와 함께 json 응답값을 반환

export default function handler(req, res) {
	res.status(200).json({ name: 'John Doe' });
}

API route 작업을 할 때 handler 함수는 무조건 export default로 내보내야 한다.
인수로는 req와 res를 받는다.
HTTP method를 다루려면 req.method를 사용할 수 있다.

export default function handler(req, res) {
	if (req.method === 'POST') {
    	// POST 요청 처리
    } else {
    	// 다른 HTTP 요청 처리
    }
}

주의

API Routes는 CORS header를 명시하지 않는다. (기본적으로 same-origin일 것이라고 전제)
=> 이 부분은 CORS middleware로 request handler를 감싸주어 해결할 수 있다.

API Middlewares

들어오는 요청(req)을 파싱해주는 내장 middleware

  • req.cookies - 요청에서 보내진 cookie를 담고 있는 객체 (기본값: { })
  • req.query - query string을 담고 있는 객체 (기본값: { })
  • req.body - 'content-type'에 의해 파싱된 body를 담는 객체 (아무 body도 안 보내지면 null)

config 설정

모든 API Routes는 config 객체를 커스텀하여 내보낼 수 있다.
api 객체는 API Routes에 사용가능한 모든 config를 포함한다.

  1. bodyParser
  • 자동적으로 사용됨 (automatically enabled)
  • Stream이나 raw-body와 함께 body를 쓰려면 false로 지정
// bodyParser가 false인 경우 형태
export const config = {
	api: {
    	bodyParser: false,
    },
}
  • bodyParser의 sizeLimit
    파싱된 body의 최대 길이 (bytes 단위로 표시)
export const config = {
	api: {
    	bodyParser: {
        	sizeLimit: '500kb',
    },
}
  1. externalResolver
    • boolean 값
    • 해당 route가 express나 connect 같은 외부 resolver에 의해 handle되는지 여부
export const config = {
	api: {
    	externalResolver: true,
    },
}
  1. responseLimit
    • bodyParser처럼 자동적으로 사용됨 (automatically enabled)
    • API route의 응답 body의 크기가 4MB 이상일 때 경고 표시
    • 특정 바이트 값 또는 false로 설정 가능 (특정 바이트로 설정할 경우 기본값은 4MB)
    • 특정 바이트 설정값은 응답의 가능한(경고가 뜨지 않는) 최대 사이즈
export const config = {
	api: {
    	responseLimit: '8mb',
    },
}

Connect/Express middleware

cors 설정하기

  1. cors 라이브러리 설치
npm i cors 
# 또는
yarn add cors
  1. cors 미들웨어를 API route에 추가
import Cors from 'cors';

// cors middleware 시작
const cors = Cors({
	// GET, HEAD 요청만 허가 (HEAD: header 정보 조회)
	methods: ['GET', 'HEAD'] 
})

// Helper 메서드 
// 다음으로 진행하기 전 middleware가 실행되는 것을 기다리거나 
// middleware에서 에러가 발생했을 때를 처리
function runMiddleware(req, res, fn) {  // 들어오는 fucntion은 위에서 만든 cors 객체
	return new Promise((resolve, reject) => {
    	fn(req, res, (result) => {
        	if (result instanceof Error) {
            	return reject(result);
            }
            
            return resolve(result);
        })
    })
}

async function handler(req, res) {
	await runMiddleware(req, res, cors); // 실행이 되어서 결과값이 반환되고 나면 다음을 실행
    
    // cors 여부를 통과한 후 기본 Rest API 로직 실행
    res.json({ message: 'Hello everyone!' });
}

export default handler;

CORS 처리 예시

// pages/api/cors.js

import Cors from 'cors';
import initMiddleware from '../../lib/init-middleware';

// Cors 객체를 initMiddleware로 감싸서 cors middleware 시작
const cors = initMiddleware(
	Cors({
    	// GET, POST, OPTIONS 요청만 허가
        methods: ['GET', 'POST', 'OPTIONS'],
    })
)

export default async function handler(req, res) {
	// cors 실행
    await cors(req, res);
    
    // (cors 처리가 끝나고 나면) Rest API 로직 실행
    res.json({ message: 'Hello Everyone!' });
}

참고: initMiddleware

// Helper 메서드
export default function initMiddleware(middleware) {
	return (req, res) => 
    	new Promise((resolve, reject) => {
        	middleware(req, res, (result) => {
            	if (result instanceof Error) {
                	return reject(result);
                }
                
                return resolve(result);
            })
        })
}

pageExtensions

  • API Routes 관련 configuration
  • next.config.js에서 설정
module.exports = {
  pageExtensions: ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'],
}

// default value는 ['tsx', 'ts', 'jsx', 'js'].

Pre-rendering

Next.js는 기본적으로 모든 페이지를 pre-rendering 한다.

각 페이지마다 HTML을 '미리' 생성하는 것
=> 더 좋은 성능 및 SEO와 연관

SSG에서는 viewport에 있는 내용들이 기본적으로 pre-fetch된다.
하지만 SSR 방식을 사용한 route에서는 데이터가 pre-fetch되지 않는다.

Hydration 개념

각 페이지를 위해 생성된 HTML은 페이지를 구성하는 데 필요한 최소한의 Javascript 코드를 가지고 있다.
페이지가 브라우저에 의해 로딩되면 이 Javascript 코드가 실행되면서 해당 페이지를 완전히 interactive 하게 만든다.
이를 hydration이라고 한다. (HTML 문서를 Javascript 코드로 '적시는' 것)

Pre-rendering의 2가지 형태

  1. Static Generation(SSG): 정적 생성
  2. Server Side Rendering(SSR): 서버 사이드 렌더링

페이지를 구성하는 HTML을 언제 생성하느냐의 차이이다.

Static Generation (SSG)

  • Next.js에서 권장 (recommended)
  • HTML이 build 타임에 생성되고 요청 시에 재사용된다.

Server-side Rendering (SSR)

  • HTML이 매 요청 때마다 생성된다.

Next.js에서 각 페이지마다의 렌더링 방식은 선택할 수 있다.
Ex) 대부분의 페이지는 SSG로 만들되 일부는 SSR을 사용

Next.js에서는 성능적인 측면에서 SSR보다 SSG를 권장한다.
SSG로 생성된 페이지는 성능 향상을 위한 추가 설정 없이 CDN에 의해 캐싱이 가능하기 때문이다.

일부 페이지에서는 CSR 방식도 사용할 수 있다.

CSR에서의 data fetching

CSR 방식은 SEO를 요구하지 않는 페이지에서 사용된다.

  • 데이터를 굳이 '미리 렌더링'(pre-render)할 필요가 없을 때
  • 페이지의 컨텐츠가 자주 업데이트될 때

데이터는 runtime에서 fetch되고, 페이지의 컨텐츠는 데이터가 바뀔 때마다 업데이트된다.
SSR 방식과 달리 data fetching이 컴포넌트 레벨에서도 가능하다.
컴포넌트 레벨에서 사용되면 데이터는 컴포넌트가 마운트될 때 fetch된다.

CSR의 성능 문제

CSR 방식을 쓰면 페이지 로딩 속도가 느려질 수 있다.
이는 데이터 fetching이 컴포넌트나 페이지가 마운트되었을 때 실행되며,
또한 데이터가 캐싱되지 않아 매번 불러오기 때문이다.

CSR에서 useEffect로 데이터 가져오기

function Profile() {
	const [data, setData] = useState(null);
    const [isLoading, setLoading] = useState(false);
    
    useEffect(() => {
    	setLoading(true);
        fetch('/api/profile-data')
        	.then((res) => res.json())
            .then((data) => {
            	setData(data);
                setLoading(false);
            })
    }, [])
    
    if (isLoading) return <p>Loading...</p>
    if (!data) return <p>No profile data</p>
    
    return (
    	<div>
        	<h1>{data.name}</h1>
            <p>{data.bio}</p>
        </div>
    )
}

SWR을 사용하면?

SWR은 data fetching을 위한 리액트 hook 라이브러리이다.
=> CSR 방식을 사용할 때 매우 추천
=> 자동으로 데이터 캐싱, revalidation(데이터가 stale이 되면 자동으로 유효성 재검사), focus tracking, 간격 두고 데이터 다시 받아오기 등 여러 가지 가능

** 위의 코드 예시를 SWR을 사용하여 바꾸기

// swr의 useSWR 모듈 불러오기
import useSWR from 'swr';

// fetcher 함수 만들기 (fetch하여 받아온 데이터를 json으로 파싱하는 고차함수)
const fetcher = (...args) => fetch(...args).then((res) => res.json());

function Profile() {
	// ** useState로 데이터를 관리할 필요가 없어짐
	~~const [data, setData] = useState(null);
    const [isLoading, setLoading] = useState(false);~~
    
    // useSWR로 fetching 로직 처리 (데이터 받아오기 + 에러 처리)
    const { data, error } = useSWR('/api/profile-data', fetcher);
    
    // useEffect도 필요 없어짐
    ~~useEffect(() => {
    	setLoading(true);
        fetch('/api/profile-data')
        	.then((res) => res.json())
            .then((data) => {
            	setData(data);
                setLoading(false);
            })
    }, [])~~
    
    // (여기 로직 대신 아래 사용)
   ~~ if (isLoading) return <p>Loading...</p>
    if (!data) return <p>No profile data</p>~~
    
    // 데이터가 없으면 로딩중
    // fetch 과정에서 에러가 발생하면 에러 메시지 렌더링
    if (error) return <div>Failed to load</div>
    if (!data) return <div>Loading...</div>
    
    return (
    	<div>
        	<h1>{data.name}</h1>
            <p>{data.bio}</p>
        </div>
    )
}
profile
데이터 분석가 준비 중입니다 (티스토리에 기록: https://cherylog.tistory.com/)

0개의 댓글