Tech

정적 생성 대신 실제 브라우저로 프리렌더링한 이유

2026.08.26오토포커스

홈페이지를 검색엔진이 읽게 만들려다 SSG를 포기했습니다. 앱을 고치는 대신 진짜 크롬을 띄워 결과 화면을 떠내는 쪽을 택한 기록입니다.

React로 만든 사이트는 처음 응답하는 HTML이 거의 비어 있습니다. 브라우저가 자바스크립트를 실행한 뒤에야 본문이 채워지죠. 검색엔진도 요즘은 자바스크립트를 실행하지만, 실행을 기다리는 것과 처음부터 글이 있는 것은 다릅니다. 링크를 공유했을 때 미리보기를 만드는 스크래퍼들은 아예 자바스크립트를 실행하지 않습니다.

그래서 빌드 시점에 각 페이지의 HTML을 미리 만들어 두기로 했습니다. 문제는 방법이었습니다.

SSG를 포기한 지점

일반적인 선택은 정적 생성(SSG)입니다. Node에서 컴포넌트를 렌더해 HTML을 뽑는 방식이죠. 이 방식은 앱 전체가 "브라우저 없이도 실행 가능"해야 합니다. 그런데 저희 코드는 그렇지 않았습니다.

  • 스크롤 애니메이션 플러그인이 다섯 개 파일의 모듈 최상단에서 등록됩니다. import만 해도 실행됩니다.
  • 데모 화면은 canvas 2D를, 3D 씬은 WebGL을 씁니다. 둘 다 Node에 없습니다.
  • 테마·언어 설정은 localStorage에 저장됩니다. 이것도 Node에 없습니다.

전부 "브라우저에서만 실행" 가드로 감싸고, 앱 진입점을 라우트 객체 구조로 다시 쓰면 됩니다. 되긴 됩니다. 다만 그 개편의 목적이 SEO 하나이고, 개편 범위가 앱 전체라는 점이 걸렸습니다. 가드를 하나 빠뜨리면 빌드가 깨지고, 새 파일을 만들 때마다 같은 규칙을 기억해야 합니다.

진짜 브라우저를 띄우는 쪽

반대로 접근했습니다. 앱을 Node에 맞추는 대신, 빌드 과정에 실제 크롬을 띄워 앱을 실행하고 그 결과 DOM을 떠내는 것입니다. 브라우저에서 돌아가는 코드는 브라우저에서 그대로 돌아가니, 클라이언트 전용 가드가 한 줄도 필요 없습니다.

앱을 빌드 환경에 맞추지 않고, 빌드 환경을 앱에 맞췄습니다.

대가는 빌드 시간입니다. 8.7초에서 33초가 됐습니다. 배포 파이프라인에서 25초는 지불할 만한 값이었습니다.

실제로 걸린 함정 세 가지

“그냥 스크린샷 찍듯 DOM을 떠내면 된다”가 아니었습니다. 세 번 고쳤습니다.

첫째, 스크롤 등장 애니메이션. 요소를 투명도 0으로 깔아 두고 스크롤이 닿을 때 나타내는 방식이라, 스크롤하지 않고 캡처하면 본문이 숨겨진 채로 굳습니다. 페이지를 끝까지 훑어 내린 뒤 캡처해야 합니다.

둘째, 훑고 나서 다시 맨 위로 돌아와야 합니다. 스크롤 위치에 따라 내용이 바뀌는 화면이 있어서, 훑고 온 상태로 캡처하면 기본값이 아닌 항목이 열린 채로 저장됩니다.

셋째, 배경 영상. 영상은 첫 화면을 그린 뒤에 붙도록 지연 로딩해 두었는데, 프리렌더는 그 "이후" 상태를 뜨기 때문에 영상 태그가 정적 HTML에 그대로 박힙니다. 그러면 브라우저가 HTML을 읽는 순간 수 MB를 내려받기 시작해 지연 로딩이 통째로 무효가 됩니다. 캡처 직전에 영상 태그를 제거하는 것으로 해결했습니다. 앱이 부팅되면 원래 로직대로 다시 붙습니다.

가장 오래 못 찾은 버그

React가 렌더한 <head> 태그와, 정적 HTML에 원래 있던 폴백 태그가 중복으로 남는 문제였습니다. 특히 canonical 주소가 겹쳤습니다. 프리렌더되지 않은 경로가 홈의 canonical을 물려받아, 검색엔진에 "이 페이지는 홈의 중복본"이라고 신고하고 있었습니다.

정적 HTML의 폴백 태그에 표시를 남겨 두고, 앱이 부팅되면 그 표시가 붙은 태그를 지우게 했습니다. 프리렌더되지 않는 경로는 아예 빈 셸을 따로 두어 홈의 메타를 물려받지 않게 했습니다.

정리하면, 프리렌더링에서 어려운 부분은 HTML을 만드는 일이 아니었습니다. 만들어진 HTML이 앱이 부팅된 뒤와 어긋나지 않게 맞추는 일이었습니다.

플랫폼 시작하기딜러십 운영에 필요한 모든 모듈을 한 곳에서