티스토리 뷰
안녕하세요. 이번 글에서는 프로젝트를 진행하면서 성능을 개선했던 경험을 공유하고자 합니다. 저는 현재 현 회사에서 담당한 프로젝트 중 상세 페이지에 대한 전체 부분을 담당했습니다.
설명에 앞서 상세 페이지는 동적인 페이지로 Next.js Page router의 getServerSideProps을 사용하여 SEO에 필요한 데이터를 패칭한 점을 알려드립니다.
이로인해 블로킹 현상을 겪었고, 주 원인이 API 응답 지연으로 파악이 되어 해당 부분에 대해 클라이언트 측에서 개선한 점에 대해 다루었습니다. 요청 캐시는 이미 하고 있으므로, 캐싱에 대해서는 다루지 않고, 주로 페이지 전환시 블로킹되는 현상에 대한 부분에 대한 글입니다.
꼭 원인이 API 응답 지연이 아니더라도, 서버사이드에서 동적패칭을 한다면 도움이 될 글이라고 생각합니다.
1. 북 상세 페이지 SSR 및 CSR 조정으로 속도 향상
문제 상황
제가 개발한 상세 페이지는 검색에 유리해야하는 특성상 SEO에 신경을 많이 썼어야 했습니다. 때문에 해당 페이지에서 SEO에 필요한 데이터를 주는 SEO API를 서버사이드에서 패칭 하도록 개발을 해두었습니다.
이로 인해 해당 페이지로 이동할 때 서버에서 API 요청이 완료될 때까지 라우트 이동이 지연되는 문제가 발생했습니다. 잘 알려진 SSR의 단점인 HTML이 완전히 그려지기 전까지 페이지 이동이 블로킹되는 현상이 발생했습니다. TTFB를 측정했더니 평균 250ms 대가 나왔습니다.
이에 대해 성능을 개선할 수 있는 방법을 고민해 보았습니다.
문제 원인 분석 및 고민
- 기존에 SEO 데이터가 불충분했던 이유로 (null값이 많았던 상황)SEO API 외에 상세 API도 함께 요청하고 있었음.
- 기획상 SEO 데이터가 없을 수도 있다는 이유로 SEO에서 title, description이 없을 경우 상세 api 를 요청해 메타데이터를 채웠습니다.
- SSR 특성 > SSG + ISR로 변경해야할까?
- 정적 렌더링을하면 속도는 개선이 되었겠지만, 상세 id가 2000개가 넘는 상황으로, 미리 id를 가져와야하는 SSG는 비효율적이라는 판단이 들었습니다.
- Link prefetch를 적용해 봐야할까?
- Link의 prefetch 속성을 사용해 보려고 했지만, 정적인 데이터만 가능한 이유로 사용하지 않았습니다.
- API 요청 응답 시간 측정시 265ms ~ 329ms 정도로 아주 느리다고는 할 수 는 없지만, 보통 100ms이하면 빠른 편이라고 알고 있고, 제 기준 체감이 조금 되었습니다 .
개선 항목
- 상세 API 제거, CRM 개발자 측에 상세 데이터를 등록하면 SEO 에 Title, Description을 같이 등록되게 하는 방향으로 제안드렸고, 더 이상 상세 API는 필요하지 않게 되어 제거하였습니다.
- Next.js Routing 속성 활용
이 부분을 개선을 어떻게 할 수 있을까 고민하며 문서를 다시 보았는데요. Next.js는 모든 페이지에 대해 pre-rendering을 수행하며 Nextjs 에서 제공하는 router, link를 사용하면 CSR 처럼 라우팅을 합니다. 그렇다면 페이지간 라우팅 시에는 요청 URL이 다르지 않을까 라는 생각이 들었고, getServerSideProps에서 주는 context를 확인 한 결과 아래와 같이 요청 경로가 다름을 확인했습니다.
동작방식 | 요청경로 | 반환 데이터 | 동작 방식 |
SSR (새로고침, URL 직접 입력) | /detail/[id] | HTML 전체 | 새로운 HTML을 서버에서 받아옴 |
CSR처럼 동작 (router.push('/book/[id]')) | /_next/data/development/detail/[id].json?id=[id] | JSON 데이터 | 기존 HTML 유지, React 상태 업데이트 |
SSR로 동작할 때만 SEO API를 패칭해주면 개선이 될 수 있겠다는 생각을 했습니다. 메타데이터 적용이 필요한 부분은 검색이나 오픈그래프에 필요한 부분이기 때문에 페이지 전환시에는 SEO API를 패칭 시키지 않는 것이 비용적으로나, 성능적으로나 효율적이라는 판단을 했습니다.
그래서 저는 아래처럼 context에서 route 분기를 요청 경로(req.url)를 통해 판단 후 html를 처음 그려야하는 URL 직접 접근이 아니라면 (검색을 통한 페이지 직접 접근, 새로고침, URL 직접 입력) SEO API요청을 하지 않게 수정하였습니다.
이로 인해 한 서비스에서 상세 페이지 전환 될 때 패칭을 하지 않기 때문에 블로킹 현상이 현저히 줄어들었습니다.
export const getServerSideProps = (async (context) => {
const id = context.query.id ?? '';
const isReqByNextRoute = context.req.url?.startsWith('/_next');
try {
if (!isReqByNextRoute) { // 첫 요청시에만 데이터 패칭
await Promise.all([
queryClient.prefetchQuery({
queryKey: QUERY_KEY_BOOK_SEO_API.RETRIEVE({
secretKey: String(id),
}),
queryFn: () => bookSeoApi.bookSeoRetrieve({ secretKey: String(id) }),
}),
]);
}
return {
props: {
data: {
dehydratedState: dehydrate(queryClient),
id: String(id),
},
},
};
} catch (e) {
return {
notFound: true,
};
}
}) satisfies GetServerSideProps<{ data: BookDetailServerProps }>;
2. 상세 페이지의 이미지 로드 속도 개선
문제 상황
1. 기존에는 Next Image 를 사용했지만, 클라이언트 측 요청으로 전달 받은 호스트를 통해 최적화 된 이미지를 사용하는 것으로 변경하게 됨.
2. 때문에 모든 이미지를 Next Image에서 기본 Chakra Image 변경하고, 호스트를 통해 이미지를 특정 퀄리티나, 확장자로 변경하기 위해 요청하는 과정에서 width 변경을 크게 요청하면 응답이 지연됨, 최대 2.7초까지 지연되는 것으로 확인 되었던 상황
3. Next image의 최적화 옵션은 사용하지 못하는 상황이 되었고, 서버의 응답 시간을 직접 단축시킬 수 없으니 최대한 클라이언트 측에서 이미지 지연 개선이 필요해지게 됨.
개선 항목
1. 로딩, 에러시 보여지는 fallback props 을 2단계로 적용
최종 로드되어야 할 이미지가 width 788px 이미지고 해당 이미지의 fallback으로 작은 사이즈의 이미지를 요청해 적용했고, 그 작은 사이즈의 이미지의 fallback은 스켈레톤을 적용하여 로딩지연, 에러시 노출되는 최종 이미지를 지정하였습니다.
이로 인해 초기 스켈레톤을 최대 2.7초까지 봐야했던 사용자는 화질 저하는 있지만, 실제 보게 될 이미지를 먼저보게 되며, 유저 경험을 향상시켰습니다.
2. fetchpriority 속성을 'high'로 설정
상세의 메인 이미지의 fetchpriority 속성을 'high'로, 같은 페이지의 하단 쪽에 위치한 작은 이미지들은 'low'로 두어 브라우저가 메인 이미지를 더 빨리 로드하도록 설정했습니다.
<Image
w="100%"
position={'absolute'}
objectFit={'cover'}
bgPos={'center'}
alt={'도서 커버 이미지 영역'}
aria-label="도서 메인 이미지 영역"
fallback={
<Image
w="100%"
position={'absolute'}
objectFit={'cover'}
bgPos={'center'}
alt={'도서 커버 이미지 영역'}
aria-label="도서 메인 이미지 영역"
css={{ fetchpriority: 'high' }}
src={withImageOptions(data?.coverUrl || '')}
fallback={<Skeleton h="627px" pos="absolute" w="100%" />}
/>
}
css={{ fetchpriority: 'high' }}
src={withImageOptions(data?.coverUrl || '', {
w: isApp ? 768 : 430,
})}
/>
마무리
이번 글에서는 Next.js SSR 및 CSR 최적화를 통한 페이지 응답 시간 개선 방법을 소개했습니다.
개선 작업을 진행하면서 API 응답 시간이 길어 사용자 경험이 저하될 수 있다는 점을 인지했고, 클라이언트 측에서 할 수 있는 최적화를 적극적으로 도입하려고 했습니다. 결국 중요한 것은 기본 개념에 충실하는 것이라는 점을 다시 한 번 깨달았습니다. Next.js는 SSR을 지원하지만, CSR처럼 페이지 전환이 가능하다는 점을 활용하여 최적화 방법을 고민했고, 이를 통해 성능을 개선할 수 있었습니다.
앞으로도 성능 최적화를 지속적으로 고민하며, 더 나은 사용자 경험을 제공하기 위해 노력하겠습니다.
'Frontend > NextJS' 카테고리의 다른 글
[nextJS] getLayout: 페이지간 상태를 공유하고 싶을 때 (0) | 2023.10.31 |
---|
- Total
- Today
- Yesterday
- 자바스크립트
- 프로그래머스
- 프로그래머스 자바스크립트
- python
- 알고리즘자바스크립트
- 백준
- github
- 항해99
- GIT
- 모두를위한컴퓨터과학
- 타입스크립트
- 자바스크립트알고리즘
- 자바스크립트 비동기 처리
- reactquery
- javascript
- React Query
- 리액트
- cs50
- html
- 네트워크
- React
- 리액트네이티브
- 실전프로젝트
- network
- 클로저
- 프로그래머스 베스트앨범 자바스크립트
- 자바스크립트 클로저
- 무한스크롤
- css
- 모두를 위한 컴퓨터 과학
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |