가장 처음에 알아볼 최적화 방법은 정말 최신에 나온 Nextjs13
입니다. 사용하신 분들은 아시겠지만 Next12
와는 정말 차원이 다른 프레임워크라고 생각이 됩니다.
제가 요즘 흥미롭게 보고있는 remix
라는 프레임워크에서 많이 따온 컨셉이 많이 보이더군요. 그만큼 next13은 next12와 차이점 그리고 장점이 두드러지게 존재합니다. 하지만 그만큼 잘 알고 사용하지 않는다면 더 안좋은 성능, 안 좋은 개발자 경험을 겪을 수 있다고 생각됩니다.
오늘은 Next13에 대해서 얘기를 다루지만 Next13
을 어떻게 이용하는지에 대한 너무 상세한 내용은 담지 않겠습니다. (이는 추후에 다른 게시물로 작성할 예정입니다. 🙏)
목차
사실 이 최적화 부분은 파트가 나뉘어져 있지만 대부분 Nextjs13
부분에서 최적화를 진행합니다. 나머지는 부수적인 느낌이 강합니다.
추가적으로 Nextjs13의 실험적인 기능은 app
directory를 이용하여 최적화를 진행하였습니다.
사실 이번 성능최적화의 가장 큰 역할을 한 Server Component 입니다.
사실 이 Server Component라는것은 react 18에서 릴리즈될 예정이였지만 아직 안정적이지 않아 출시를 하지 않고 이것을 Vercel에서 맡아서 Nextjs13에 Server Component라는 개념이 나오게 되었습니다.
그래서 이 Server Component가 성능에서 어떠한 도움을 주었을까요?
기존의 react에서 컴포넌트를 불러오는 시점은 사용자가 브라우저에 들어가서 번들링된 파일(ex main.js
)을 다운로드 받고 그 파일이 다운로드가 완료될시에 이제 화면에 그려지게 되는것이죠.
그래서 처음에 불러오는 index.html에는 <div id="root"></div>
밖에 존재하지 않는거죠. 이로 인해 SPA의 가장 큰 단점. 초기 로드 속도가 느리며, 초반 js를 받아올때까지 사용자는 흰화면만을 보게됩니다.
Server Component는 말그대로 서버에서 이 컴포넌트를 렌더링을 합니다. 사실 서버에서 렌더링? 한다면 당연히 생각나는 Nextjs의 기술이 존재하죠. 바로 SSR
입니다. 도대체 이 SSR이 존재하는데 왜 Server Component를 이용하고 어떤 부분에서 더 좋아서 사용하는 걸까요?
SSR과 RSC(React Server Component)는 언뜻보면 비슷해보이지만 실제로는 바라보는 목적과 그 동작이 다릅니다.
RSC는 SSR에서의 top level 페이지 단위로 강제되는 서버 사이드 렌더링을 컴포넌트 단위로 가능하게 만듭니다. 예를 들어 서버사이드 렌더링에서는 어떠한 페이지에 하나라도 실시간 데이터가 있다면 그 페이지는 ssg를 이용하고 싶어도 이용하지 못한다는 뜻입니다. 강제로 그 페이지는 ssr로 이용을 할수 밖에 없죠.
하지만 서버 컴포넌트 같은 경우에는 각각의 컴포넌트가 서버에서 호출을 하고 서버에서 렌더링을 하기 떄문에 컴포넌트 별 csr, ssr, ssg, isr 전략을 세우는 것이 가능하며 컴포넌트 레벨에서 fetch를 사용을 할 수 있습니다.
RSC는 실제 client 번들에 들어가지 않습니다. 사실 서버 컴포넌트의 가장 큰 강점이라고 할 수 있습니다. SSR은 실제로 모든 코드가 javascript내로 들어가며 처음에 html을 브라우저에 제공하여 lcp, fcp등이 굉장히 빨라지지만 TTI(Time To Interactive) 시간은 기존 spa와 동일하기 때문에 여전히 사용자가 상호 작용하기 까지는 불편함이 있습니다.
하지만 RSC는 말그대로 서버에서만 부르는 컴포넌트이며 아예 번들에 포함되지 않고 json형식으로 직렬화된 정보를 브라우저에 제공할 뿐입니다. 그래서 실제로 SSR을 사용했을때보다 훨씬 작은 번들크기를 가지며 이로 인하여 매우 빠른 TTI 시간을 가지게 됩니다.
client side에서 fetch할때 발생하는 waterfall 현상이 존재하지 않습니다. 기존 fetch를 여러개 하여 오래 걸리는 api가 있다면 waterfall 때문에 blocking되는 현상이 발생하는데요.
server에서는 이러한 문제가 없기때문에 서버에서 호출하고 서버에서 html을 그리는 서버 컴포넌트는 waterfall 현상이 일어나지 않습니다.
실제로 확인하면 처음에 받아오는 크기가 86kb밖에 되지 않습니다.
저는 fetch를 사용하여 데이터 리스트를 보여주는 모든곳에는 server component를 이용했습니다. 그리고 그 데이터를 통하여 state관리 혹은 swiper 라이브러리를 통하여 슬라이드 기능을 추가할때는 부모나 자식 컴포넌트를 client component로 만들어서 클라이언트 로직을 위임하였습니다.
server component를 사용하면 내부에서 useState, useEffect, custom hook등 아무것도 이용하지 못하기 때문에 조금 더 관심사의 분리와 어떻게든 사용할 수 있도록 할 수 있는 전략이 존재하는데 이는 다른 게시물에서 다루도록 하겠습니다.
다음 얘기 해볼것은 ISR (Incremental Static Regeneration) 입니다.
이것은 SSR의 문제점을 해결한 SSG의 문제점을 해결한 렌더링 전략이라고 할 수 있습니다. 😅😅
SSR
의 매 요청마다 서버에 요청을 하여 그려 서버의 부담이 크다라는 문제점을 정적 사이트 생성기인 SSG
가 해결하였고 빌드 할떄만 데이터의 변경이 적용되어서 개인 블로그같은 서비스가 아니면 이용하기 어렵다는 문제점을 ISR
이 해결을 해주었죠.
ISR은 SSG와 동일하게 빌드타임에 정적인 페이지를 생성을 하고 몇초마다 그것을 재갱신 하는 동작을 말합니다. 예를들어 revalidate를 30초를 준다면 30초마다는 새로운 데이터를 갱신하는 것이죠.
Next12까지는 페이지 단위별로 ssg, isr, ssr, csr을 관리해야되었지만 Next13에서는 컴포넌트별로 관리할 수 있습니다. 바로 remix에서 이용하는 fetch의 캐시 전략을 가져와서 ssr처럼, ssg처럼 이용할 수 있습니다.
fetch(url, { cache: 'force-cache' }) // ssg
fetch(url, { cache: 'no-store' }) //ssr
fetch(url, { next: { revalidate: 60 }}) // 60초 마다 isr
next13에서 컴포넌트 단위로 하는 isr은 동작하지 않습니다.. 🥲🥲
말그대로 아직 실험적인 기능이기 때문이죵.. 현재 nextjs13에서 isr이 동작하려면 다음과 같은 동작 뿐입니다.
dynamic='force-static'
,revalidate=<시간>
이런식으로 이용해야 합니다.즉 아직 컴포넌트 단위로 isr을 이용할 수 는 없습니다. 하지만 server component로 이용한것은 isr로 동작하고 client component는 실시간으로 불러올 수 있기에 하나라도 실시간 데이터가 있을떄 ssg를 이용못하던 그런 문제는 해결할 수 있습니다.
저는 이번에 3분마다 revalidate하는 옵션으로 정적인 어드민 데이터들을 isr
로 실시간 예약 데이터는 csr
로 처리하도록 진행했습니다.
nextjs에서도 ssg를 사용할수 있으면 ssg를 사용하라고 합니다.
ssr도 빠르기는 하지만 기본적으로 요청을 할떄마다 서버에 부담이 생긴다는게 가장 큰 문제겠죠. 그래서 혼자서 로컬에서 돌려보는데는 적합하지만 어느정도 트래픽이 들어오는 서비스에서는 ssr이라는것은 꽤나 부담을 많이 주는 작업입니다.
오히려 트래픽이 늘어나면 csr보다 느려질 수 있다는 단점도 존재하죠. 믈론 load balancing
을 이용하여 최적화 할수는 있지만 서버를 최적화하는 것도 모두 비용이기에 client단에서 처리할 수 있으면 처리하는게 베스트입니다.
isr을 이용하면 3분마다 요청을 단 한번씩 받고 데이터를 갱신하는게 전부입니다. http web cache가 아니기 때문에 특정 사용자에게 종속된 캐시가 아니라 서버 캐시이기때문에 모든 사용자에게 3분마다 달라진 ui를 보여줍니다.
그래서 100만명이 들어오더라도 실제 호출은 3분에 한번씩 가기에 성능적으로 훨씬 좋을수밖에 없습니다.
다들 nextjs image 최적화보면 당연하듯이 next/image
를 사용할것입니다. 하지만 next/image
는 이미지를 어떻게 최적화를 해줄까요?
next/image는 lqip(low quality image placeholder), file formatiing (jpg -> avif/webp), image resizing, lazy loading, responsive image
등등 정말 많은 것을 최적화 해줍니다.
사실 사용만 하면 알아서 최적화되는 마법의 문법같지만 도대체 저 작업들을 어디서 할까? 라고 생각하면 next/image의 사용이 부담스러워질 수 있습니다.
바로 이 next/image들이 이 작업을 하는것들은 모두 서버에서 하는 작업입니다.
메인 화면에 많은 이미지들이 존재할텐데 사용자가 들어올떄마다 next/image
가 서버에서 이미지를 최적화 해준뒤 브라우저에 서빙을 한다..? 사용자가 많지 않은 서비스면 큰 문제가 없겠지만 사용자가 많은 서비스라면 이 next/image
사용은 서버의 메모리를 모두 잡아먹는 역할을 할겁니다.
next image를 사용하시는 분들은 한번 부하테스트를 해보시길 바랍니다. 거기에 ssr까지 이용하고 있다면 더더욱 해봐야겠죠?🙂🙂
그렇기 때문에 이러한 next/image가 하는 작업들을 cloudinary와 같은 cdn, 각종 태그및 속성을 이용하여 구현할 수 있는데 그 부분은 2편인 이미지 최적화에서 보실수있습니다.
이것은 LightHouse 점수에 들어가는 요소는 아니지만 사용자 경험이 매우 향상이 됩니다.
그런데 진짜같은 Skeleton UI란 뭘까요..?
그 전에 Server Component의 단점(?)을 말씀 드릴까 합니다.
Server Component를 이용하면 이미 html에 그려진것 상태로 로딩이라는것이 존재하지 않은채 바로 우리 화면에 보여지게 됩니다. 하지만 여기서 client Component를 섞어쓴다면?
너무 빠른 Server Component에 비해서 js가 로드되야 나오는 client Component는 실제 렌더링이 될시 layout shift(CLS)가 발생할 수 밖에 없습니다.
보통 이러한 layout shift를 막기 위해서 Skeleton UI를 이용하는데요. 기존에 우리가 알고있는 Skeleton UI는 회색으로 로딩이 되고있는것 같은 UI를 많이 보여주는데요.
client component로 생기는 cls는 회색 UI가 아닌 Server component로 처리할 수 있습니다.
사실 client component를 이용하는 이유는 그 컴포넌트내에 onClick, useState, useEffect등이 존재하기 때문에 어쩔수 없이 client Component를 이용할 수 밖에 없습니당.
그래서 이것을 이용하여 이벤트 핸들러, useEffect, useState를 뺀 데이터 페칭 & UI만 존재하는 Skeleton UI를 만들어 버리면 껍데기뿐인 UI는 server component와 함께 로드되고 그 뒤에 js가 로드될시 로직이 첨가되는 것이죠.
<header>
<div>할룽</div>
<p>클릭</p>
</heaader>
Server Component
'use client'
<header>
<div>할룽</div>
<p onClick={() => handleClick('click')}>클릭</p>
</heaader>
Client Component
cdn
과 html tag
를 이용해서 next/image 처럼 비슷하게 구현할 수 있습니다.
Next JS 를 공부하고 있는데, 마침 좋은 포스트가 떴네요. 글 재밌게 잘 읽었습니다!