Next.js는 풀스택 웹 어플리케이션을 구축하기 위한 React 프레임워크이다. 프론트 UI구성은 React로 하고, Next.js를 사용하여 추가 기능과 최적화를 수행한다. 내부적으로 Next.js는 번들링, 컴파일 등과 같이 React에 필요한 도구를 추상화하고 자동으로 구성한다. 이를 통해 구성에 시간을 낭비하는 대신 어플리케이션 구축에 집중할 수 있다.
💡 프레임워크?
개발자가 기능 구현에만 ‘딱’ 집중할 수 있도록 필요한 모든 프로그래밍적 재원을 지원하는 ‘기술의 조합’
제어의 역전(IoC : Inversion Of Control)
라이브러리는 개발자가 직접 제어하고 세팅한다. 반대로 프레임워크는 개발자가 직접 제어할 필요 없이 이미 정해진 기능을 적재적소에 사용만 해주면 된다.
✔️ Next.js 사용 이유
1. Full Stack
API Route를 지원하여 full stack 웹 개발이 가능
2. 다양한 렌더링 기법
기존 SPA 라이브러리에서 사용하던 CSR에서 벗어나 SSR, ISR, SSG등을 가능케 함
Next.js는 코드스플리팅을 default로 지원
3. Data Fetching
여러 옵션을 통해 한 번만 값을 가져올지, 일정 주기별로 가져올지, 지속적으로 계속 가져올지 결정할 수 있다.
4. 쉬운 배포
vercel로 일괄 배포
✔️ Next.js의 6가지 개발 원칙
out-of-the-box functionality requiring no setup => 설정이 필요없다.
JavaScript everywhere + all functions are written in JavaScript => 자바스크립트를 어디서나 사용할 수 있다.
automatic code-splitting and server-rendering => 코드 스플리팅과 서버 렌더링이 자동화되어있다.
configurable data-fetching => 다양한 방식으로 데이터를 가져올 수 있는 유연한 옵션을 제공한다.
anticipating requests => 사용자가 원하는 것이 무엇인지를 먼저 예측 = 요구사항 예측
simplifying deployment => 배포가 간단하다.
CSR(Client Side Rendering)
브라우저에서 JavaScript를 이용해 동적으로 페이지를 렌더링하는 방식
React의 CSR에서, 브라우저는 최소한의 HTML 파일과 페이지에 필요한 최소한의 JS 코드만을 다운로드 받는다.
그 후 클라이언트는 JS를 사용하여 DOM을 업데이트하고, 페이지를 렌더링한다.
애플리케이션이 처음 로드되었을 때, 사용자는 그들이 전체 페이지를 보기 전에 약간의 지연을 겪게 된다.
root 라는 div 안에 번들링이 큰 components가 들어있기 때문에 로드되기까지 시간이 소요된다.
이는 모든 JS 코드가 다운로드 되어 파싱되고, 실행되기 전까지 페이지가 렌더링되지 않기 때문이다.
장점
사용자와의 상호작용이 빠르고 부드럽다.
서버에게 추가적인 요청을 보낼 필요가 없기 때문에, 사용자 경험이 좋다.
서버 부하가 적음
단점
첫 페이지 로딩 시간(Time To View)이 길다.
JavaScript가 로딩되고 실행될 때까지 페이지가 비어있어 검색 엔진 최적화(SEO)에 불리하다.
CSR을 통해 SPA을 구축하는 라이브러리로 페이지의 전환 없이 한 페이지에서 바뀐 부분에 대해서만
화면 전환이 이루어지기 때문에 사용자 경험을 향상시키는 데 유리하다.
하지만 이런 React에도 단점이 존재하는데 CSR이기 때문에 서버에서 화면에 보여줄 수 있는 html파일을 보내주는 게
아니라 처음에는 빈 html파일을 보여주고 그 이후에 필요한 resource들을 다운 받아서 보여주기 때문에
1) 초기 로딩 속도가 느리다는 문제와 2) 처음에 빈 html을 보여주기 때문에 검색 엔진 최적화 측면에서 불리하다는
문제가 있다.
✔️ pre-rendering
SSG(Static Side Generation)
서버에서 페이지를 렌더링하여 클라이언트에게 HTML을 전달하는 방식
최초 빌드 시에 HTML이 생성되고 매 요청마다 HTML을 재사용 한다.
SSG에서 HTML은 next build 명령어를 사용할 때 생성
그 후에는 CDN으로 캐시가 되어지고 요청마다 HTML을 재사용
주로 about 페이지와 같이 정적으로 생성된 정보를 요청마다 동일한 정보로 반환하는 경우에 사용한다.
사전에 미리 정적페이지를 여러개 만들어놓음 → 클라이언트가 홈페이지 요청을 하면, 서버에서는 이미 만들어져있는 사이트를 바로 제공 → 클라이언트는 표기만 함
장점
첫 페이지 로딩 시간이 매우 짧아(TTV) 사용자가 빠르게 페이지를 볼 수 있다. 또한, SEO에 유리
CDN(Content Delivery Network) 캐싱 가능
단점
정적인 데이터에만 사용할 수 있음
사용자와의 상호작용이 서버와의 통신에 의존하므로, 클라이언트 사이드 렌더링보다 상호작용이 느릴 수 있다.
계속해서 서버에 요청하기 때문에 서버 부하가 크다.
마이페이지처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가
ISR(Incremental Static Regeneration)
정적 페이지를 먼저 보여주고, 필요에 따라 서버에서 페이지를 재생성하는 방식
SSG에 포함되는 개념이며 SSG와의 차이는 설정한 시간마다 페이지를 새로 렌더링 한다는 점
SSG는 빌드 시에 페이지를 생성하기 때문에 데이터가 변경되면 다시 빌드를 해야하지만, ISR은 일정 시간마다 특정 페이지만 다시 빌드하여 페이지를 업데이트
장점
정적 페이지를 먼저 제공하므로 사용자 경험이 좋으며, 콘텐츠가 변경되었을 때 서버에서 페이지를 재생성하므로 최신 상태 유지
CDN 캐싱 가능
단점
동적인 콘텐츠를 다루기에 한계가 있을 수 있습니다. 실시간 페이지 X
마이페이지처럼 데이터에 의존하여 화면을 그려주는 경우 사용 불가
코드 ➡️ revalidate
import React from 'react';
function HomePage({ data }) {
return <div>{data}</div>;
}
export async function getStaticProps() {
const res = await fetch('https://...'); // 외부 API 호출
const data = await res.json();
return {
props: { data },
revalidate: 60, // 1초 후에 페이지 재생성
};
}
export default HomePage;
SSR(Server Side Rendering)
빌드 시점에 모든 페이지를 미리 생성하여 서버 부하를 줄이는 방식
클라이언트가 페이지 요청할 때마다 html을 생성한다.
항상 최신 상태를 유지해야하는 웹 페이지나, 분석 차트 등 사용자의 요청마다 동적으로 페이지를 생성해서 다른 내용을 보여주어야 하는 경우에 사용
장점
빠른 로딩 속도(TTV)와 높은 보안성을 제공
SEO 최적화
실시간 데이터 사용
마이페이지처럼 데이터에 의존한 페이지 구성 가능
CDN 캐싱 불가
단점
사이트의 콘텐츠가 변경되면 전체 사이트를 다시 빌드하는 과정에서 시간이 오래 걸릴 수 있다. → 서버 과부하
대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을 줄일 수 있다
queryFn 의 매개변수로 Abort Signal 을 받을 수 있고, 이를 이용해서 Query 취소 가능
✏️ 사용방법
QueryFunctionContext
queryFn 은 매개변수로 QueryFunctionContext 이란 객체를 받는다
원래 데이터를 서버로부터 가지고 올 때 받는 매개변수 받는 곳을 비워놓았었는데, 비워져있는게 아닌 Tastack query 에서는 이미 queryFnContext가 들어가 있다. 두번째 인자에 signal 을 넣어주면 GET 요청 시 abort signal 이 옵션으로 들어간 경우에만 unmount 시 자동으로 네트워크 취소가 된다.
1. queryKey: 배열형태의 쿼리키
2. pageParam: useInfiniteQuery 사용 시 getNextPageParam 실행 시 적용
3. signal: AbortSignal 을 의미 (네트워크 요청을 중간에 중단시킬 수 있는 장치)
4. meta: query에 대한 정보를 추가적으로 메모를 남길 수 있는 string 필드
🚨 주의
불필요한 네트워크 요청을 최소화 한다는 명분으로 단순하게 모든 GET 요청마다 Abort Signal 을 심는 것은 작업부하를 올리고 바람직하지 않다.
동영상 다운로드 같은 대용량 fetching이 아닌 이상 대부분의 GET 요청은 빠르게 완료 및 캐싱처리되어 성능에 유의미한 영향을 끼치지 못한다.
대용량 fetching 이 있는 경우 또는 Optimistic UI 를 구현할 때처럼 필요한 경우에만 적용하는 것을 권장
2. Optimistic Updates
낙관적 업데이트, 네트워크 요청이 끝나기 전에 미리 UI 변경
서버 요청이 정상적으로 잘 될거란 가정하에 UI 변경을 먼저하고, 서버 요청 하는 방식. 혹시라도 서버 요청이 실패하는 경우, UI 를 원상복구(revert / roll back)
페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출 (prefetching)
캐시 데이터가 있는 상태로 해당 페이지로 이동 시 로딩없이 바로 UI를 볼 수 있다
✏️ 사용방법
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
// prefetch 할 queryKey와 queryFn 은 이동할 페이지의 쿼리와 동일해야 적절합니다.
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
4. Paginated / Lagged Queries
기존 UI를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용
useQuery의 옵션 중 keepPreviousData를 true 로 바꾸면 데이터를 가져오기 전까진 기존 데이터를 가져와서 사용한다.
로딩 중임을 사용자에게 명시적으로 보여줘야할 때, 쿼리를 가져오는데 5초 이상이 걸린다면 사용자는 5초동안 갱신되지 않는 상태의 UI를 보게된다. 그럴때에는 로딩 중임을 보여주는 것이 좋다. 여러 상황에 맞게 처리해주는 것이 좋다.
5. Infinite Queries
Data Fetching 이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 hook
더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합
실행 순서는 아래와 같다.
queryFn 실행
→ 캐시 데이터 등록 { pages, pageParam }
→ getNextPageParam 실행 (리턴된 NextPageParam는 훅 내부 메모리에 저장. 캐시에 저장X)
→ (NextPageParam 이 undefined이 아니면) hasNextPage true로 상태변경
→ fetchNextPage 실행
→ queryFn 실행 (이 때 내부적으로 저장되어 있던 NextPageParam을 queryFn 의 매개변수로 넘겨줌)