[실전 성능 10] 코드가 너무 많다

지난 편에서 렌더링을 막는 자원을 미루는 법을 다뤘다. 그런데 미루는 것에는 한계가 있었다. 결국 브라우저가 처리해야 할 코드의 총량이 너무 많으면, 아무리 순서를 조정해도 어딘가에서 부담이 됐다. 이번 편은 그 총량 자체를 줄이는 이야기, 즉 번들 크기와 코드 분할이다. 미루기의 다음 단계라 할 수 있다.


1. 번들이 자꾸 커지는 이유

사이트를 만들다 보면 자바스크립트 코드가 하나로 뭉쳐진 큰 덩어리, 즉 번들이 생긴다. 처음엔 작던 이 번들이 기능을 추가할수록 조용히 커졌다. 편의를 위해 이런저런 라이브러리를 가져다 쓰고, 새 기능을 붙이다 보면 어느새 번들이 무거워져 있었다. 사용자는 페이지를 열 때마다 이 큰 덩어리를 다운로드하고 브라우저는 그걸 다 해석해야 했다.


번들이 크면 두 군데서 비용이 났다. 하나는 다운로드였다. 파일이 크면 특히 느린 회선에서 받는 데 시간이 걸렸다. 다른 하나는 해석과 실행이었다. 다운로드가 끝나도 브라우저가 그 코드를 해석하고 실행하는 데 처리 능력을 썼고, 약한 기기일수록 이 부담이 컸다. 큰 번들은 로딩과 반응성을 동시에 갉아먹었다.


특히 문제였던 건 사용자가 당장 안 쓰는 코드까지 다 받는다는 점이었다. 사이트의 모든 기능에 필요한 코드가 하나의 번들에 들어 있으면, 첫 화면만 보려는 사용자도 잘 안 쓰는 기능의 코드까지 통째로 받았다. 대부분 페이지에서 실제로 쓰이는 코드는 그중 일부인데 전부를 받는 건 큰 낭비였다. 나는 이 낭비를 줄이고 싶었다.


외부 라이브러리도 번들을 키우는 흔한 원인이었다. 작은 기능 하나를 위해 큰 라이브러리를 통째로 가져오면, 정작 쓰는 건 일부인데 전체가 번들에 실렸다. 나는 라이브러리를 가져올 때 그 크기를 확인하고, 작은 기능이라면 직접 구현하거나 가벼운 대안을 찾았다. 편리함의 대가가 성능일 때가 많았다. 몇 줄이면 될 일을 위해 수십 배 무거운 라이브러리를 통째로 지고 가는 건 어리석은 거래였고, 나는 가져오기 전에 그 무게를 반드시 저울에 올려보게 되었다.


2. 필요한 만큼만 나눠 보내기

해법의 핵심은 번들을 잘게 나누는 것이었다. 하나의 큰 덩어리 대신 여러 조각으로 나눠서, 지금 필요한 조각만 받게 하는 방식이다. 첫 화면에 필요한 코드와 특정 기능에서만 쓰는 코드를 분리하니, 사용자는 당장 필요한 것만 다운로드하면 됐다. 코드 분할이라 부르는 이 방식으로 초기 부담을 크게 줄였다.


예를 들어 글을 쓸 때만 필요한 편집기 코드는 글쓰기 화면에 갔을 때만 불러오게 했다. 목록을 구경하러 온 사용자는 편집기 코드를 아예 받지 않았다. 특정 상황에서만 쓰는 무거운 기능일수록 이렇게 분리하는 효과가 컸다. 필요할 때 불러오니 초기 로딩은 가벼워지고, 그 기능을 쓰려는 순간에만 잠깐의 로딩이 있었다.


이렇게 나누면 자주 안 바뀌는 코드와 자주 바뀌는 코드를 분리하는 이점도 있었다. 라이브러리처럼 잘 안 바뀌는 부분을 따로 두면, 내 코드를 수정해 배포해도 그 부분은 사용자 브라우저에 캐시된 걸 재사용했다. 매번 전체를 다시 받는 게 아니라 바뀐 조각만 받으니 재방문이 빨랐다. 분할이 캐시 효율까지 높인 것이다.


다만 너무 잘게 쪼개는 것도 능사는 아니었다. 조각이 지나치게 많으면 각각을 요청하는 부담이 늘어 오히려 손해였다. 나는 의미 있는 단위로 나누되 지나친 분할은 피했다. 첫 화면, 주요 기능, 부가 기능 정도로 큰 덩어리를 가르는 선에서 균형을 잡았다. 분할도 적정선이 있었다. 무엇이든 극단으로 밀면 부작용이 생긴다는 걸 여기서도 확인했는데, 나누는 것 자체가 목적이 아니라 사용자가 받는 총량과 요청 횟수를 함께 줄이는 것이 진짜 목적이었다.


3. 안 쓰는 코드 걷어내기

나누기 전에 애초에 안 쓰는 코드를 걷어내는 것도 중요했다. 개발을 하다 보면 한때 쓰다가 지금은 안 쓰는 코드가 번들에 계속 남아 있곤 했다. 이런 죽은 코드는 아무 기능도 안 하면서 번들만 키웠다. 나는 주기적으로 실제로 쓰이는지 확인하고 안 쓰는 것을 제거했다. 없애도 되는 코드가 생각보다 많았다.


라이브러리에서도 안 쓰는 부분을 덜어낼 수 있었다. 큰 라이브러리라도 실제로 쓰는 기능만 골라 담으면 번들이 훨씬 작아졌다. 전체를 가져오는 대신 필요한 부분만 가져오도록 방식을 바꾸니, 같은 기능을 유지하면서 크기가 줄었다. 도구가 이런 걷어내기를 자동으로 해주기도 해서, 나는 그 도구를 적극 활용했다.


중복된 코드도 번들을 키우는 원인이었다. 여러 곳에서 비슷한 일을 하는 코드가 각각 들어 있으면 그만큼 커졌다. 나는 반복되는 부분을 공통으로 묶어 한 번만 담기게 정리했다. 코드를 깔끔하게 유지하는 것이 곧 번들을 가볍게 유지하는 일이기도 했다. 정리와 성능이 같은 방향을 가리켰다.


이런 걷어내기는 한 번으로 끝나지 않았다. 코드는 계속 자라고 그 과정에서 죽은 코드와 중복이 다시 쌓였다. 나는 번들 크기를 주기적으로 확인해서, 어느 순간 갑자기 커졌으면 무엇이 들어왔는지 추적했다. 크기를 감시하는 습관이 번들이 슬금슬금 무거워지는 걸 막았다. 방치하면 반드시 커졌다. 번들은 중력처럼 늘 무거워지는 쪽으로 끌렸고, 의식적으로 저항하지 않으면 어느새 초기의 몇 배가 되어 있었기에 나는 배포할 때마다 크기 변화를 확인하는 절차를 두었다.


4. 압축으로 마지막을 짜내기

코드를 나누고 걷어낸 다음에는 남은 것을 최대한 압축했다. 코드에서 사람이 읽기 위한 공백이나 주석, 긴 이름 같은 것은 브라우저 실행에는 불필요했다. 이런 것들을 걷어내 코드를 최소한의 형태로 줄이니 파일이 작아졌다. 기능은 그대로면서 크기만 줄이는 이 과정은 배포 단계에서 자동으로 처리했다.


전송 과정의 압축도 큰 몫을 했다. 서버가 파일을 보낼 때 압축해서 보내면, 브라우저가 받아서 풀었다. 텍스트 기반의 코드는 압축이 잘 돼서, 실제 전송되는 크기가 원래의 일부로 줄었다. 이 전송 압축은 서버 설정으로 간단히 켤 수 있어서 비용 대비 효과가 좋았다. 안 켜면 손해인 기본 조치였다.


압축은 코드뿐 아니라 사이트가 보내는 대부분의 텍스트 자원에 적용됐다. 문서, 스타일, 데이터 응답까지 압축하니 전체 전송량이 줄었다. 나는 압축이 제대로 적용되는지 확인했는데, 놓치기 쉬운 자원이 압축 없이 나가는 경우가 종종 있었기 때문이다. 빠짐없이 압축되는지 점검하는 것도 성능 관리의 일부였다.


이 모든 조치는 곱해져서 효과를 냈다. 나누고, 걷어내고, 압축하니 사용자가 실제로 받는 코드 양이 여러 겹으로 줄었다. 각 조치의 효과는 작아 보여도 합치면 초기 로딩이 확연히 가벼워졌다. 나는 이 여러 단계를 배포 과정에 자동으로 녹여서, 매번 신경 쓰지 않아도 항상 적용되게 했다.


5. 덜 보내는 것이 근본이다

번들을 다루며 얻은 결론은 결국 덜 보내는 것이 근본이라는 점이었다. 미루기와 순서 조정은 훌륭한 기법이지만, 애초에 보내는 양이 적으면 그런 기교가 덜 필요했다. 코드를 가볍게 유지하는 것이 다른 모든 최적화의 토대였다. 나는 새 기능을 만들 때마다 이게 번들에 얼마를 더하는지 의식했다.


이 의식은 개발 습관을 바꿨다. 편리한 큰 라이브러리를 반사적으로 가져오는 대신, 정말 필요한지 그 무게를 감당할 가치가 있는지 먼저 따졌다. 작은 기능은 직접 구현하는 편이 나을 때도 많았다. 성능을 염두에 두니 코드 선택 자체가 신중해졌고, 그 신중함이 사이트를 가볍게 유지했다.


덜 보내는 원칙은 사용자에 대한 배려이기도 했다. 내가 코드를 무겁게 짜면 그 대가는 사용자의 데이터와 시간, 그리고 기기의 부담으로 돌아갔다. 특히 느린 기기와 회선을 쓰는 사용자에게 무거운 번들은 큰 벽이었다. 나는 코드를 가볍게 유지하는 것을 그들에 대한 예의로 여겼다. 내 편의보다 그들의 경험이 우선이었다. 개발자의 몇 분을 아끼려고 사용자 수천 명의 시간을 축내는 선택은 규모로 따져도 남는 게 없었고, 이 관점을 갖고 나니 무거운 코드를 짜는 것에 자연스럽게 죄책감이 들었다.


이렇게 클라이언트로 보내는 코드를 줄였으니, 다음은 그 코드와 콘텐츠가 사용자에게 도달하는 경로를 볼 차례다. 아무리 가볍게 만들어도 사용자와 서버가 멀면 느렸다. 다음 편에서는 사용자 가까운 곳에서 응답하게 하는 엣지 캐시와, 그것이 첫 바이트 시간을 어떻게 줄이는지를 다루겠다.