React로 웹 애플리케이션을 처음부터 만들려면 다음과 같은 것들을 고려해야 한다.
위의 문제들을 모두 해결하면서 적절한 추상화가 이루어져 있고 훌륭한 개발자 경험(DX)를 보유한 프레임워크가 바로 Next.js이다.
Next.js는 리액트 프레임워크이다. Next.js는 위에서 말한 React의 모든 문제점들을 해결해준다.
Next.js에서는 API를 개발하는 방법을 제공한다.
pages/api 폴더 안에 있는 파일들은 모두 /api/* 경로와 매칭되고 page 대신 API endpoint로 사용된다.
또한 서버 사이드에 번들되기 때문에 클라이언트쪽의 번들 사이즈를 늘리지 않는다.
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를 감싸주어 해결할 수 있다.
들어오는 요청(req)을 파싱해주는 내장 middleware
모든 API Routes는 config 객체를 커스텀하여 내보낼 수 있다.
api 객체는 API Routes에 사용가능한 모든 config를 포함한다.
// bodyParser가 false인 경우 형태
export const config = {
api: {
bodyParser: false,
},
}
export const config = {
api: {
bodyParser: {
sizeLimit: '500kb',
},
}
export const config = {
api: {
externalResolver: true,
},
}
export const config = {
api: {
responseLimit: '8mb',
},
}
npm i cors
# 또는
yarn add cors
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;
// 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!' });
}
// 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);
})
})
}
module.exports = {
pageExtensions: ['mdx', 'md', 'jsx', 'js', 'tsx', 'ts'],
}
// default value는 ['tsx', 'ts', 'jsx', 'js'].
Next.js는 기본적으로 모든 페이지를 pre-rendering 한다.
각 페이지마다 HTML을 '미리' 생성하는 것
=> 더 좋은 성능 및 SEO와 연관
SSG에서는 viewport에 있는 내용들이 기본적으로 pre-fetch된다.
하지만 SSR 방식을 사용한 route에서는 데이터가 pre-fetch되지 않는다.
각 페이지를 위해 생성된 HTML은 페이지를 구성하는 데 필요한 최소한의 Javascript 코드를 가지고 있다.
페이지가 브라우저에 의해 로딩되면 이 Javascript 코드가 실행되면서 해당 페이지를 완전히 interactive 하게 만든다.
이를 hydration이라고 한다. (HTML 문서를 Javascript 코드로 '적시는' 것)
페이지를 구성하는 HTML을 언제 생성하느냐의 차이이다.
Next.js에서 각 페이지마다의 렌더링 방식은 선택할 수 있다.
Ex) 대부분의 페이지는 SSG로 만들되 일부는 SSR을 사용
Next.js에서는 성능적인 측면에서 SSR보다 SSG를 권장한다.
SSG로 생성된 페이지는 성능 향상을 위한 추가 설정 없이 CDN에 의해 캐싱이 가능하기 때문이다.
일부 페이지에서는 CSR 방식도 사용할 수 있다.
CSR 방식은 SEO를 요구하지 않는 페이지에서 사용된다.
데이터는 runtime에서 fetch되고, 페이지의 컨텐츠는 데이터가 바뀔 때마다 업데이트된다.
SSR 방식과 달리 data fetching이 컴포넌트 레벨에서도 가능하다.
컴포넌트 레벨에서 사용되면 데이터는 컴포넌트가 마운트될 때 fetch된다.
CSR 방식을 쓰면 페이지 로딩 속도가 느려질 수 있다.
이는 데이터 fetching이 컴포넌트나 페이지가 마운트되었을 때 실행되며,
또한 데이터가 캐싱되지 않아 매번 불러오기 때문이다.
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은 data fetching을 위한 리액트 hook 라이브러리이다.
=> CSR 방식을 사용할 때 매우 추천
=> 자동으로 데이터 캐싱, revalidation(데이터가 stale이 되면 자동으로 유효성 재검사), focus tracking, 간격 두고 데이터 다시 받아오기 등 여러 가지 가능
// 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>
)
}