[실전 미디어 07] 온더플라이 변환으로 넘긴 짐

지난 편에서 긴 이미지는 클라이언트 캔버스로 압축하지 말고 원본을 보존한 뒤 서버측 변환에 맡기자는 방향을 잡았다. 이번 편은 그 서버측 변환을 실제로 어떻게 구현했는지, 즉 CDN의 온더플라이 이미지 변환에 짐을 넘긴 이야기다. 저장 시점에 모든 걸 확정하려던 사고방식을, 표시 시점에 필요한 만큼만 변환하는 사고방식으로 바꾼 전환이기도 했다. 언제 변환을 확정하느냐라는 시점의 문제가, 실은 저장 용량과 품질과 유연성을 모두 좌우하는 설계의 핵심 축이라는 걸 이 과정에서 배웠다.


1. 저장 시점 확정의 한계

그전까지 내 파이프라인은 저장 시점에 모든 걸 확정하려 했다. 업로드가 들어오면 그 순간 최종 크기와 포맷으로 변환해 저장하고, 표시할 때는 그 결과물을 그대로 내려주는 방식이었다. 단순하고 예측 가능했지만, 저장 시점에 하나의 결과만 확정한다는 게 한계였다.


화면마다 필요한 이미지 크기가 다르다는 게 문제였다. 목록의 작은 썸네일, 본문의 중간 크기, 원본을 보고 싶을 때의 큰 이미지가 다 다른데, 저장 시점에 하나로 확정하면 나머지 상황에서는 너무 크거나 작았다. 여러 크기를 미리 다 만들어 두는 것도 저장 용량 낭비였다. 어떤 크기가 실제로 쓰일지 저장 시점에는 알 수 없으니, 미리 만들어 둔 크기의 상당수는 한 번도 안 쓰이고 자리만 차지할 가능성이 높았다. 쓰지도 않을 결과물을 위해 용량을 미리 지불하는 셈이었다.


긴 이미지는 이 한계를 극단적으로 드러냈다. 저장 시점에 브라우저 캔버스로 확정하려다 가로를 죽였으니 말이다. 만약 저장은 원본 그대로 두고 표시할 때 그 상황에 맞는 크기와 포맷으로 그때그때 변환한다면, 저장 시점의 한 번의 잘못된 확정이 사라질 터였다.


이 발상이 온더플라이 변환이다. 저장은 원본 한 벌만, 변환은 요청이 올 때 필요한 만큼. 앞서 애니메이션 편에서 잠깐 실험했던 그 CDN 변환을, 이번에는 긴 이미지의 정식 해법으로 제대로 도입하기로 했다.


2. 온더플라이 변환이란

CDN 이미지 변환은 원본 이미지 주소 앞에 변환 옵션을 얹은 특별한 주소로 동작했다. 주소에 원하는 폭과 품질, 출력 포맷을 파라미터로 적으면, CDN이 그 주소로 요청이 올 때 원본을 가져다 지정한 대로 변환해 내려준다. 코드로는 주소에 폭 팔백, 품질 자동, 포맷 자동 옵션을 붙여 원본을 감싼다 정도였다.


핵심은 변환이 표시 순간에 일어난다는 것이다. 저장소에는 원본 한 벌만 있고, 목록에서는 작은 폭으로, 본문에서는 큰 폭으로 같은 원본을 다른 파라미터로 요청하면 그만이었다. 필요한 크기가 늘어나도 저장 용량은 그대로였다. 크기 조합을 미리 다 만들어 둘 필요가 사라진 것이다. 필요할지 안 할지 모르는 크기를 미리 만들어 쌓아 두는 낭비 대신, 실제로 요청된 크기만 그때 만들어 두는 방식이 훨씬 군더더기가 없었다.


긴 이미지에는 이게 결정적이었다. 원본을 그대로 보존하니 캔버스 면적 한도를 아예 건드리지 않았고, 표시할 때 가로를 유지한 채 원하는 폭으로 고품질 리샘플링을 받았다. 브라우저 기본 보간이 아니라 서버측 리샘플러가 텍스트 획을 살려주니, 긴 이미지 열화의 세 원인이 한꺼번에 걷혔다.


면적 여유도 넉넉했다. 서버측 변환은 브라우저 캔버스가 막히는 지점보다 훨씬 큰 이미지를 다뤘기 때문에, 십칠 메가픽셀짜리 긴 캡처도 무리 없이 처리했다. 클라이언트에서 그토록 골치였던 크기가 서버측에서는 평범한 입력이었다. 같은 이미지가 어느 환경에서는 한계를 넘는 괴물이고 다른 환경에서는 그냥 보통 파일이라는 사실은, 문제를 해결하는 가장 좋은 방법이 때로는 문제를 다른 환경으로 옮기는 것임을 다시 확인시켜줬다.


3. 전부가 아니라 위험군만

그렇다고 모든 이미지를 온더플라이로 바꾸진 않았다. 그러면 변환 비용과 원본 보관 용량이 전체로 커지기 때문이다. 그래서 앞 편에서 예고한 대로 하이브리드를 택했다. 일반 이미지는 기존 클라이언트 webp 압축을 그대로 두고, 캔버스 한도가 위험한 긴 이미지나 아주 큰 이미지만 원본 보존 후 온더플라이 변환으로 표시했다.


이 분기의 기준은 이미지의 크기와 비율이었다. 폭 대비 높이가 지나치게 길거나 총 면적이 일정 기준을 넘으면 위험군으로 분류해 원본 보존 경로로 보내고, 나머지 평범한 사진은 기존 경로로 처리했다. 대다수 업로드는 평범한 사진이라 기존 경로로 갔고, 위험군은 소수였다.


이렇게 나누니 두 마리 토끼가 잡혔다. 다수의 평범한 이미지는 변환 비용 없이 클라이언트에서 처리되어 서버 부담이 0이었고, 소수의 문제적 이미지만 서버 변환의 품질 이득을 받았다. 문제가 있는 곳에만 비싼 해법을 쓰고 나머지는 싼 해법을 유지하는, 비용 대비 효과가 가장 좋은 구성이었다. 모든 경우에 최고의 해법을 일괄 적용하는 것은 깔끔해 보이지만 대개 과잉이고, 실제로는 상황별로 적정한 해법을 골라 쓰는 편이 자원을 훨씬 아꼈다.


이 하이브리드 사고는 이 시리즈 전체를 관통하는 방식이기도 했다. 하나의 해법으로 모든 걸 밀어붙이는 대신, 입력의 성격에 따라 경로를 나누는 것이다. 정적 이미지와 애니메이션을 나눴듯이, 평범한 이미지와 위험한 이미지도 나눴다.


4. 비용과 캐시를 계산하다

서버 변환에는 비용이 따르니 계산이 필요했다. 온더플라이 변환은 고유한 변환 조합마다 횟수가 집계되고, 무료로 주어지는 월간 한도가 있었다. 한도를 넘으면 초과분에 대해 소액이 과금되는 구조였다. 그래서 얼마나 많은 변환이 실제로 발생하는지 가늠해야 했다.


다행히 캐시가 비용을 크게 눌러줬다. 같은 원본을 같은 파라미터로 변환한 결과는 CDN이 캐시하기 때문에, 두 번째 요청부터는 새 변환 없이 캐시된 결과가 나갔다. 즉 변환 횟수는 방문 수가 아니라 고유한 이미지와 크기 조합의 수에 비례했다. 인기 글의 이미지가 아무리 많이 조회돼도 변환은 처음 몇 번뿐이었다. 조회수와 변환 횟수를 분리해서 생각하는 것이 비용 예측의 핵심이었는데, 이 둘을 헷갈리면 트래픽이 늘 때마다 변환비가 폭증할 거라 오해해 겁을 먹게 된다. 실제로는 캐시가 그 걱정을 대부분 흡수했다.


위험군만 이 경로를 태운다는 결정도 비용 관점에서 옳았다. 전체 이미지를 온더플라이로 돌리면 변환 조합이 폭증해 한도를 위협했겠지만, 소수의 긴 이미지만 대상이면 변환 횟수가 무료 한도 안에 넉넉히 들어왔다. 문제군을 좁게 잡은 덕에 품질은 얻고 비용은 아꼈다.


물론 이 계산은 상시 감시가 필요했다. 위험군의 비중이 갑자기 늘거나 변환 대상을 넓히면 비용 구조가 달라지기 때문이다. 그래서 변환 횟수와 한도 소진을 주기적으로 확인하는 걸 습관으로 삼았다. 비용은 한 번 계산하고 잊는 게 아니라 계속 지켜봐야 하는 지표였다.


5. AVIF까지 자연스럽게

온더플라이 변환에는 뜻밖의 보너스가 있었다. 출력 포맷을 자동으로 두면, 요청한 브라우저가 더 좋은 포맷을 지원할 때 CDN이 알아서 그 포맷으로 내려줬다. 즉 AVIF를 지원하는 브라우저에는 AVIF를, 아니면 webp를 주는 식이었다.


앞서 포맷 편에서 AVIF는 압축률이 좋지만 인코딩이 느리고 호환성이 아직 부족해 기본으로 삼지 않았다고 했다. 그 판단은 저장 시점 인코딩 이야기였다. 온더플라이 변환에서는 CDN이 인코딩을 대신 해주고 지원되는 브라우저에만 골라 내려주니, 인코딩 부담과 호환성 리스크 없이 AVIF의 압축 이득만 취할 수 있었다. 같은 기술이라도 어느 시점에 쓰느냐에 따라 부담이 되기도 하고 공짜 이득이 되기도 한다는 게 흥미로웠다. AVIF는 저장 시점에는 짐이었지만 표시 시점에는 선물이었다.


이렇게 기본은 안전한 webp로 저장하되, 표시 계층에서 지원되는 곳에만 AVIF를 얹는 구성이 완성됐다. 포맷 편에서 그려둔 밑그림, 즉 저장은 webp로 통일하고 최적화는 표시 계층에서 조건부로 한다는 그림이 여기서 실제로 맞물린 것이다. 앞 편에서 내린 결정들이 뒤 편에서 자연스럽게 이어질 때, 설계가 일관된 방향으로 가고 있다는 확신이 들었다.


여기까지가 이미지의 저장과 변환에 관한 이야기다. 이제 이미지를 잘 저장하고 잘 변환했으니, 목록 화면에서 각 글을 대표할 작은 썸네일이 필요했다. 다음 편에서는 글의 첫 미디어에서 대표 썸네일을 자동으로 뽑아내는 구조와, 그게 실패했을 때의 대비책을 다룬다.