Next.js의 도입 배경, 그리고 Next.js 공식문서 중 간단하게 진행해볼 수 있는 learn course(https://nextjs.org/learn/basics/create-nextjs-app)를 경험해본 후기를 정리해본다. Next 10 버전으로 테스트했다.
(글이 엄청 길어요)
1. 서버 사이드 렌더링과 클라이언트 사이드 렌더링
Next.js에 대한 설명을 시작하면 꼭 나오는 게 SSR과 CSR 방식이다. 아무래도 React에서 서버 사이드 렌더링을 구현하기 위해 만들어진 프레임워크이다 보니, 렌더링 방식에 대한 이해가 필요하다.
기존의 전통적인 서버 사이드 렌더링(Server Side Rendering) 환경에서는 페이지에 대한 요청이 들어오면, 서버에서 렌더링을 마치고 데이터와 결합된 완성된 정적 HTML 파일을 내려준다. 사용자가 다른 페이지에 접근하면 서버는 또 다시 새로운 HTML 파일을 생성하고, 클라이언트에 내려준다. 이 방식이 서버 사이드 렌더링이다. SSR에서는 각 페이지에 접근할 때마다 페이지 전체에서 새로고침이 발생하면서 매번 페이지 갱신이 이루어지기 때문에 비효율적이지만, 페이지가 처음 그려질 때부터 완성된 HTML로 넘어오기 때문에 크롤링을 고려한 SEO에서는 강점이 있다.
반면 클라이언트 사이드 렌더링(Client Side Rendering)은 서버에서는 페이지를 구성하기 위한 최소 리소스만 전달하고, 클라이언트에서 페이지 컨텐츠를 구성하는 방식이다. CSR 방식에서는 초기 렌더링 시에 필요한 모든 리소스들을 모두 내려받고 나면, 그 이후부터는 사용자가 접근한 페이지들에서 요청하는 일부 데이터만을 요청한다. 따라서 초기 로딩 속도는 느리지만 불필요한 페이지 갱신과 데이터 요청을 줄일 수 있다는 점에서 큰 장점을 가진다.
이렇게 클라이언트 사이드 렌더링으로 구현하는 최근의 웹 트렌드가 SPA(Single Page Application)이다. (그렇다고 CSR이 SPA와 같은 개념은 아니다!) React나 Vue.js와 같은 라이브러리를 많이 사용하면서 자바스크립트가 애플리케이션을 구성하는 데 핵심적인 역할을 담당하고, 번들된 자바스크립트 소스의 실행을 통해 페이지를 구성하는 방식이 많아졌다. SPA는 기본적으로 단일 페이지로 구성되며, 초기 렌더링 시에 페이지에 필요한 리소스를 내려받은 다음부터는 사용자에 필요한 화면만을 갱신한다.
예를 들어, SSR으로 구성된 웹페이지에서 개발자 콘솔에 들어가 최상위의 <html>
태그에 임시로 스타일을 지정한다고 해보자. 다른 페이지로 이동하면 해당 스타일은 사라진다. 페이지가 완전히 새로고침되면서 다시 그려지기 때문이다. 그러나 SPA로 구성된 페이지에서는 동일한 <html>
안에서 필요한 부분만 갱신되므로 아까와 동일한 html이기 때문에 스타일이 사라지지 않는다.
이렇게 SPA가 새로운 트렌드로 떠오르면서 웹 페이지에서도 네이티브 앱과 같은 편리하고 효율적인 사용자 경험이 가능해졌다.
SPA의 단점은 SEO(검색엔진 최적화) 측면에서 문제가 생긴다는 것이다. 많은 크롤러들이 자바스크립트를 지원하지 않기 때문에 크롤링 단계에서 화면에는 빈 HTML만 보여질 수 있다. 물론 구글 봇 같은 경우에는 자바스크립트를 지원하기 때문에 CSR 사이트도 SEO가 가능하긴 하다. (최신 버전의 Google Bot은 ES2015 이상의 최신 자바스크립트도 지원한다고 한다)
2. Next.js의 특징
Next.js는 클라이언트 사이드 렌더링에서 발생하는 여러 문제들을 해결하고, React에서 SSR을 구현할 수 있게 해 준다. React로 애플리케이션을 구성하는 데에는 아래와 같은 문제가 있을 수 있다.
- 자바스크립트 코드를 번들링 하기 위해 webpack 등의 모듈 번들러를 사용하지만, 그렇게 생성된 자바스크립트 번들은 용량이 크고 그만큼의 로딩 시간이 든다. 최적화를 위해서는 코드 스플리팅 처리가 필요하다.
- SEO를 고려하기 위해 자바스크립트 소스를 렌더링하기 전에 정적 소스를 먼저 불러와야 할 경우가 있을 수 있다.
- React를 사용하는 경우 SSR 처리가 어렵다.
Next.js는 위와 같은 고려 사항들을 해결하기 위해 여러가지 기능을 내장했다. 자체 렌더링 서버를 먼저 실행하여 렌더링 단계에서 먼저 실행되어야 하는 로직을 수행한 다음 클라이언트 로직을 수행한다. 최초에 Next 서버로 요청이 들어왔을 때, Next 서버에서는 요청이 들어온 페이지에 들어갈 데이터를 Fetch하고 HTML을 구성하여 클라이언트로 보내주는 방식이다.
아래는 공식 문서에서 가져온 Next.js의 특징 중 일부이다.
- 직관적인 페이지 기반의 라우팅 시스템 (다이나믹 라우팅을 지원)
- Pre-rendering, static generation(SSG)과 server-side-rendering(SSR)을 페이지 단위로 각각 구성할 수 있다.
- 빠른 페이지 로딩을 위해 자동적인 코드 스플리팅을 지원한다.
- 최적화된 prefetching으로 클라이언트 사이드 렌더링을 구현할 수 있다.
- CSS와 Sass 및 CSS-in-JS 라이브러리들을 지원한다.
- 다이나믹 라우팅(dynamic routes) : 라우트의 경로에 특정 값을 넣어 해당 페이지로 이동할 수 있게 하는 것. 동적으로 변경되는 정보는 URL parameter 혹은 query parameter로 전달할 수 있음
- CSS-in-JS : 자바스크립트 코드를 기반으로 CSS를 작성하는 방법론. styled-component, emotion, styled-jsx, JSS 등이 있으며 Javascript template literal을 사용하고 클래스 이름을 자동 생성해주는 등 자바스크립트를 사용한 스타일 지정이 가능. 또한 CSS를 해당 컴포넌트에 종속되도록 함으로써 전역에 대한 영향도를 낮추고 유지보수성을 높일 수 있음
3. next.js의 대표적인 기능들
Next.js는 렌더링 서버를 자체적으로 지원하고, 기본적으로 모든 페이지를 프리 렌더링한다. 클라이언트 사이드에서 모든 작업을 수행하는 대신 미리 각 페이지에 대해서 HTML 파일을 미리 만들어 성능과 SEO 측면에서 도움을 준다.
이 기능을 테스트하기 위해 Next.js learn course에서는 개발자 콘솔의 disabled javascript 기능을 사용한다.
- 개발자 콘솔에서
command + shift + p
-> 명령어 입력기로 들어가서 'Disable JavaScript'를 클릭한다.
2.일반적인 React 환경으로 구성된 페이지에 들어가면, 아래와 같은 메시지가 노출되며 화면이 렌더링되지 않는다.
- 반면 Next.js로 구성된 페이지에 들어가면, pages/index.js에 설정한 내용대로 HTML이 생성되어 있다. (CSS는 js 실행이 필요하므로 적용되지 않는다)
신기한건 getStaticProps, getStaticPaths 함수를 사용해 data fetch를 미리 보낸 부분도 렌더링 서버에서 미리 실행되어서, 화면이 그려지기 전에 data를 제대로 가져오는 것이 가능하다.
이렇게 Next.js는 최소한의 자바스크립트 코드를 사용해 HTML 화면을 먼저 생성한다. 그리고 이어 자바스크립트가 로드되면, 그때 컴포넌트와 앱 화면이 완전히 활성화된다. 이러한 과정을 hydration이라고 한다.
Next.js는 두 가지 형태의 프리 렌더링을 지원한다. 정적 생성(Static Generation)과 서버 사이드 렌더링(SSR)이다. Next.js에서는 성능상의 이유로 SSG를 추천하고 있지만, 각 페이지마다 어떤 종류를 택할지 선택할 수 있다.
Next.js에서는 프리 렌더링 단계에서의 data fetching을 위해 아래 세 함수를 지원한다.
- getStaticProps : 빌드 타임에 데이터 요청
- getServerSideProps : 매 요청에 따라 데이터 요청
- getStaticPaths : 요청된 데이터에 의한 다이나믹 라우팅을 명시
1) getStaticProps (9.3.0 버전부터 도입)
각 페이지에서 async 함수로 getStaticProps가 export 되면, Next 서버는 빌드 시점에 이 함수를 호출하고 리턴한 props을 이용해 페이지를 프리 렌더링 한다.
// javascript case
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
};
}
// typescript case
import { GetStaticProps } from 'next';
export const getStaticProps: GetStaticProps = async (context) => {}
getStaticProps는 props, revalidate, notFound, redirect라는 옵셔널 키를 가진 객체를 반환한다. props는 페이지 렌더링 시에 사용될 값이고, 나머지는 각각 첫 fetching 이후 캐시된 데이터를 얼마나 가져갈 것인지, 에러시 404 페이지로 보낼 것인지, 리다이렉트 처리를 할 것인지를 의미한다. (자세한 내용은 여기)
2) getServerSideProps (server side rendering, 9.3.0 버전부터 도입)
Next.js가 매 요청마다 새로운 데이터를 갱신하게 하려면 getServerSideProps 안에서 데이터를 페치한다. 기본적으로 정적 페이지라면 getStaticProps로 데이터를 가져오고 revalidate 옵션으로 캐싱 주기를 조정하지만, 매번 최신화된 데이터를 사용해야 하는 경우는 요청마다 페칭을 보내도록 할 수 있다.
// javascript case
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
};
}
// typescript case
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async (context) => {}
3) getStaticPaths
동적 라우팅 처리를 하려면, 빌드 타임에 HTML이 그려질 때 각 라우팅 path 정보를 가진 리스트가 필요하다. 라우팅 처리를 하는 곳에서 async 함수로 getStaticPaths를 export 하면 Next 서버는 해당 함수가 리턴한 모든 path 경로의 페이지들을 프리 렌더링한다.
export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } }
],
fallback: true
};
}
함수가 리턴하는 객체에서 paths 안에는 라우팅 처리될 페이지들의 리스트가 담긴다. 만약 pages/posts/[id].js
로 동적 라우팅을 한다면, 위의 코드처럼 paths의 params라는 키 안에 있는 [id]의 값에 프리 렌더링 할 페이지의 값을 넣어준다. 위 코드대로라면 빌드 시점에 posts/1.js와 post/2.js 페이지를 미리 렌더링할 것이다.
또한 fallback이라는 키 안에는 getStaticPaths에서 지정되지 않은 경로로 접속했을 때 404 페이지로 처리할 것인지 여부를 담는다.
Next.js에서 라우팅 처리되는 컴포넌트들은 최상위에 있는 pages라는 폴더 안에 위치한다. 라우팅은 기본적으로 해당 페이지의 디렉토리 경로에 맞게 호출된다. 만약 라우트가 /authors/me
라면, pages/authors 폴더 안에 me.js 파일이 있어야 한다.
동적 라우팅도 사용 가능하다. 변수 파라미터를 표시하기 위해 브라켓 파라메터를 사용한다.
pages/post/[pid].js
-> /blog/:pid
pages/post/[username]/settings.js
-> /post/:username/settings
이렇게 전달된 path 파라미터는 쿼리 파라미터로 해당 컴포넌트에 전달된다.
export default function Post() {
const router = useRouter();
const { pid } = router.query;
// router.query 안에 동일한 이름으로 들어가 있음
}
파라미터가 여러개일 경우 하나의 객체로 머지되어 전달된다.
pages/post/[pid]/[comment].js
라면, 렌더링된 컴포넌트에서 받는 쿼리 객체는 이렇게 된다.
{ "pid": "abc", "comment": "a-comment" }
next 10.0 버전 이후부터는 html의 <img>
태그를 확장한 next/image
라는 컴포넌트를 사용할 수 있다. 사용 방법은 아래와 같다.
import Image from 'next/image';
<Image
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={144}
width={144}
alt={name}
/>
next/image
는 이미지 최적화 기능을 내장하고 있어 리사이징, 옵티마이징, 그리고 WebP와 같은 최신 포맷이 사용 가능한 브라우저에서는 이러한 포맷으로 이미지를 제공한다. 이미지 옵티마이징을 할 때 빌드되는 시점이 아니라 사용자에 의해 리소스가 요청되는 시점에 실행하기 때문에, 이미지가 많다고 해도 최적화에 따른 빌드 타임이 증가하지 않는다. 또한 레이지 로딩 기능도 내장하고 있어, viewport 바깥 영역에 있는 이미지들은 스크롤로 화면에 표시될 때만 로드된다.
참고:
[web] SPA란?
(https://linked2ev.github.io/devlog/2018/08/01/WEB-What-is-SPA/)
Next js 구동방식 과 getInitialProps
(https://velog.io/@cyranocoding/Next-js-%EA%B5%AC%EB%8F%99%EB%B0%A9%EC%8B%9D-%EA%B3%BC-getInitialProps)
TIL 93 | Next.js의 Pre-rendering과 Data Fetch 방법
(https://velog.io/@hyounglee/TIL-93)
Next.js 공식문서
(https://nextjs.org/)
data fetching에 대해서 헷갈렸는데 좋은 글 잘보고 갑니다!
감사합니당 ㅎㅎ