돌아가기

[번역] 극한의 서버 사이드 렌더링

#Web#HTML#CSS#translate

이 글은 원저자 Sophie의 EXTREME SERVER SIDE RENDERING(2025년 2월 16일)을 허락을 받아 번역한 글입니다.

(일반적인) 서버 사이드 렌더링이란?

극한의 서버 사이드 렌더링(extreme server side rendering, XSSR)에 들어가기 전에, 일반적인 서버 사이드 렌더링(SSR)부터 이야기해야 합니다. 두 가지 방식이 있는데, 여기서는 구식과 신식이라고 부르겠습니다.

구식 SSR에서는 서버가 어떤 로직에 따라 웹 페이지의 HTML을 그때그때 생성합니다. 예를 들어 /users/39에 접속하면 39번 사용자의 정보를 보여줄 수 있습니다. 이 정보는 데이터베이스에서 가져올 수도 있고, 다른 곳에서 가져올 수도 있습니다. 중요한 점은 디스크에 그에 해당하는 39.html 파일이 없다는 것입니다. 백엔드 서버가 HTML을 동적으로 생성합니다. 프런트엔드에서는 페이지를 렌더링하는 데 자바스크립트나 다른 로직이 필요하지 않습니다. 따라서 페이지를 불러오고 나면 동적으로 바꿀 수 없습니다.

신식 SSR은 구식 SSR과 비슷하지만, 프런트엔드에 자바스크립트 로직이 조금 들어갑니다. 보통은 사용자가 웹 페이지와 상호작용하면 자바스크립트가 백엔드에 요청을 보내는 식으로 구현합니다. 그러면 백엔드가 HTML을 반환하고, 프런트엔드의 자바스크립트가 그 HTML을 페이지 어딘가에 삽입합니다. 이렇게 하면 동적인 페이지를 만들 수 있지만, 그 대신 프런트엔드 로직을 어느 정도 유지보수해야 합니다.

더 나은 방법이 있다면 좋지 않을까요?

아주 느린 HTTP 서버

잠깐 옆길로 새서 HTTP 서버 이야기를 해보겠습니다! HTTP 요청을 보내면 실제로 무슨 일이 일어날까요? 서버는 헤더와 실제 HTML 콘텐츠에 해당하는 ASCII 텍스트를 잔뜩 반환합니다. 이 텍스트는 한 글자씩 전송됩니다. 물론 버퍼링이 좀 있기는 하지만, 여기서 하려는 일과는 관련이 없으니 무시해도 됩니다.

HTML을 사람이 입력하는 정도로 천천히 보내면 어떻게 될까요? 아래 영상에서는 제가 netcat으로 인간 HTTP 서버 역할을 하고, 브라우저가 저에게 접속하게 했습니다. netcat은 줄 단위로 버퍼링하므로, 제가 엔터를 누를 때마다 한 줄씩 브라우저로 전송됩니다.

새 HTML이 들어올 때마다 페이지가 바로 렌더링되는 것을 볼 수 있습니다. 멋집니다. 게다가 제가 테스트한 두 브라우저(Firefox와 Chromium) 모두에서 작동합니다. CSS까지 작동하니, 요소를 계속 움직이거나 기존 요소를 숨기고 새 요소를 추가하는 것도 가능하겠습니다. 그러니 HTTP 연결을 무기한 열어 두면, 자바스크립트 없이도 서버에서 페이지를 동적으로 업데이트할 수 있습니다!

사용자 입력 받기

서버에서 페이지를 업데이트하는 것도 멋지지만, 사용자 입력도 받으면 좋겠습니다. 자바스크립트 없이 입력을 받는 일반적인 방법은 <form> 요소를 사용하는 것입니다. 안타깝게도 이 방법은 페이지를 불러와야 하므로 XSSR의 취지가 무색해집니다! 페이지를 다시 불러와도 괜찮다면 일반적인 구식 SSR로 충분합니다. 그런데 제 친구 DJ Chase가 꽤 멋진 요령을 알려줬습니다.


  <iframe style="display: none;" name="transFrame" id="transFrame"></iframe>
  <form target="transFrame" method="POST" action="/action">
    <input type="hidden" name="uuid" value="USER-UUID-GOES-HERE" />
    <input type="Submit" value="" />
  </form>

폼의 대상을 iframe으로 지정하면, 폼을 제출할 때 메인 페이지 대신 해당 iframe을 다시 불러오게 할 수 있습니다. 그 iframe을 숨기면 사용자 입력이 전혀 끊김 없이 이루어집니다!

uuid 값도 짚고 넘어갈 만합니다. 이 값은 페이지를 처음 불러올 때 서버에서 생성할 수 있습니다. 이벤트를 원래 페이지와 연결하는 데 사용하므로, 이벤트에 응답할 때 사용자가 뒤섞이는 것을 방지합니다.

XSSR로 무언가 만들어 보기

좋습니다. 이론상으로는 입력에 반응하는 웹 앱을 만드는 데 필요한 것이 다 갖춰졌습니다. 무엇을 만들면 좋을까요?

플래피 버드 클론은 어떨까요? (소스 코드는 여기에 있습니다.)

여기서 몇 가지 짚어볼 것이 있습니다. 우선, '진짜' 플래피 버드 클론에 필요한 것이 모두 있다는 점을 봐주세요. 점수 텍스트는 동적으로 바뀌고, 파이프는 예상대로 움직이며, 클릭하면 새가 점프합니다. 마지막 기능은 앞서 소개한 iframe 기법에 화면 전체를 덮는 보이지 않는 버튼을 결합해 구현했습니다. 그래서 어디든 클릭할 수 있습니다. 사실 한 번 클릭해서 보이지 않는 버튼을 선택하고 나면, 스페이스 바와 엔터 키로도 계속 클릭할 수 있습니다.

두 번째로, 페이지 로딩이 끝나지 않는다는 점을 봐주세요. 물리 엔진이 전부 백엔드에서 실행되고, 파이프와 새의 위치, 점수 텍스트의 내용을 60 FPS로 업데이트하기 위해 새 CSS를 계속 보내기 때문입니다. 다시 말하지만, 프런트엔드 자바스크립트는 필요 없습니다. 꺼보면 사이트가 똑같이 작동하는 것을 확인할 수 있습니다! 페이지를 curl로 요청해서 끝없이 쏟아지는 CSS의 장관을 감상해도 좋습니다.

생각만큼 나쁘지는 않습니다

성능

사실 클라이언트와 서버 양쪽 모두 성능이 그렇게 나쁘지는 않습니다. Firefox와 Chromium 모두 몇 분이 지나면 사이트가 버벅거리기 시작한다는 점은 인정합니다. 하지만 지금은 실제로 60 FPS로 스트리밍하고 있다는 점을 감안해야 합니다. 예를 들어 사용자가 상호작용한 뒤에만 업데이트하는 페이지라면, 실제로는 문제가 되지 않을 수도 있습니다.

서버 쪽은 더 좋습니다. 제가 Rust로 간단히 구현한 서버는 집에서 운영하는 홈랩(homelab)의 비교적 오래된 서버에서도 코어당 약 500개의 게임을 동시에 실행할 수 있습니다. 이 정도면 적당히 복잡한 Ruby on Rails 앱보다도 훨씬 낫습니다! (애정을 담아 하는 말입니다. 저는 Ruby on Rails를 좋아합니다.) 프레임 레이트를 낮추거나, 사용자와 상호작용한 뒤에만 업데이트하는 페이지에 XSSR을 사용하면 성능을 크게 높일 수 있을 것입니다.

대역폭

대역폭 사용량은… 아주 합리적입니다! 게임 하나를 실행하는 데 약 20KiB/s의 대역폭을 사용합니다. 감을 잡자면, 게임을 약 49초 동안 해야 1MB짜리 자바스크립트 애플리케이션을 불러오는 것과 같은 양을 전송한다는 뜻입니다. (안타깝게도 요즘에는 1MB면 작은 편입니다.) 여기서도 프레임 레이트를 낮추면 크게 개선할 수 있습니다.

지연 시간

아, 이런.

그러니까, 세계 어디에 있느냐에 따라 저 데모가 정말 형편없었을 수도 있습니다. 클릭할 때마다 서버를 왕복해야 하기 때문입니다. 사용자의 행동을 반영해 서버의 물리 엔진을 업데이트한 다음, 새가 이동한 위치를 브라우저에 돌려줘야 합니다. 이때 걸리는 시간은 거의 전적으로 네트워크 전송 시간에 좌우됩니다. 그래서 캐나다 대서양 연안 지역에 있다면, 그러니까 서버가 있는 저희 집과 어느 정도 가까운 곳이라면 데모가 아주 쾌적하게 돌아갑니다. 멀어질수록 점점 더 느려지고요. 동부 해안을 따라 남쪽으로 내려가면 지연이 느껴지지만, 사람이 적응할 수 있는 수준입니다. 조금 일찍 클릭하기만 하면 됩니다. 서부 해안에서는 플레이에 방해가 되고, 지구 반대편에서는 아예 플레이할 수 없습니다. 데모가 별로였고 저와 멀리 떨어진 곳에 살고 있다면, 소스 코드를 복제해 로컬에서 실행해 보기를 강력히 추천합니다.

하지만 요즘 멋 좀 아는 사람들은 다 엣지(edge)에 올리는 걸 좋아하잖아요? 그러니까, 음, 그냥 그렇게 하면 되겠네요. 그럼 상관없겠습니다!

Fedi를 다운시키다

데모가 꽤 자랑스러워서 제 페디버스(Fediverse) 서버(Pleroma를 사용합니다)에 올렸습니다. 얼마 지나지 않아 서버가 다운되면서 게시물을 불러올 수 없게 됐습니다. 약 20분쯤 뒤에 서버가 다시 살아났고, 사람들이 제 게시물을 재게시했다는 알림이 한꺼번에 쏟아졌습니다. 그러더니 또 다운됐습니다! 이런 일이 서너 번 반복된 뒤에야 안정됐습니다. 그 게시물을 본 다른 Pleroma 서버들도 비슷하게 돌아가며 먹통이 됐을 것이라고 추측할 뿐입니다.

알고 보니 일부 애플리케이션은 연결을 무기한 열어 둔 채 데이터를 조금씩 흘려보내는 사이트를 정말 싫어했습니다! 제 친구 Olivia가 Pleroma의 소스 코드를 살펴보고, 서버에서 링크 미리보기를 생성할 때 문제가 생긴다는 것을 알아냈습니다. DoS 공격에 악용할 수 있는 문제라 보안 버그로 신고했고, 빠르게 패치됐습니다.

앞으로 해볼 일

전부 서버에서 실행되는 리액트 포크를 만들면 재미있을 것 같습니다. 가상 DOM을 비교하고, 그 차이를 XSSR로 프런트엔드에 전송할 수 있을 것입니다. 여기서도 프런트엔드 자바스크립트는 필요 없습니다. 그다지 실용적일 것 같지는 않지만, 멋지기는 하겠습니다.

조금 더 쓸모 있을 만한 아이디어로는, 오래된 기기에서 XSSR이 어떻게 작동할지 궁금합니다. 제 생각에는 꽤 잘 작동할 것 같습니다. 인터넷 속도가 느렸다는 점을 생각하면, 데이터가 들어오는 대로 동적으로 렌더링하는 기능을 의도적으로 넣었을 수도 있으니까요. 그렇다면 헤드리스 브라우저(headless browser) 인스턴스로 웹 페이지를 DOM으로 렌더링하는 프록시 서비스를 만들 수 있겠다는 생각이 듭니다. 이 DOM을 XSSR로 프런트엔드에 전송할 수 있을 것입니다. 이렇게 하면 자바스크립트를 전혀 지원하지 않는 오래된 기기에서도 현대적인 웹 페이지를 사용할 수 있을 것 같습니다.


원저자 Sophie는 The Cat House 블로그를 운영하는 소프트웨어 엔지니어입니다.

PREVIOUS ARTICLE

이전 글이 없습니다

NEXT ARTICLE

[번역] 참조 안정성을 타입으로 만들기