Gatsby와 Next.js로 똑같은 앱을 만들었을 때 Gatsby의 성능이 더 낫다는 것을 알게 되었다.
원문 : https://medium.com/antlerglobal/gatsby-won-against-next-js-in-this-head-to-head-cd868c3f1275
코로나 펜데믹과 사회적 거리두기로 인해 많은 행사들이 온라인 가상 행사로 대체되고 있다. 필자는 Antler의 소프트웨어 개발자이며, Antler에서는 세계적인 스타트업 양성 프로그램으로 약 12개의 새로운 스타트업 회사를 소개하는 데모 데이(Demo Day) 행사를 매년 진행한다. 이 행사 역시 온라인 개최를 해야 하는 상황에 직면하게 되었다.
우리는 포트폴리오 회사의 프레젠테이션 컨텐츠에 초점을 맞춘 탄탄한 온라인 경험을 제공하고자 했다. 이 행사의 많은 청중과 사용자에게 Antler가 온라인상에서 첫 번째 노출일 수 있을 거라는 사실을 고려해, 우리는 온라인으로 제공되는 프레젠테이션이 빠르게 로드될 수 있도록 최선을 다해야 했다. 이것은 고성능 점진적 웹 앱(PWA, Progressive Web App)의 좋은 사례였다.
배경을 설명하자면 개발한 모든 웹 제품들은 React와 Material-UI 라이브러리로 만들어져 있는데, 개발 속도를 높이고 새 코드가 다른 프로젝트와 호환될 수 있도록 하기 위해 해당 기술 스택을 고수했다. 다른 React 애플리케이션들과 주요한 차이점은 create-react-app으로 부트스트랩 되었고 완벽하게 클라인트 측에서 렌더링(CSR, Client Side Rendering) 된다는 것이다. 그래서 사용자들은 최초로 자바스크립트가 파싱되고 실행되는 동안 빈 화면을 보게 된다.
최고의 성능을 원했기 때문에, 이러한 빈 화면에 대한 초기 로드 경험을 개선하기 위해 서버 사이드 렌더링(SSR, Server-Side Rendering) 또는 정적 사이트 생성기(SSG, Static Site Generation)를 활용하는 방법을 찾고 있었다.
데이터는 Algolia를 통해 Cloud Firestore에서 가져오기 때문에 제한된 API를 키로 공개 데이터 접근을 더욱 세밀하고 필드 레벨 수준에서 제어할 수 있다. 이는 또한 쿼리 성능도 향상시킨다. 일화로, Algolia의 쿼리는 더 빠르며 Firestore 자바스크립트 SDK에서 86KB로 압축되던 것이 Algolia에서는 7.5KB로 압축된다.
또한 오류가 실시간으로 발생할 때 제공하는 데이터가 가능한 한 최신 상태인지 확인하고 싶었다. 표준 SSG 관행에 따르면 컴파일 타임에 이러한 데이터 쿼리를 수행하지만, 관리자용 인터페이스, firetable 및 설립자를 위한 웹 포털에서 데이터베이스를 자주 쓰는 것을 예상하여 여러 빌드가 동시에 실행되도록 했다. 또한 데이터베이스 구조로 인해 관련 없는 업데이트가 새 빌드를 유발할 수 있고, CI/CD 파이프라인을 엄청나게 비효율적으로 만들었기 때문에 사용자들이 페이지를 요청할 때마다 데이터를 쿼리해야 했다. 불행하게도 이것은 "순수한" SSG 웹 앱이 아니었다.
처음에, 앱은 이미 Gatsby로 만들어진 랜딩 페이지들을 유지하고 있었고 랜딩 페이지 중 하나는 Material-UI로 부트스트랩 되어있었기 때문에 Gatsby로 구축했다. 앱 초기 버전에서는 데이터가 로드되는 동안 스켈레톤을 표시하는 페이지를 생성했는데 첫 번째 의미 있는 페인트(FMP, First Contentful Paint) 시간이 약 1초밖에 걸리지 않았다. 🎉
그러나 데이터가 클라이언트 측에서 로드되었기 때문에,
그래서 긴 공휴일 기간에 Next.js로 서버 렌더링 버전을 실험해보기로 했다. 운 좋게도 Material-UI에는 이미 Next.js 프로젝트에 대한 예제가 있었고 프레임워크를 처음부터 배울 필요가 없었다(튜토리얼과 문서의 특정 부분을 살펴보긴 했야 했다). 앱을 변환하고 서버 측에서 데이터를 쿼리하면 위에서 말한 세 가지 사항들이 모두 해결되었으며 비교 결과는….
Google PageSpeed Insights 실행 결과
FMP가 3배 정도 차이가 났다. 또한 Lighthouse에서 속도(speed index) 항목에서는 4배 차이가 났고, 첫 번째 바이트까지 걸리는 시간은 10~20ms에서 2.56초로 증가했다.
Next.js로 개발한 버전이 다른 서비스로 호스팅 된다는 점은 주목할 만했지만(ZEIT Now vs Firebase Hosting - ZEIT Now가 더 높은 TTFB에 기여했을 수 있다), 데이터 패칭을 서버에서 처리했을 때 컨텐츠가 거의 동시에 로드되더라도 눈에 띄게 결과가 느렸고 이 때문에 사용자가 빈 화면만 보게 되었다.
두 가지 로딩 버전에 대한 녹화 화면. 동시에 녹화되지는 않았다. 녹화는 엔터 키를 누르는 순간에 동기화되었다.
이 실험은 프런트 엔드 개발에서 '사용자에게 시각적 피드백을 제공하라'라는 중요한 교훈을 남겼다. 한 연구에 따르면 스켈레톤 화면을 사용하는 앱은 더 빨리 로드되는 것으로 인식되었다고 한다.
또한 결과는 당신이 지난 몇 년 동안 읽어온 웹 개발에 관한 기사를 통해 느꼈던 감정에 반할 수도 있을 것이다.
클라이언트 측은 악마가 아니다. SSR은 모든 성능 문제를 해결하는 솔루션이 아니다.
두 프레임워크는 각각 정적 사이트 생성과 서버 사이드 렌더링 앱 전용으로 알려졌지만, Next.js 9.3에서는 라이벌인 Gatsby처럼 SSG 구현을 정비했다.
글 작성 당시에 이 버전 업데이트는 한 달이 조금 지났었고 Next.js 사이트 메인에는 프레임워크의 SSG 구현에 대한 비교가 많지 않았었다. 그래서 직접 실험해 보기로 했다.
Gatsby로 만들었던 앱에서 있었던 변경 사항 중 다시 클라이언트 측에서 데이터를 가져오는 것으로 변경하고 Gatsby와 Next.js 두 버전이 기능이 정확하게 동일하도록 만들었다. SEO, 파비콘 생성, PWA 매니페스트(manifest) 등 Gatsby 플러그인으로 처리되는 기능들을 비활성화해야 했다. 오직 프레임워크를 통해서 생성된 자바스크립트 번들 파일만 비교하기 위해서 이미지나 외부에서 로드한 컨텐츠를 넣지 않았고 둘 다 Firebase Hosting에 배포했다. 참고로 두 버전은 각각 Gatsby 2.20.9와 Next.js 9.3.4를 사용해서 만들어졌다.
로컬 컴퓨터에서 버전마다 Lighthouse를 6번씩 실행했다. 결과는 Gatsby가 조금 더 좋았다.
각 프레임워크에서 Lighthouse를 6번씩 실행한 평균 점수 및 시간
Next.js 버전은 전체 성능 점수, FMP, 속도 항목에서 Gatsby보다 약간 뒤처졌었다. 또한 더 높은 수치의 Max Potential First Input Delay를 기록했다.
Chrome 개발자 도구에서 네트워크 패널로 들어가 답을 찾으면, Next.js 버전은 자바스크립트 페이로드를 3개 이상의 청크(생성된 매니페스트 파일은 무시)로 분할했지만, 압축된 페이로드는 20KB로 줄었다. 번들 크기를 줄였음에도 불구하고 이러한 추가 요청이 무거워서 성능에 영향을 줄 수도 있을까?
자바스크립트 성능을 살펴보면, 개발자 도구에서는 Next.js 버전이 첫 번째 페인트에 달성하는 데 300ms가 더 걸렸고 실행 스크립트를 평가하는 시간도 오래 걸렸다고 보여준다. 개발자 도구에서는 이를 "긴 작업(long task)"으로 표시하기도 했다.
어떤 구현 차이로 성능 저하에 원인이 될 수 있는지를 알아보기 위해 두 프로젝트 브랜치를 비교했다. 사용하지 않는 코드를 제거하고 누락된 타입스크립트 타입을 수정하는 것 외에 유일한 변경 사항은 페이지의 특정 부분으로 이동할 때 부드러운 스크롤링을 구현하는 것이었다. 이전에는 gatsby-browser.js
파일에 있었고 동적 import 컴포넌트로 옮겨서 브라우저에서만 실행된다(smooth-scroll npm 패키지를 사용했고, import 하는 시점에 window
객체가 필요하다). 이것이 범인일 수도 있지만, Next.js가 이 기능을 어떻게 처리하는지는 잘 모르겠다.
궁극적으로 Gatsby 버전을 유지하기로 했다. SSG Next.js에 비해 아주 사소한 성능 이점(정말 0.6초 차이가 더 나은가?)을 무시하고, Gatsby 버전에는 이미 더 많은 PWA 기능이 구현되어 있었으며 다시 구현할 시간도 없었다.
처음 Gatsby 버전을 빌드할 때, 더 완벽한 PWA 경험을 만들기 위한 최종 처리 작업을 빠르게 추가할 수 있었다. 페이지별 SEO 메타 태그를 구현하려면 Gatsby의 가이드를 읽어야 했다. PWA 매니페스트를 추가하려면 Gatsby 플러그인을 사용해야 했다. 그리고 다른 모든 플랫폼을 지원하는 파비콘을 구현하는 건 아직도 혼란스럽긴 한데.. 음, 방금 설치한 매니페스트 플러그인의 일부였다. 만세! (역자주: Gatsby의 문서와 플러그인을 통해서 구현이 까다로운 기능들을 쉽게 구현하는 데 도움이 되었다는 의미로 문단이 해석된다)
Next.js 버전에서 이러한 기능들을 구현한다면 튜토리얼과 구현 예시를 찾기 위해 더 많이 구글링하고 특히 Next.js 버전이 성능을 향상시키지 않기 때문에 어떤 이점도 제공하지 못했을 것이다. 또한 Gatsby 버전과 비교했을 때 그 기능들을 비활성화하기로 한 결정적 이유이기도 하다. Next.js의 문서가 더 간결(Gatsby보다 더 간소하다)하기도 하고 게임화된 튜토리얼 페이지를 굉장히 좋아하지만, 한 눈에도 봐도 압도적인 Gatsby의 광범위한 문서와 가이드는 실제로 PWA를 구축하는 데 더 많은 가치를 제공했다.
하지만 Next.js에 감사해야 할 부분도 많긴 하다.
async
함수와 fetch
를 중심으로 이루어지므로 프레임워크를 완벽하게 활용하기 위해 GraphQL까지 배울 필요 는 없다고 생각한다.SSG 지원 검증을 통해 Next.js는 페이지 단위로 SSR, SSG 및 CSR 중에서 쉽게 선택할 수 있는 강력한 프레임워크가 되었다.
사실 이 앱을 완벽하게 정적으로 생성할 수 있었다면, Algolia의 기본 자바스크립트 API를 사용하고 데이터 패칭 코드를 컴포넌트와 함께 같은 파일에서 관리할 수 있기 때문에 Next.js가 더 적합할 것이다. Algolia에는 내장 GraphQL API가 없고 Algolia를 위한 Gatsby 소스 플러그인이 없기 때문에 Gatsby에서 이를 구현하려면 새 파일에 다음 코드를 추가해야 하고 이것은 페이지를 지정할 때 보다 직관적인 선언 방식에 위배된다.
이러한 과정에서 Lighthouse의 성능 점수가 100점에 가까워지도록 더 많은 성능 개선이 이루어졌다.
preconnect
힌트를 추가하는 것을 권장했다(안타깝게도 이메일에 있는 코드 스니펫은 잘못되었고 정확한 코드는 여기에 있다).읽어줘서 고맙다! 최종 프로젝트를 볼 수 있는 링크는 게시하지만 법적인 이유 때문에 공개적으로 공유할 수는 없다.
트위터 @nots_dney로 팔로우할 수 있고, 트위터에 프런트 엔드 엔지니어로서의 내 경험을 더 많이 작성하고 공유할 예정이다.