[실전 미디어 05] 움직이는 이미지를 지키는 법

지난 편까지 정적 이미지 압축을 안정화했다. 그런데 커뮤니티에는 움직이는 이미지가 생각보다 많았다. 반응을 담은 짧은 GIF, 밈, 애니메이션 스티커 같은 것들이다. 나는 모든 이미지를 webp로 통일한다는 원칙을 세웠는데, 이 원칙을 움직이는 이미지에 그대로 적용하자 움직임이 통째로 사라지는 사고가 났다. 이번 편은 움직임을 죽이지 않고 webp로 보존하는 방법을 찾아간 기록이다. 정지 이미지에서 통했던 원칙이 시간 축을 가진 이미지에서는 그대로 통하지 않았고, 나는 하나의 파이프라인 안에 두 개의 서로 다른 처리 세계가 필요하다는 걸 이때 배웠다.


1. 정적 인코더가 움직임을 죽이다

서버측에서 쓰던 표준 webp 인코더는 정지 이미지 한 장을 다루도록 만들어져 있었다. 여러 프레임이 흐르는 애니메이션을 이 인코더에 넣으면, 오류가 나는 게 아니라 첫 프레임 한 장만 뽑아서 webp로 만들었다. 결과물은 멀쩡한 webp였지만 움직임이 사라진 정지 화면이었다.


이건 인코더의 버그가 아니라 애초에 정적 이미지 전용 도구를 애니메이션에 잘못 들이댄 문제였다. 도구가 여러 프레임이라는 개념 자체를 몰랐으니, 첫 장만 읽고 나머지를 버리는 게 그 도구 입장에서는 정상 동작이었다. 잘못은 도구가 아니라 그걸 그렇게 쓴 내 파이프라인에 있었다. 도구의 한계를 탓하기 전에 그 도구가 무엇을 전제로 만들어졌는지 확인했어야 했는데, 나는 정적 인코더에 애니메이션을 넣으면 알아서 잘 처리해줄 거라 막연히 기대했던 것이다.


실제로 측정해보니 정적화는 순식간에 일어났다. 수십 메가에 백 프레임이 넘는 큰 GIF를 이 인코더에 넣어도 변환은 백 밀리초 안쪽으로 빨랐고, 용량도 확 줄었다. 문제는 그 빠르고 작은 결과물이 움직이지 않는다는 것뿐이었다. 속도와 용량만 보면 완벽해 보여서 오히려 함정이었다.


결국 움직이는 이미지는 정지 이미지와 근본적으로 다른 처리 경로가 필요하다는 결론에 도달했다. 프레임이라는 시간 축을 이해하는 도구로 인코딩해야 했다. 정지 이미지 파이프라인에 특별 케이스를 하나 더 붙이는 게 아니라, 애니메이션을 별도의 종류로 인식하고 다른 길로 보내야 했다.


2. 업계는 움직이는 이미지를 어떻게 다루나

조사해보니 크게 세 갈래의 접근이 있었다. 첫째는 업로드된 움직이는 이미지를 아예 짧은 무음 동영상으로 변환하는 방식이다. 대형 소셜 플랫폼들이 이 길을 많이 택했는데, 동영상 코덱이 애니메이션 이미지 포맷보다 압축 효율이 좋아서 용량이 크게 줄기 때문이다. 사용자에게는 여전히 자동 반복 재생되는 짤처럼 보인다.


둘째는 움직이는 이미지를 그대로 유지하되 포맷만 바꾸는 방식이다. GIF를 애니메이션 webp로 변환하는 표준 도구가 있는데, 프레임을 모두 보존하면서 용량을 큰 폭으로 줄여준다. 무손실로도 어느 정도 줄고 손실을 허용하면 절반 이하로도 떨어졌다. 이미지를 이미지로 유지한다는 점에서 내 통일 원칙과 잘 맞았다.


셋째는 저장은 원본으로 두고 표시할 때 CDN이 실시간으로 변환해주는 방식이다. 저장 시점에 변환을 확정하지 않고, 요청이 올 때 파라미터에 따라 애니메이션 webp로 내려주는 것이다. 서버리스 스택과 궁합이 좋아 보였지만, 뒤에서 보듯 실제로는 조건이 까다로웠다.


나는 이미지를 이미지로 유지하고 싶었고, 동시에 서버리스 제약 안에서 자동화하고 싶었다. 그래서 둘째와 셋째를 놓고 저울질했다. 동영상으로 바꾸는 첫째 방식은 표시와 썸네일 로직이 전부 동영상 취급으로 바뀌어야 해서 변경 범위가 컸고, 커뮤니티의 짧은 짤에는 다소 과한 선택이라 우선순위에서 뒤로 뒀다. 동영상으로 취급하는 순간 자동 재생 여부, 소리 처리, 별도 썸네일 생성 같은 부수 문제가 줄줄이 딸려 오는데, 그 복잡도를 감수할 만큼 이득이 크지 않다고 봤다.


3. 온더플라이 변환의 함정

가장 서버리스다운 셋째 방식, 즉 CDN 온더플라이 변환부터 실험했다. 이론은 매력적이었다. 원본을 그대로 두고 표시 주소에 포맷과 애니메이션 유지 옵션을 붙이면 CDN이 알아서 애니메이션 webp로 바꿔 내려준다. 코드는 주소 문자열만 조립하면 되니 서버 부담이 없었다.


그런데 실제로 우리 이미지들로 때려보니 조건이 까다로웠다. 우선 요청 헤더에 webp를 받겠다는 표시를 명시적으로 넣어줘야만 CDN이 변환을 했다. 그 표시가 없으면 원본 GIF를 그대로 흘려보냈다. 브라우저는 알아서 그 헤더를 붙이지만, 내가 직접 변환을 유도하려면 헤더를 챙겨야 했다.


더 큰 걸림돌은 작은 파일이었다. CDN이 자체 판단으로, 변환해봤자 더 작아지지 않겠다 싶은 작은 애니메이션은 변환을 거부하고 원본을 그대로 돌려줬다. 실제로 작은 GIF 하나는 webp를 요청해도 계속 GIF로 왔고, 응답 헤더를 보고서야 CDN이 패스스루했다는 걸 알았다. 큰 파일만 변환되고 작은 파일은 남는 것이다.


이건 CDN이 대체로 옳은 판단을 한 것이지만, 나에게는 문제였다. 나는 저장물을 webp로 완전히 통일하고 싶었는데, 작은 애니메이션이 GIF로 남으면 파편화가 부분적으로 되살아났다. 온더플라이 변환만으로는 webp 통일을 끝까지 강제할 수 없다는 게 실험의 결론이었다. CDN의 판단은 대역폭 효율 관점에서는 합리적이었지만, 저장물의 포맷을 완전히 통일하려는 내 목표와는 방향이 어긋났다. 좋은 최적화가 반드시 내 정책과 일치하는 건 아니었다.


4. 확실하게 바꾸는 도구

완전 통일을 원한다면 크기와 무관하게 무조건 변환해주는 도구가 필요했다. 그래서 눈을 돌린 것이 범용 미디어 변환 도구였다. 여기에는 애니메이션 webp를 만드는 인코더가 들어 있어서, 명령 한 줄로 GIF를 프레임을 모두 유지한 애니메이션 webp로 바꿀 수 있었다.


명령은 대략 입력 GIF를 애니메이션 webp 인코더로, 품질 75, 무한 반복 옵션으로 출력 하는 형태였다. 실제로 돌려보니 크기가 크든 작든 항상 변환에 성공했다. 앞서 CDN이 거부하던 작은 애니메이션도 이 도구는 군말 없이 애니메이션 webp로 바꿔줬고, 프레임 수도 그대로 보존됐다.


다만 이 도구는 브라우저나 엣지 함수 안에서 돌릴 수 있는 게 아니었다. 그래서 동영상 처리에서 이미 쓰던 패턴을 그대로 가져왔다. 업로드 시점에는 움직이는 이미지를 감지해 재처리 대상으로 표시만 해두고, 실제 변환은 내 PC에서 이 도구로 일괄 처리한 뒤 스토리지의 파일을 교체하는 방식이다.


이 반자동 방식은 완전 자동은 아니지만 확실하고 무료였다. 감지와 표시는 엣지에서 자동으로 되고, 무거운 변환만 내가 주기적으로 돌리면 됐다. 검증된 변환 도구를 쓰니 결과도 안정적이었다. 자동화의 이상과 서버리스의 제약 사이에서 찾은 현실적인 타협점이었다. 완벽한 무인 자동화에 집착하다 아무것도 처리하지 못하는 것보다는, 감지까지는 자동으로 하고 무거운 변환만 사람이 주기적으로 돌리는 편이 실제로는 훨씬 안정적으로 굴러갔다.


5. 움직임을 감지하는 법

이 모든 처리의 출발점은 애니메이션인지 아닌지를 감지하는 것이었다. 정적 이미지는 기존 인코더로 바로 webp로 만들고, 움직이는 이미지만 재처리 대상으로 빼내려면 둘을 구분해야 했다. 겉보기 확장자만으로는 부족했고 파일 내용을 들여다봐야 했다.


판별은 파일 구조에서 프레임 정보를 확인하는 방식으로 했다. GIF라면 프레임을 구분하는 제어 블록이 둘 이상 있는지, webp라면 애니메이션임을 나타내는 특정 청크가 들어 있는지를 검사했다. 프레임이 하나뿐이면 정적으로 취급해 바로 변환하고, 둘 이상이면 애니메이션으로 분류해 재처리 큐로 보냈다.


이 감지 로직 덕분에 파이프라인은 두 종류의 이미지를 자동으로 갈라 처리할 수 있었다. 사용자는 자기가 올린 게 정적인지 애니메이션인지 신경 쓸 필요가 없었고, 시스템이 알아서 맞는 경로로 흘려보냈다. 입구에서 종류를 판별해 분기하는 이 구조가 이후 처리 전체를 깔끔하게 만들었다. 뒤쪽 코드가 저마다 정적인지 애니메이션인지 다시 판단하지 않아도 되니, 분기가 한 곳으로 모여 관리하기 쉬웠다. 판별은 되도록 이른 지점에서 한 번만 하는 게 좋았다.


움직임까지 보존하는 문제를 해결하고 나니, 이미지에서 남은 큰 난관은 크기였다. 특히 세로로 아주 긴 캡처 이미지가 앞서 여러 번 언급한 캔버스 면적 한도를 정면으로 때리고 있었다. 다음 편에서는 그 긴 이미지가 왜 텍스트를 알아볼 수 없게 뭉개졌는지, 그 원인을 파고든 이야기를 다룬다.