[Next JS] 리액트 프레임워크 맛보기
next, typescript로 개발을 시작한 지 3개월이 되어간다. JS 문법도 핵심으로만 파악하고 리액트로도 크롬익스텐션 하나만 만들어보고 바로 next js로 뛰어왔다. 어느 정도 리액트에 대한 이해가 있었고, next를 프로젝트에 써보면서 그냥 react 만 썼을 때는 어떻게 했을지 생각하며 개발했다.
속도는 더디지만 지금까지 배웠던 것들을 한번 정리하고 가려고 한다. 현재 프론트 단에서 next를 사용하려고 하는 이유는 여러 가지가 있겠지만 그중 핵심적인 것만 짚고 넘어가자.
1. 서버사이드 렌더링 및 정적 사이트 생성
이제는 많은 사람들이 next js 하면 SSR과 SSG를 연관지어서 바로 떠올릴 것이다. 보통 클라이언트 단에서 페이지가 렌더링 되지만, 서버 단에서 페이지를 구현하기 때문에 클라이언트에게 완성된 HTML이 전달된다. 이로 인해 초기 페이지 로드 속도가 빠르고, SEO도 유리하다는 사실은 귀가 닳도록 들었다. 또한 SSG는 빌드 시 모든 페이지를 정적으로 생성하기 때문에 성능과 보안 측면에서도 이점이 있다고 한다.
정적 사이트 생성 (SSG) 에 대해서 좀 더 자세하게 설명하면, SSG는 빌드 시점에 HTML 페이지를 미리 생성하는 방식으로, 이를 위해 각 페이지의 콘텐츠를 미리 렌더링 하여 정적인 HTML 파일로 저장하는 것을 말한다. 이러한 파일들은 서버에 배포되고, 사용자가 웹사이트에 접속할 때 실시간으로 생성할 필요 없이 미리 생성된 HTML파일을 제공받는다. 따라서 빌드 시점에 데이터를 가져와서 HTML을 생성하므로 서버에서 즉시 제공해 페이지 로딩도 빠르다. 서버 부하도 감소하고 SEO 최적화도 가능하다.
Next에서는 getStaticProps 함수로 정적 페이지를 생성했었는데 13버전으로 오면서 fetch안에 인자를 하나 더 넣으며 이를 구현할 수 있게 되었다.
// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
// `force-cache` is the default and can be omitted.
fetch(URL, { cache: 'force-cache' });
// This request should be refetched on every request.
// Similar to `getServerSideProps`.
fetch(URL, { cache: 'no-store' });
// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
fetch(URL, { next: { revalidate: 10 } });
앞서 말했듯이 페이지 요청 시 마다 최신 데이터를 가져오려면 SSR로 구현하는 게 낫다. SSG는 정적인 페이지를 만드므로 그때그때 실시간 데이터를 업데이트하기 어렵다. 페이지 로딩속도 면에서는 SSG가 빠르고, 서버 부하도 적지만, 빌드시간은 페이지가 많아질수록 SSG가 길어진다고 한다. SSR과 SSG를 혼합하여 일정 시간 주기로 페이지를 다시 만드는 ISR 방식도 있다고 한다.
나는 학교 주변 맛집 리뷰서비스를 구현 중이라서, 대부분 SSR을 사용하는 중이다.
2. 정적 파일 지원 및 이미지 최적화
정적 파일 지원은 정적 파일 등을 public 디렉토리에 넣어두고 사용할 수 있음을 말한다. 우리는 흔히 CRA(Create React App)을 사용하면서 당연히 되는 것이라고 생각했지만, 지원하지 않는 경우도 있다고 한다. Vite를 사용해서 크롬 익스텐션을 만들 때 이미지를 넣는 데에 애먹었던 경험이 떠올랐다. Vite에서도 public 폴더를 사용할 수 있었지만, 접근하는 방식이 다르고 인터넷에 자료도 많지 않아서 힘들었다. 초기에 리액트 앱을 만들 때는 webpack과 babel 설정할 게 많아서, 어려웠는데 next 앱을 만들 때는 아직은 설정을 커스텀할 일이 없어서 편했던 것 같다.
이미지 최적화를 해준다는 말은 수없이 들었는데, 정확히 어떤 최적화를 해준다는 걸까?
- 이미지 크기 조정
next/image는 브라우저의 화면 크기나 기기 해상도에 따라 적절한 크기의 이미지를 제공한다. 이는 <img> 태그의 srcset 속성과 비슷한 방식으로 동작하는데, 고해상도 화면에서는 더 큰 이미지가, 저해상도 화면에서는 더 작은 이미지가 로드된다.
- 포맷 변환
next/image는 이미지를 WebP와 같은 최신 이미지 포맷으로 자동으로 변환한다. WebP는 JPEG나 PNG 보다 훨씬 용량을 줄일 수 있고 동시에 화질도 유지하는 포맷이다.
- Lazy Loading
프론트 개발을 하면서 한 번쯤 들어봤을 단어인데, 웹 페이지의 성능을 최적화하는 중요한 기술이다. 특히 이미지가 많은 페이지에서 초기 로딩 속도를 단축하고, 불필요한 네트워크 요청을 줄이는 데 도움이 된다. Lazy Loading은 사용자가 실제로 해당 요소를 보기 전까지 리소스를 로딩하지 않는 방식이다. 페이지가 처음 로드될 때, 모든 이미지를 한 번에 로드하지 않고, 사용자가 스크롤하여 이미지를 볼 때 로드하는 방식이다. 그러니까 일이 닥치지 않으면 로딩을 뒤로 미루는 것이다. 마치 블로그 포스팅을 미루는 나처럼
이는 필요한 리소스만 로드하기 때문에 네트워크 요청도 줄일 수 있다. 또한 메모리 사용량도 줄일 수 있다.
Intersection Observer로 요소가 뷰포트에 들어오거나 나갈 때 이를 감지할 수 있는데, 이를 이용해서 Lazy Loading을 구현할 수도 있다.
import { useEffect, useRef } from 'react';
const LazyImage = ({ src, alt }) => {
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
},
{ rootMargin: '0px 0px 200px 0px' }
);
observer.observe(imgRef.current);
return () => {
observer.disconnect();
};
}, []);
return <img ref={imgRef} data-src={src} alt={alt} />;
};
export default function Home() {
return (
<div>
<h1>Welcome to Lazy Loading with Intersection Observer</h1>
<LazyImage src="/images/logo.png" alt="Logo" />
</div>
);
}
이 옵저버는 리액트 쿼리로 무한스크롤을 구현할 때도 사용한 적이 있는데, 이것에 대해서는 후에 다루겠다.
3. 파일 기반 라우트
Next 는 파일 시스템을 기반으로 하는 직관적인 라우팅 시스템을 제공한다. pages 디렉토리 내에 파일을 생성하면 해당 파일이 자동으로 라우팅 된다. (13 버전에서 app폴더로 바뀜). 아직은 순수 리액트 프로젝트로 페이지 라우팅을 해본 적이 없어서 불편한 것을 모르겠지만, 폴더 구조로 페이지가 나뉘는 것은 매우 편리했다. 안 써봐도 알 것 같았다.
이외에도 Vercel과 완벽한 호환, API 라우트 등 많은 장점이 있지만 오늘은 여기까지만 다루겠다.