Offispace 프로젝트 최적화 해보기
# Offispace 프로젝트 최적화 경험을 소개합니다.
2024년 06월 01일
최적화 #1 CSR → SSR 전환
Offispace 프로젝트는 Next.js로 개발되고 있었지만, SSR을 적용하고 있지 않아 Next.js가 제공하는 가장 큰 장점을 활용하지 못하고 있다. 이에 따라, 내가 담당하고 있는 커뮤니티 페이지에 SSR을 적용하여 LCP 성능을 향상시켜보고자 한다. 이번 글에서는 CSR 방식과 SSR 방식을 비교하여 성능 향상이 어느 정도 이루어졌는지에 대해 소개하고자 한다.
SSR을 적용해볼 커뮤니티 글 상세 페이지
❗ 커뮤니티 글 상세페이지에서 query를 사용하는 부분은 Main 컨텐츠와, 댓글이고, 해당 영역의 컴포넌트를 SSR로 전환하고 성능 개선을 확인해 볼 예정이다.
SSR 적용 전, 글 상세페이지의 Lighthouse 성능 측정
현재 CSR 방식의 성능 점수는 41점이다. 스크린샷 올리기 부끄러울 정도로 낮다.
CSR vs SSR vs SSG
시작 전에 간단히 CSR, SSR, 그리고 SSG 방식의 장단점을 비교해보자
CSR
클라이언트 측에서 페이지 렌더링을 수행하는 방식
- 사용자의 브라우저에서 JS를 사용하여 페이지를 동적으로 생성
- 서버는 빈 HTML 페이지만 제공하고, 이후 데이터 & 페이지를 렌더링하는 역할은 클라이언트가 수행한다.
- 사용자가 새로운 사이트 요청
- 서버에서 빈 HTML 파일 제공 (화면x, 상호작용x)
- 브라우저가 JS파일 다운로드
- 브라우저가 JS실행
주로 웹 애플리케이션에서 클라이언트 측 라우팅 및 상호작용이 많은 경우에 사용한다.
[장점]
- 상호 작용성 : 클라이언트에서 페이지를 렌더링 하므로 사용자와의 상호 작용이 빠르게 이루어짐
- 서버 부하 감소 : 서버는 초기 HTML만 제공하고 이후에는 클라이언트에서 데이터를 로드함
- 자연스러운 앱 경험 : SPA로 구현되는 경우가 많은데, 이는 자연스러운 앱과 유사한 사용자 경험을 제공한다
[단점]
- SEO 어려움 : CSR은 초기 HTML에 콘텐츠가 없으므로 검섹 엔진 최적화를 구현하기 어려움
- 그 외 : 초기 로딩 후 콘텐츠 표시까지 시간이 걸리는 문제, 성능문제, JS에 의존하는 문제
SSG
정적 사이트 생성. SSG는 페이지를 사전에 빌드 시점에서 생성하고 정적 파일로 제공하는 방식
- 기본적으로 Next는 SSG 방식으로 데이터를 패칭함
- 초기 로딩 속도가 빠르며 SEO가 우수함
- 미리 빌드된 페이지를 제공하므로 서버 부하가 낮음
- 하지만 정적 데이터를 사용하므로 동적 콘텐츠에는 제한이 있음
- 주로 블로그, 포트폴리오 웹 사이트, 회사 홈페이지 등 정적인 사이트에 사용
[장점]
- 빠른 초기 로딩 속도, SEO 우수, 서버 부하 낮음
[단점]
- 동적 데이터 제한, 업데이트된 데이터에 대한 재빌드 필요
SSR
서버에서 페이지를 미리 생성한 뒤, 사용자에게 페이지를 렌더링 하는 방식
- 사용자가 페이지에 접속할 때 서버에서 미리 HTML을 생성하고 클라이언트로 보냄
- 사용자에게 초기 내용을 빠르게 표시하고 SEO를 개선하는데 도움을 줌
- 사용자가 새로운 사이트 요청
- 서버에서 미리 생성된 HTML 파일 제공 (화면O, 상호작용X)
- 브라우저가 JS파일 다운로드
- 브라우저가 JS실행
주로 웹 애플리케이션에서 클라이언트 측 라우팅 및 상호작용이 많은 경우에 사용한다.
[장점]
- SEO 최적화 : SSR 된 페이지들은 검색 엔진에서 쉽게 색인화 가능
- 초기 로딩 속도 개선 : 사용자에게 초기 컨텐츠를 더 빠르게 표시할 수 있음
- 데이터 최신화 : 매 요청마다 최신 데이터를 가져올 수 있음
[단점]
- 서버 부하 : 매 요청마다 서버에서 페이지를 가져오면서 서버 자원을 많이 사용할 수 있음
- 느린 네트워크 연결 : 서버에서 HTML 생성해서 가져오는데, 느린 네트워크 영향을 받으면 초기 로딩이 느려질 수 있음
정리
CSR : 초기 로딩이 빠르지만 SEO가 어려우며 클라이언트에서 데이터 로딩이 필요
SSR : 초기 로딩이 빠르고 SEO가 우수하지만 서버 부하가 증가 할 수 있음
SSG : 초기 로딩이 빠르고 SEO가 우수하며 서버 부하가 낮지만 동적 데이터에 제한이 있음
Main 컨텐츠 SSR 전환
#1
클라이언트 측에서 데이터를 가져오는 방식 때문에 초기 로딩 시간이 길어졌다고 판단하여 초기 페이지 로드 시 서버에서 데이터를 미리 가져와 props로 initialPostData
를 내려주고, 해당 데이터를 useQuery
의 initialData
로 넣어줬다. 해당 방식은 매우 단순하고, 클라이언트 측에서는 해당 데이터를 받아서 처리할 수 있다.
다만, 글을 가져오기 위해서는 로그인 상태가 되어 있어야 header 의 Access Token을 포함해 Api요청을 날리게 되는데 해당 방식을 사용 할 시 미리 Next 서버측에서 Token이 없는 상태로 Api 요청을 날리게 되어, 글을 가져오지 못했다. 이 코드에서는 getServerSideProps
함수에서 req.headers
를 사용하여 쿠키에서 토큰을 추출한다. 추출된 토큰은 API 요청의 Authorization 헤더에 포함되어서 해당 포스트 데이터를 가져오는 데 사용된다. 토큰이 없거나 API 요청이 실패할 경우 적절히 처리하여 initialPostData
를 null로 설정한다.
결과
#2
또 다른 방식으로 QueryClient
를 사용하여 서버에서 데이터를 미리 가져올 수 있다. 이후 dehydrate를 사용하여 QueryClient
의 상태를 직렬화한 dehydratedState
를 설정한다. 클라이언트 측에서는 dehydratedState
를 사용하여 초기 상태를 설정하고 react-query
를 통해 추가적인 데이터 관리 및 캐싱 기능을 활용할 수 있다.
주의할점은 prefetchQuery를 사용할 때, 쿼리 키값을 정확히 설정하는 것이 매우 중요하다. 글 상세 페이지에서는 query.id
를 사용하여 해당 포스트의 ID를 가져오고 있으므로, 이를 getServerSideProps
에서도 같은 query.id
로 가져와 prefetchQuery의 쿼리 키로 사용 하고 있다. prefetchQuery
메소드는 useQuery 메소드와 유사하지만 실제로 데이터를 반환하지는 않고 해당 API에서 넘어온 데이터를 캐싱하는 역할만 한다.
dehydrate
은 hydration
의 반대의 개념인데, React Query에서는 쿼리 결과를 서버에서 클라이언트로 전송할 수 있도록 쿼리 캐시를 직렬화하는 과정을 의미한다. dehydratedState
는 페이지 컴포넌트의 props에 할당 되어 _app.tsx 파일에서 pageProps
객체로 참조가 가능하다. Dehydrate된 QueryClient
를 Hydrate
라는 컴포넌트가 CSR을 시작할 때 동일한 Query를 호출하는 부분을 찾아 initialData
에 할당하는 역할을 수행하게 되어 initialData
없이 SSR 처리가 가능하게 해준다.
hydrate는 서버에서 만들어진 데이터를 클라이언트에서 활용할 수 있도록 동기화 시켜주는 작업이라고 생각하자.
결과
최적화 #2 Next Image로 변환
커뮤니티 페이지 기능 완료후 배포된 사이트의 커뮤니티 페이지를 Light House로 측정한 결과, 매우 처참한 점수을 얻었다. 사실 어느정도 이미지 최적화를 해야지 생각은 하고 있었는데, 생각보다 더 안좋은 결과인것 같아서, 빠르게 이미지 최적화를 시도해보려고 한다.
현재 Next로 개발하고 있음에도, 이미지는 img 태그를 사용하고 있다. Next에서는 Image 컴포넌트를 자체적으로 제공해주고 있다. 공식문서에 따르면, 우수한 Core Web Vitals를 달성하기 위해 Image 컴포넌트에 기본으로 최적화 기능이 포함되어 있다고 한다. 장점으로는 아래와 같다.
• Faster Page Loads : 이미지가 뷰포트에 들어왔을 때만 로드되기 때문에 초기 페이지 로드 속도가 빠름 • Improved Performance : 최신 이미지 형식을 사용하여 디바이스 사이즈에 맞게 최적화된 이미지를 제공 • 자동 스켈레톤 UI(placeholder통해서), CLS 방지 • 자동으로 Lazy Loading을 통해 이미지 최적화를 지원 • next.config.js를 통해 지정된 곳에서만 이미지를 받아오며 악의적인 유저로부터 앱을 보호
그렇다면 본격적으로 Next Image로 변환해보고 얼만큼 성능이 향상 됐는지 알아보자
- 적용 전
커뮤니티 메인에 접속했을 때 모습이다. 이미지 용량 자체가 매우 크고, 뷰포트에 존재하지 않는 이미지도 불러온 모습이다. 보통 이런 경우 lazy loading을 구현하려면, scroll 이벤트를 이용해 해당 section이 뷰포트 안에 들어왔을 때 이미지를 로드하거나, Intersection Observer, Native Lazy loading(img 태그에 loading=”lazy” 설정) 등의 방법을 사용해서 개발을 해야 한다.
- 적용 후
하지만, Next/Image 컴포넌트를 사용하면 lazy loading을 따로 구현할 필요가 없다. 위 적용 전 사진에 빨간 상자 부분의 이미지가 아직 뷰포트에 진입하지 않았기 때문에 아직 로딩 되지 않았고
진입 시 로딩 속도도 1.17초 → 0.88초로 줄어들었다.
또한 이미지 포맷을 png에서 webp로 변환해줘서
이미지 용량 자체도 2.6Mb → 94.7kb로 매우 크게 줄어들었다.
#3 배포 사이트 성능 측정