![[실전 미디어 10] 서버에서 프레임을 뽑다](https://img.thenullpage.com/posts/5783/5783_1_6f6257.webp)
지난 편에서 클라이언트로는 동영상 첫 프레임을 뽑을 수 없다는 결론에 이르렀다. 대다수 사용자 환경의 브라우저 엔진이 화면 밖 동영상을 캔버스에 그리지 못하는 구조적 한계 때문이었고, 이건 코드로 넘을 수 있는 벽이 아니었다. 그렇다면 답은 하나였다. 브라우저 환경과 완전히 무관한 곳, 즉 서버에서 동영상의 한 프레임을 뽑아내는 것이다. 이번 편은 그 서버사이드 프레임 추출로 오랜 벽을 허물고, 손으로 썸네일을 붙이던 반복 노동을 영구히 없앤 이야기다.
1. 정답은 서버사이드였다
서버사이드로 옮긴다는 발상의 핵심은 문제를 우회하는 게 아니라 해당 없음으로 만드는 데 있었다. 클라이언트에서 뽑으려 하면 브라우저 엔진의 한계가 개입하지만, 서버에서 뽑으면 그 한계가 개입할 자리 자체가 없다. 특정 환경에서 되고 안 되고를 따질 필요가 사라지는 것이다. 문제가 존재할 수 없는 곳으로 작업을 옮기는 것이 진짜 근본 해결이라는, 앞 편에서 얻은 결론을 그대로 실행하는 셈이었다. 남은 건 서버에서 동영상을 다룰 실제 수단을 찾는 일이었다.
그런데 내 서버리스 스택에는 큰 제약이 있었다. 엣지 함수 안에서는 무거운 미디어 처리 도구를 돌릴 수 없었다. 동영상을 직접 디코딩해 프레임을 뽑는 네이티브 도구는 함수 환경에 붙일 수도 없고, 설령 붙인다 해도 실행 시간과 메모리 한도에 걸렸다. 그래서 함수가 직접 동영상을 처리하는 길은 처음부터 막혀 있었다. 함수 안에서 다 하려는 시도는 절대 하지 말아야 할 것이었다.
다행히 내가 쓰는 인프라에는 미디어 변환을 대신 해주는 기능이 마련되어 있었다. 어떤 출처의 동영상이든 특정 형식의 주소로 요청하면, 그 동영상에서 한 프레임을 뽑아 이미지로 돌려주는 서비스였다. 함수가 직접 처리하는 게 아니라, 함수는 이 서비스에 요청만 하고 결과를 받아 저장하면 됐다. 무거운 일은 인프라가 하고 내 코드는 지시만 하는 구조였다. 서버리스 제약 안에서 딱 맞는 도구였다.
이 대목에서 서버리스의 제약이 오히려 좋은 안내판이었다는 생각도 들었다. 함수 안에서 뭐든 할 수 있었다면 나는 아마 무거운 도구를 어떻게든 욱여넣으려다 실행 시간 한도와 씨름했을 것이다. 못 하게 막혀 있으니 처음부터 인프라가 제공하는 전용 기능을 찾게 됐고, 그게 결과적으로 더 안정적이고 손이 덜 가는 길이었다. 제약이 나를 더 나은 설계로 떠민 셈이다.
2. 미디어 변환 기능을 쓰다
이 변환 기능은 주소에 옵션을 얹는 방식으로 동작했다. 원본 동영상 주소 앞에, 프레임 한 장을 뽑겠다는 모드와 어느 시점의 프레임인지, 그리고 결과 이미지의 크기와 자르기 방식을 파라미터로 적어주는 것이다. 코드로는 정지 프레임 모드로, 재생 시작 부근의 한 시점을, 정사각형에 맞춰 잘라 지정 크기로 뽑는 형태였다. 앞서 이미지 썸네일에서 쓰던 중앙 크롭 정사각형 규격을 여기에 그대로 맞췄다. 그러면 이미지 썸네일이든 동영상 썸네일이든 목록에서 똑같은 모양으로 보였다.
어느 시점의 프레임을 뽑을지는 신경 써야 할 부분이었다. 맨 첫 순간을 뽑으면 아직 화면이 검거나 페이드인 전이라 빈 장면이 나올 수 있었다. 그래서 재생이 살짝 진행된 시점을 골라, 실제 내용이 담긴 장면이 잡히도록 했다. 너무 뒤로 가면 엉뚱한 장면이 대표가 될 수 있으니 시작 부근의 적당한 지점이 무난했다. 이 작은 조정 하나가 대표 이미지의 품질을 크게 좌우했다.
중요한 건 이 모든 과정이 원본 동영상을 전혀 건드리지 않는다는 점이었다. 변환 기능은 원본에서 한 장면을 읽어 이미지를 만들 뿐, 원본 자체는 그대로 스토리지에 남았다. 동영상은 압축하지 않고 원본을 보존한다는 내 정책과 아무 충돌이 없었다. 프레임 추출과 동영상 보관이 서로 독립적으로 굴러갔다. 하나의 작업이 다른 정책을 침범하지 않는 깔끔한 분리였다.
결과 이미지의 저장도 이미지 썸네일과 똑같이 처리했다. 변환 기능이 돌려준 프레임 이미지를 받아 썸네일 자리에 webp로 저장하니, 이후 목록은 그게 동영상에서 온 것인지 이미지에서 온 것인지 구분하지 않아도 됐다. 목록 입장에서는 그냥 정사각형 webp 썸네일 하나일 뿐이었다. 서로 다른 출처를 같은 형태로 수렴시키니 뒤쪽 코드가 단순해졌다. 파이프라인 곳곳에서 반복된 이 수렴의 이점을 여기서도 다시 확인했다.
3. 예약 작업으로 자동화하다
수단을 찾았으니 이제 자동화 차례였다. 주기적으로 도는 예약 작업이, 대표 썸네일이 없는 글 중 첫 미디어가 동영상인 것을 찾아내도록 했다. 예전에는 이런 글을 만나면 처리할 방법이 없어 그냥 포기하고 사람에게 떠넘겼는데, 이제는 그 자리에서 변환 기능으로 프레임을 받아왔다. 받아온 이미지를 썸네일 자리에 저장하고, 재처리가 필요하다는 표시를 지웠다. 이미지 글과 완전히 똑같은 흐름으로 동영상 글도 처리된 것이다.
이 자동화의 의미는 컸다. 그전까지 동영상 글의 썸네일은 세션마다 반복되는 수작업이었다. 새 동영상 글이 올라올 때마다 내가 첫 프레임을 뽑아 붙여야 했고, 바쁘면 누락되어 목록이 휑했다. 이제는 예약 작업이 알아서 채우니 사람이 개입할 일이 사라졌다. 반복 노동이 코드 몇 줄로 영구히 제거된, 이 시리즈에서 가장 통쾌한 순간 중 하나였다.
기존 아키텍처를 거의 그대로 재활용한 것도 만족스러웠다. 이미 썸네일 없는 글을 찾아 처리하고 표시를 관리하는 예약 작업 구조가 있었으니, 여기에 동영상이면 변환 기능을 호출한다는 분기 하나만 추가하면 됐다. 완전히 새 시스템을 만든 게 아니라, 있던 틀에 빠진 한 조각을 끼운 것이다. 잘 설계된 구조는 새 요구가 생겨도 통째로 갈아엎지 않고 확장으로 대응할 수 있었다.
4. 비용과 한도를 따지다
새 기능을 도입할 때는 늘 비용을 먼저 계산했다. 이 변환 기능은 정지 프레임 한 장을 뽑을 때마다 변환 한 건으로 집계됐고, 매달 무료로 주어지는 한도가 있었다. 내 사이트의 동영상 글 수를 생각하면 이 무료 한도는 차고 넘쳤다. 동영상 글은 이미지 글보다 훨씬 적었고, 게다가 한 글당 썸네일은 한 번만 만들면 됐기 때문이다. 비용 걱정 없이 쓸 수 있는 규모였다.
한 번만 만들면 된다는 점이 특히 중요했다. 썸네일은 한 번 만들어 저장해 두면 그 뒤로는 저장된 이미지를 계속 쓰니, 변환은 글마다 최초 한 번뿐이었다. 조회수가 아무리 높아도 변환이 반복되지 않았다. 앞서 온더플라이 변환에서 캐시가 비용을 눌러줬듯이, 여기서는 저장이 같은 역할을 했다. 변환 횟수를 발생 원인에 붙들어 매는 것이 비용 관리의 핵심이었다.
이 기능이 같은 인프라 안에서 동작한다는 점도 이점이었다. 앞서 긴 이미지에 쓴 온더플라이 이미지 변환과 이 동영상 프레임 추출이 같은 변환 계열에 속했다. 도구가 갈래지지 않고 하나의 인프라로 모이니, 활성화나 한도 관리 같은 운영도 한곳에서 하면 됐다. 여러 문제를 같은 도구로 풀 수 있을 때 시스템은 배우기도 지키기도 쉬웠다.
혹시 변환이 실패하는 동영상이 있을까 봐 대비책도 생각해 뒀다. 손상됐거나 형식이 특이한 동영상은 프레임 추출이 안 될 수 있는데, 그럴 때는 재처리 표시를 지우지 않고 남겨 두어 다음 주기에 다시 시도하게 했다. 그래도 계속 실패하면 그 소수만 따로 확인하면 됐다. 다수를 자동으로 처리하고 극소수 예외만 사람이 보는 것이, 모든 걸 사람이 하던 예전과는 완전히 다른 부담이었다.
5. 발행 즉시 vs 예약 처리
자동화가 예약 작업 기반이면 한 가지 아쉬움이 있었다. 예약 작업은 주기적으로 도니, 동영상 글을 올린 직후에는 아직 썸네일이 없다가 다음 작업 주기가 돼서야 채워졌다. 대개는 문제없지만, 방금 올린 글의 목록 대표가 잠깐 비어 보이는 건 매끄럽지 않았다. 그래서 발행 시점에도 같은 변환 기능을 호출해 즉시 썸네일을 만드는 선택지를 검토했다. 그러면 예약 작업을 기다릴 필요 없이 발행 즉시 대표 이미지가 떴다.
즉시 생성과 예약 처리는 서로 배타적이지 않고 겹쳐 쓰는 게 좋았다. 발행 시점에 즉시 만들어 빠른 노출을 얻고, 그게 어떤 이유로 실패하면 예약 작업이 뒤에서 다시 채우는 것이다. 앞선 썸네일 편에서 본 빠른 경로와 확실한 경로를 겹치는 패턴이 동영상에도 그대로 적용됐다. 즉시성과 완결성을 둘 다 챙기는 익숙한 구조였다.
이 기능을 쓰려면 인프라 쪽에서 해당 변환을 활성화하는 설정이 먼저 필요했다는 점도 기록해 둘 만하다. 처음 시도했을 때는 변환 주소가 동작하지 않아 당황했는데, 알고 보니 그 기능이 아직 켜져 있지 않았던 것이다. 설정에서 한 번 켜주자 정상적으로 프레임이 뽑혔다. 새 인프라 기능은 코드만 맞다고 되는 게 아니라 활성화 여부까지 확인해야 한다는 걸 그때 배웠다.
이렇게 동영상 썸네일이라는 오랜 골칫거리가 서버사이드 처리로 완전히 해결됐다. 이제 동영상은 대표 이미지까지 자동으로 갖추게 됐다. 하지만 동영상에는 훨씬 무겁고 근본적인 문제가 아직 남아 있었다. 바로 용량이었다. 다음 편에서는 원본 그대로 두면 눈덩이처럼 불어나는 동영상 용량을, 해상도와 비트레이트에 상한을 걸어 잡는 트랜스코딩 이야기를 다룬다.