← 염승준
08

페이지별 렌더링 전략 재배분

캐시 속도인가, 데이터 신선도인가. 그리고 서버가 아니라 폰에 남아 있던 캐시.

문제
  • 거의 모든 페이지가 getInitialProps로 SSR이라 캐시로 충분한 페이지까지 매 요청 서버가 새로 그림
  • 첫 로딩 때 내려받는 번들도 큼
해결
  • "캐시 속도인가, 데이터 신선도인가"를 기준으로 32개 페이지를 재분류, SSR이 필요 없는 23개는 정적 최적화로 전환
  • "지금 당장 보는가"를 기준으로 모달·숨은 탭 등은 dynamic import, 로딩 중엔 스켈레톤
  • 메인은 ISR 적용 후 옛 화면(이미 내려간 광고)이 보여 서버 인스턴스별 캐시로 보고 SSR로 되돌렸으나 해결되지 않음. 실기기 추적 결과, 두 방식 모두 비워 둔 max-age를 Cloud CDN client TTL(366일)이 채워 폰 브라우저에 옛 HTML이 고정된 것. max-age=0을 빠뜨릴 수 없는 헬퍼와 CI 검사를 처방으로 정함
결과
  • /setting 페이지 번들 -403kB(-37.0%), 전체 평균 번들 -16.7%. 서버 부하와 TTFB 감소
  • 되돌린 대가(뒤로가기 시 약 2.6초 빈 화면)는 알고 감수하고, 이유를 코드 주석으로 남김
SSR을 걷어낸 페이지
32개 중 23개
/setting 페이지 번들
-403kB (-37.0%)
전체 페이지 평균 번들
-16.7%

문제

거의 모든 페이지가 getInitialProps를 통해 SSR로 동작하고 있었습니다. 캐시로 충분한 페이지까지 매 요청 서버가 새로 그렸고, 첫 로딩 때 내려받는 번들도 컸습니다.

기준 하나로 재배분

"이 페이지가 원하는 게 캐시 속도인가, 데이터 신선도인가"를 기준으로 전체 32개 페이지를 다시 나눴습니다.

그중 SSR이 필요 없는 페이지가 23개였습니다. 소개 페이지처럼 모든 사용자에게 같은 정적 콘텐츠를 보여 주는 페이지, 그리고 화면의 뼈대는 정적이고 사용자 데이터만 클라이언트에서 가져와도 충분한 페이지들입니다. 이 페이지들에서 SSR을 걷어내 Next.js의 Automatic Static Optimization이 동작하게 했습니다. 서버는 데이터 조회를 기다리지 않고 미리 빌드된 정적 파일을 바로 내려주게 되어, 서버 부하와 첫 응답 시간(TTFB)이 줄었습니다. 나머지 페이지도 성격에 따라 ISR·SSR·CSR 중 하나로 정리했습니다.

지금 보이지 않는 건 나중에

같은 기준을 컴포넌트에도 적용했습니다. "사용자가 지금 당장 보는가?"를 묻고, 버튼을 눌러야 열리는 모달이나 다른 탭 뒤에 숨은 패널처럼 첫 상호작용에 필요 없는 UI는 dynamic import로 뒤로 미뤘습니다. 가장 무거웠던 /setting 페이지 번들이 403kB(37.0%) 줄었고, 전체 페이지 평균 번들은 16.7% 줄었습니다.

코드를 나누면 늦게 도착한 조각이 스타일 없이 잠깐 드러나거나 레이아웃이 흔들릴 수 있습니다. 그래서 로딩 중에는 스켈레톤 UI로 자리를 잡아 두었습니다.

메인은 되돌렸지만, 원인은 다른 곳에 있었다

메인은 처음 판단이 틀렸던 페이지입니다.

메인에 ISR을 적용하자 일부 사용자에게 옛 화면이 계속 보였습니다. 메인은 광고가 HTML에 함께 구워지는 페이지라, 옛 화면은 곧 이미 내려간 광고의 노출이었습니다. 처음에는 서버 인스턴스마다 캐시가 따로 남는 문제로 보고 SSR로 되돌렸지만, 해결되지 않았습니다.

그래서 서버, CDN 캐시, 서비스 워커, 앱 코드를 차례로 배제하고 실기기에서 추적했습니다. adb와 Chrome DevTools Protocol로 앱을 처음 켤 때의 문서 응답을 캡처하니, 문서는 네트워크가 아니라 폰의 디스크 캐시에서 나오고 있었습니다(fromDiskCache=true). 응답 헤더에는 코드에 없는 max-age=31622400이 붙어 있었습니다.

원인은 제 실수였습니다. ISR일 때도, SSR로 되돌린 뒤 붙인 CDN 캐시 헤더(public, s-maxage=60, stale-while-revalidate=300)에서도 제가 브라우저용 max-age를 비워 뒀습니다. Cloud CDN의 client TTL(366일)이 그 빈칸을 채웠고, 폰 브라우저는 옛 메인 HTML을 1년짜리 신선한 사본으로 보관하고 있었습니다. 렌더링 방식을 바꿔도 빈칸은 그대로였으니, SSR로 되돌려도 해결되지 않았던 것입니다.

일부 사용자만 겪은 데도 이유가 있었습니다.

처방은 코드 쪽에 두기로 했습니다.

같은 처방을 다른 페이지에도 적용하는 중입니다. 팀 규칙도 하나 생겼습니다. SSR 페이지를 CDN에 태울 때 s-maxage와 max-age=0은 반드시 짝으로 둡니다.