📌 Next를 왜 사용할까?
React 대신 Next를 사용하는 이유를 생각해 보았을 때, 가장 먼저 떠오르는 것은 SSR이라고 생각한다. 하지만 SSR 때문에 Next를 사용하는 건 아니라고 생각한다. SSR은 React에서도 가능하기 때문이다. 그렇다면 다른 이점이 어떤 것이 있을까?
Next는 React를 확장한 프레임워크이기 때문에 여러 다양한 기능들을 제공하고 있다. 폴더 기반의 간편한 라우팅, 다양한 렌더링 전략, 코드 스플리팅 자동화 등등 개발자가 빠르게 사용할 수 있고 편리하게 접근할 수 있는 도구들을 제공해준다. 이외에도 여러 이미지, 폰트 최적화 기능을 제공하는 점 또한 장점이라고 생각한다. 즉, 리액트에 프레임워크적인 성격을 심어주는 것이 Next다 라고 생각하면 좋을 것 같다.
📌 내가 Next를 선택한 이유는?
기존에 React로 프로젝트를 진행하기도 했고 새로운 기술을 적용해 스펙트럼을 넓혀보고 경험해보고 싶었다. 뿐만 아니라 현재 진행중인 마니모아 SNS 프로젝트의 경우, 동적 인터렉션이 많지만 동시에 정적인 콘텐츠들도 많다고 생각했다. 때문에 각 화면별 렌더링 전략을 유연하게 경험해볼 수 있을 것이라고 생각했다. 또한, Next 내의 Node 서버를 이용해 별도 서버 구축 없이, RESTful API 를 구현할 수 있다. 이러한 여러가지 장점들을 조합해보았을 때, Next를 사용라기로 결정하게 되었다.
그러나 SNS의 특성 상 사용자 인터랙션이 많은 서비스이다 보니 SSR을 마땅히 적용할 곳을 생각하기 어려웠다. 그래서 페이지마다의 특징을 생각해보며 서버 렌더링 페이지를 선택하게 되었다. Next로 개발중이니만큼 SSR을 통해 좀 더 서비스를 최적화하는 경험이 중요하다고 생각했다.
📌 Next의 렌더린 전략 3가지
우선 기본적으로 Next 에서는 기본적으로 렌더링 전략이 3가지가 존재한다. SSR, SSG, ISR 이다.
- SSR : 접속 요청을 받을 때마다 매번 새롭게 HTML 페이지를 생성하는 방법으로, 상황에 따라 응답속도가 크게 느려질 수 있다.
- SSG : 빌드 타임에 미리 페이지를 정적으로 생성하는 방법으로, 매우 빠르지만 빌드 타임 이후에는 페이지를 재생성하지 않아 최신 데이터를 보여주기 어렵다는 단점이 있다.
- ISR : SSG 방식에 페이지 유통 기한 옵션을 추가하여 유통 기한이 지난 후에는 새로운 페이지를 재생성하는 방법이다.
📌 그렇다면 Next 에서 직접 서버 렌더링을 사용해보자
- Page router 에서는 특정 명칭의 함수 (getStaticProps, getStaticPaths) 를 사용했지만 App router 에서는 단순하게 fetch() API를 사용하면 된다!
- 'use client'를 통해 client component를 만들게 되면 서버 렌더링이 아닌 동적으로 기존의 CSR 방식으로 렌더링되게 된다.
현재 내 프로젝트에서의 목표는 메인 피드의 무한 스크롤의 초기 로딩 속도를 빠르게 개선할 수 없을까? 였다. 왜 메인 피드의 로딩 속도를 줄이는 것이 중요할까. 나는 메인 페이지의 사용자 유입 비중을 생각했다. 로그인 직후 바로 보여지는 페이지고 사람들이 가장 많이 머무르는 화면이다. 여러 사용자들이 동시에 접근해 네트워크를 호출하면 서버 부하는 커질 것이고 이는 곳 서비스 사용성을 떨어지게 만들 것이라고 생각했다. 나는 목표를 달성하기 위해 적절한 방법을 생각해보았다. 결론적으로 나는 사이트 진입 시 최초로 반드시 보여지는 첫번째 페이지는 서버 렌더링으로 불러오고 스크롤을 해야 보여지는 페이지는 동적으로 렌더링되도록 하여 통해 로딩 속도를 개선하는 방법을 선택했다.
이를 위해 나는 Tanstack query를 사용중이었기 때문에 prefetchInfinitequery 메서드를 사용하게 되었다.
📌 PrefetchInfiniteQuery
prefetch는 특정 데이터가 필요하다는 것을 알고 있느 경우, 또는 예측할 수 있는 페이지에 대해 백그라운드에서 리소스를 미리 가져오는 방식을 의미한다. prefetch를 통해 데이터를 미리 불러오는 경우 해당 데이터로 캐시를 미리 채울 수 있어 더 빠른 환경을 제공할 수 있다는 장점이 있다. Tanstack Query 에서 제공하는 PrefetchInfiniteQuery 메서드를 이용하면 prefetch 와 무한 스크롤 기능을 쉽게 구현할 수 있다.
그렇다면 이론적 지식을 공부해보았으니 본격적으로 구현해보겠다. 동작과정은 아래와 같다.
Tanstack Query 를 이용한 server rendering 동작 과정
- html 마크업 문서 생성/렌더링 전에 미리 데이터를 fetch 해온다.
- fetch 해온 데이터를 마크업 문서에 임베드할 수 있는 직렬화된 포맷 데이터로만들고 마크업 문서에 dehydrate 한다.
- 클라이언트에서 리액트 쿼리 캐시로 hydrate시킨다. (클라이언트에서 fetch 하는 것을 피할 수 있다.)
1. 데이터를 fetch하는 함수를 만든다.
export const fetchFeeds = async ({
pageParam = 1,
}: QueryFunctionContext) => {
try {
const response = await fetch(`/api/feed/readAll?cursor=${pageParam}`
).then((res) => res.json());
return response;
} catch (err) {
console.error('게시글 fetch 실패', err);
throw new Error();
}
};
2. 쿼리 데이터를 fetch 하고 dehydrated 된 쿼리를 반환하는 함수를 만든다.
// 쿼리를 페칭하고 dehydrated 된 쿼리를 반환하는 함수
export async function getDehydratedQuery() {
const queryClient = new QueryClient();
await queryClient.prefetchInfiniteQuery({
queryKey: [FEED_KEY],
queryFn: fetchFeeds,
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor
}),
return dehydrate(queryClient);
}
3. 클라이언트에 hydrate 해준다.
export default async function Home() {
const = await getDehydratedQuery()
return (
<HydrationBoundary state={hydratedQuery}>
<FeedList />
</HydrationBoundary>
);
}
📌 SSR 이 잘 적용되었는지 확인하려면
- 개발자 도구 > 성능 탭을 측정해보았을 때, 아래의 그림처럼 데이터가 빈 화면이나 혹은 스켈레톤 UI 없이 바로 렌더링된 페이지가 노출된다면 적용이 되었다고 볼 수 있다.
🎉 결론 : 1초 이상 LCP 개선
- Lighthouse 측정 결과 LCP가 5.5초 -> 3.9초로 1.5초 정도나 개선되는 효과를 보았다. 꽤나 큰 차이를 보인 것 같아 만족스러웠다.
- 물론 SSR을 적용하면서 이미지 최적화 (🔗 관련 글 보러가기), 코드 스프리팅 등을 함께 진행했다. 때문에 이 또한 LCP 개선에 함께 영향을 준 것으로 보인다. 뿐만 아니라 이외에도 네트워크 상의 또다른 원인이 있었을 수도 있진 않을까 생각한다. 최적화의 길은 멀고도 험난한 것 같다...
- 꽤나 서비스 성능을 최적화 할 수 있는 좋은 경험이 되었다. 다음에는 SSR을 통해 SEO 최적화도 진행해보겠다.
'🙆♀️ FE > Performance' 카테고리의 다른 글
[✨Refactor] React Query 로 효율적으로 캐싱처리하기 (0) | 2024.11.18 |
---|