![[실전 성능 12] 첫 화면을 서버가 그린다](https://img.thenullpage.com/posts/5766/5766_1_814d20.webp)
지난 편에서 엣지 캐시로 첫 바이트를 앞당겼다. 그런데 첫 바이트가 빨라도 그 안에 알맹이가 없으면 소용없었다. 서버가 빈 껍데기만 빨리 보내고 정작 콘텐츠는 나중에 채워지면, 사용자는 여전히 빈 화면을 봤다. 이번 편은 서버가 첫 화면을 미리 완성해서 보내는 서버 렌더링과, 클라이언트에서 그릴 때 빠지기 쉬운 함정에 대한 이야기다.
1. 빈 껍데기의 문제
초기에 나는 첫 응답으로 거의 빈 문서를 보내고, 브라우저가 스크립트를 실행해 콘텐츠를 채우게 했다. 이 방식은 개발이 편했지만 성능에 문제가 있었다. 사용자가 받는 첫 문서에는 실제 내용이 없어서, 스크립트가 다운로드되고 실행되어 콘텐츠를 그릴 때까지 빈 화면을 봐야 했다. 첫 바이트는 빨라도 첫 콘텐츠는 늦었다.
이 지연은 특히 느린 기기에서 두드러졌다. 빈 문서를 받은 뒤 스크립트를 실행해 콘텐츠를 구성하는 과정이 기기의 처리 능력에 달렸는데, 약한 기기는 이 과정이 오래 걸렸다. 좋은 기기에서는 순식간이라 문제를 못 느꼈지만, 느린 기기 사용자는 한참 빈 화면을 봤다. 내 화면만 보면 놓치는 전형적인 함정이었다.
검색 엔진이나 미리보기를 만드는 곳에서도 문제가 됐다. 이들은 빈 문서를 받아 스크립트를 실행하지 않으면 콘텐츠를 못 봤다. 실제 내용이 문서에 없으니 페이지가 비어 있는 것처럼 취급될 위험이 있었다. 콘텐츠를 제대로 알리려면 문서 자체에 내용이 담겨 있어야 했다. 이건 성능뿐 아니라 노출의 문제이기도 했다.
그래서 나는 접근을 바꿨다. 브라우저가 콘텐츠를 그리게 하는 대신, 서버가 미리 콘텐츠까지 채운 완성된 문서를 보내기로 했다. 그러면 사용자는 첫 문서를 받는 즉시 실제 내용을 볼 수 있었다. 스크립트 실행을 기다릴 필요가 없으니 첫 콘텐츠가 빨라졌다. 서버 렌더링이라 부르는 이 방식으로 방향을 틀었다. 개발 편의를 위해 택했던 빈 껍데기 방식이 사용자 경험에는 손해였다는 걸 인정하는 전환이었고, 나는 이 결정으로 개발자의 편의와 사용자의 경험이 충돌할 때 어느 쪽을 택해야 하는지 기준을 세웠다.
2. 서버가 미리 그린다는 것
서버 렌더링은 서버가 사용자에게 보낼 문서를 완성된 형태로 만드는 것이었다. 콘텐츠를 조회하고, 그것을 화면 구조에 채워 넣어, 실제 내용이 담긴 문서를 만들어 보냈다. 브라우저는 이 문서를 받자마자 그릴 수 있었다. 알맹이가 이미 들어 있으니 스크립트 실행을 기다리지 않고도 첫 화면이 떴다.
이 방식의 큰 장점은 첫 콘텐츠가 빠르다는 것이었다. 사용자는 문서가 도착하는 즉시 읽을 내용을 봤다. 느린 기기에서도 서버가 이미 그려서 보냈으니 기기 성능에 덜 좌우됐다. 무거운 구성 작업을 사용자 기기가 아니라 서버가 대신 해준 셈이었다. 약한 기기 사용자에게 특히 고마운 방식이었다. 서버는 성능이 일정하고 강력하지만 사용자 기기는 천차만별이라, 무거운 일을 서버로 몰아주면 가장 약한 기기를 쓰는 사람도 비슷한 첫 화면 속도를 누릴 수 있었다.
엣지 렌더링과 결합하니 효과가 배가됐다. 앞 편에서 다룬 엣지에서 서버 렌더링을 하면, 사용자 가까운 지점이 완성된 문서를 만들어 보냈다. 거리도 짧고 알맹이도 들어 있으니 첫 바이트와 첫 콘텐츠가 모두 빨랐다. 그리고 그 완성된 문서를 캐시해두면 다음 사용자는 더욱 빨랐다. 여러 기법이 층층이 쌓여 시너지를 냈다.
서버 렌더링은 콘텐츠를 문서에 담으니 노출 문제도 함께 풀었다. 검색 엔진이나 미리보기 도구가 문서만 봐도 실제 내용을 파악할 수 있었다. 성능을 위해 택한 방식이 노출에도 도움이 되는 반가운 경우였다. 나는 사용자에게 중요한 공개 콘텐츠일수록 서버 렌더링으로 확실히 내용을 담아 보냈다.
3. 클라이언트 렌더링의 함정
서버 렌더링이 만능은 아니었다. 어떤 화면은 사용자 조작에 따라 계속 바뀌어야 해서, 브라우저에서 그리는 클라이언트 렌더링이 더 맞았다. 문제는 이 방식에 성능 함정이 숨어 있다는 점이었다. 특히 앞서 다룬 시각 안정성을 무너뜨리기 쉬웠다. 나는 이 함정에 몇 번 빠지고 나서야 조심하게 됐다.
대표적인 함정이 콘텐츠가 나중에 끼어들며 화면을 미는 것이었다. 빈 화면을 먼저 그리고 데이터가 오면 채우는 방식은, 데이터가 도착하는 순간 레이아웃이 확 바뀌며 흔들렸다. 처음엔 아무것도 없다가 갑자기 목록이 나타나며 아래가 밀리는 식이었다. 클라이언트 렌더링은 이런 뒤늦은 채움 때문에 안정성이 나빠지곤 했다.
또 다른 함정은 반응성이었다. 브라우저에서 화면을 구성하는 동안 처리 통로가 바빠서, 그 시점에 사용자 입력이 늦게 처리됐다. 화면은 그려지는 중인데 눌러도 반응이 없는 순간이 생겼다. 클라이언트 렌더링은 편리한 만큼 이런 성능 대가를 치를 수 있었다. 나는 이 방식을 쓸 때 안정성과 반응성을 특히 신경 썼다.
함정을 피하는 방법은 콘텐츠가 올 자리를 미리 잡아두는 것이었다. 데이터가 오기 전에도 그만한 크기의 빈 틀을 그려두면, 실제 데이터가 채워져도 레이아웃이 안 밀렸다. 앞서 여러 번 나온 자리 예약 원리가 여기서도 통했다. 빈 화면 대신 콘텐츠 모양의 틀을 먼저 보여주니, 흔들림도 줄고 로딩 중이라는 느낌도 자연스러웠다.
4. 두 방식을 나눠 쓰기
결국 서버 렌더링과 클라이언트 렌더링은 어느 하나가 정답이 아니라 상황에 맞게 나눠 쓸 도구였다. 첫 화면처럼 빨리 보여주고 노출도 중요한 부분은 서버 렌더링으로 완성해 보냈다. 반면 사용자 조작에 따라 실시간으로 바뀌는 부분은 클라이언트에서 그리게 했다. 각자 잘하는 영역이 달랐다.
이상적인 조합은 서버가 완성된 첫 화면을 보내 사용자가 즉시 내용을 보게 하고, 그다음 브라우저가 조용히 이어받아 이후의 상호작용을 처리하는 것이었다. 첫 화면의 속도와 이후의 유연함을 모두 챙기는 방식이었다. 사용자는 빠르게 내용을 보고, 이후 조작에도 매끄럽게 반응받았다. 두 방식의 장점을 이어 붙인 셈이었다.
이 조합에서 주의할 건 이어받는 과정이 매끄러워야 한다는 점이었다. 서버가 그린 화면과 브라우저가 이어받은 화면이 어긋나면 깜빡이거나 흔들렸다. 나는 서버가 그린 것과 브라우저가 그릴 것이 일치하도록 맞췄다. 이음새가 안 보여야 사용자가 두 방식이 섞인 걸 눈치채지 못했다. 자연스러운 연결이 핵심이었다.
어느 방식을 쓸지 판단할 때 나는 그 화면의 성격을 봤다. 자주 안 바뀌고 많은 사람이 보는 공개 콘텐츠는 서버 렌더링과 캐시가 어울렸고, 사용자마다 다르고 자주 바뀌는 부분은 클라이언트 렌더링이 맞았다. 성격에 맞는 방식을 고르니 성능과 편의를 모두 챙길 수 있었다. 획일적으로 밀어붙이지 않는 것이 중요했다. 모든 걸 서버 렌더링으로 하면 상호작용이 뻣뻣해지고, 모든 걸 클라이언트 렌더링으로 하면 첫 화면과 안정성이 무너졌으니, 두 극단 사이에서 화면마다 저울질하는 감각이 필요했다.
5. 알맹이를 빨리 보여주기
이 편의 핵심은 결국 알맹이를 빨리 보여주는 것이었다. 첫 바이트가 빨라도 알맹이가 늦으면 사용자는 여전히 기다렸다. 서버 렌더링은 그 알맹이를 문서에 미리 담아 첫 바이트와 함께 도착하게 했다. 빈 껍데기가 아니라 읽을 내용이 즉시 뜨니, 사용자가 체감하는 로딩이 크게 빨라졌다.
이 작업을 하며 성능이 여러 층위의 협력이라는 걸 다시 느꼈다. 엣지로 거리를 줄이고, 캐시로 조회를 없애고, 서버 렌더링으로 알맹이를 담고, 자리 예약으로 흔들림을 막았다. 각 층위가 자기 몫을 하면서 합쳐지니 첫 화면 경험이 확연히 좋아졌다. 하나의 마법이 아니라 여러 조치의 합이었다.
그리고 이 모든 판단의 바탕에는 역시 측정이 있었다. 서버 렌더링으로 바꾼 뒤 첫 콘텐츠 시간이 정말 빨라졌는지, 클라이언트 렌더링 부분이 안정성을 해치지 않는지 재서 확인했다. 방식을 바꾸는 큰 결정일수록 숫자로 검증했다. 좋아 보이는 변경이 실제로도 좋은지는 재봐야만 알 수 있었다.
이렇게 응답을 가까이서, 빠르게, 알맹이까지 담아 보내는 법을 다뤘다. 다음 편에서는 이 캐시를 여러 층으로 쌓고, 무거운 계산을 미리 해두는 캐시 계층 전체를 정리하겠다. 그리고 캐시가 편리한 만큼 따라오는 무효화와 보안 함정도 함께 다룬다. 앞서 잠깐 언급한 그 위험을 이번엔 정면으로 파헤친다.