![[실전 미디어 08] 대표 썸네일 자동 생성기](https://img.thenullpage.com/posts/5781/5781_1_9fc97b.webp)
지난 편까지 이미지를 잘 저장하고 상황에 맞게 변환하는 문제를 다뤘다. 이제 그렇게 저장한 이미지를 목록 화면에서 어떻게 대표로 보여줄지가 남았다. 글 목록에서는 각 글마다 작은 정사각형 대표 이미지, 즉 썸네일이 필요했다. 본문에 담긴 큰 이미지를 그대로 쓸 수는 없으니 별도의 작은 이미지를 만들어야 했는데, 이걸 사용자가 신경 쓰지 않아도 자동으로 뽑아내는 구조를 만드는 게 이번 편의 주제다. 썸네일은 사용자 눈에는 그저 목록에 뜨는 작은 그림일 뿐이지만, 그 뒤에는 무엇을 대표로 삼을지, 언제 어디서 만들지, 실패하면 어떻게 복구할지 같은 결정이 겹겹이 쌓여 있었다.
1. 목록에는 대표 이미지가 필요하다
목록 화면은 글이 수십 개씩 늘어서는 곳이다. 여기서 각 글의 본문 이미지를 원본 크기로 불러오면, 앞선 편에서 지적한 대역폭 낭비가 그대로 재현된다. 작은 칸에 보여줄 뿐인데 큰 이미지를 통째로 내려받는 셈이기 때문이다. 그래서 목록에는 처음부터 작게 만들어진 전용 썸네일이 있어야 했다. 목록은 사용자가 사이트에서 가장 자주 마주치는 화면이라, 여기가 무거우면 사이트 전체가 느리다는 인상을 준다. 그만큼 썸네일의 무게는 체감 성능에 직접 영향을 미쳤다.
썸네일의 규격도 정해야 했다. 목록의 칸은 정사각형이었으니 썸네일도 정사각형이 자연스러웠다. 그런데 원본 이미지는 가로로 넓거나 세로로 길어서 비율이 제각각이었다. 이걸 정사각형에 욱여넣으려면 찌그러뜨리거나, 아니면 중앙을 기준으로 정사각형 영역을 잘라내야 했는데 나는 후자를 택했다.
찌그러뜨리면 인물이나 사물이 왜곡되어 보기 흉했다. 반면 중앙을 기준으로 잘라내면 비율은 유지되고 가장자리만 조금 잘렸다. 대부분의 사진은 중요한 피사체가 가운데에 있으니 중앙 크롭이 무난했다. 그래서 썸네일 규칙은 원본의 중앙에서 정사각형 영역을 잘라 작은 크기로 줄인 webp로 확정했다.
크기는 실제 표시 칸보다 약간 크게 잡았다. 고해상도 화면에서는 같은 칸이라도 더 많은 픽셀을 쓰기 때문에, 표시 크기와 똑같이 만들면 고해상도에서 흐릿하게 보였다. 그래서 표시 칸의 두 배 정도 해상도로 만들어, 일반 화면에서도 고해상도 화면에서도 선명하게 보이도록 여유를 뒀다.
2. 어느 이미지를 대표로 삼나
한 글에 이미지가 여러 장이면 그중 무엇을 대표로 삼을지 정해야 했다. 규칙은 단순하게, 글에 등장하는 첫 번째 미디어를 대표로 삼기로 했다. 사용자가 글을 쓸 때 가장 먼저 넣는 이미지가 대체로 그 글을 대표하는 이미지라는 경험적 판단이었다. 복잡한 선택 로직 없이도 대개 맞아떨어졌다.
첫 미디어를 기준으로 삼으니 규칙이 예측 가능해졌다. 사용자도 첫 이미지가 목록에 뜬다는 걸 알면 원하는 대표 이미지를 맨 앞에 두는 식으로 조절할 수 있었다. 시스템이 알아서 가장 예쁜 이미지를 고르려 애쓰는 것보다, 규칙이 단순하고 사용자가 예측 가능한 편이 실제로는 더 좋았다. 똑똑한 자동 선택은 대개 어느 경우엔 맞고 어느 경우엔 엉뚱한 이미지를 골라 사용자를 당황하게 하는데, 단순하고 일관된 규칙은 틀리더라도 사용자가 이유를 이해하고 스스로 조절할 수 있었다.
다만 첫 미디어가 항상 이미지인 건 아니었다. 어떤 글은 동영상이 먼저 나왔고, 어떤 글은 텍스트만 있고 미디어가 아예 없었다. 동영상이 첫 미디어인 경우는 별도의 처리가 필요했는데, 이건 다음 편들에서 본격적으로 다룰 만큼 까다로운 주제라 여기서는 이미지가 첫 미디어인 경우에 집중했다.
미디어가 전혀 없는 글에는 썸네일 대신 다른 표현을 뒀다. 억지로 이미지를 만들어 붙이기보다, 텍스트 글은 텍스트 글답게 제목과 본문 미리보기를 강조하는 편이 자연스러웠다. 모든 글에 썸네일이 있어야 한다는 강박을 버리니 오히려 목록이 깔끔해졌다.
3. 클라이언트 생성과 서버 백업
썸네일을 언제 만들지도 결정해야 했다. 가장 빠른 건 사용자가 글을 올리는 순간 브라우저에서 첫 이미지의 썸네일까지 함께 만들어 보내는 것이었다. 이미 브라우저에서 이미지를 webp로 압축하고 있었으니, 그 김에 중앙을 잘라 작은 썸네일도 하나 더 만드는 건 부담이 크지 않았다.
하지만 클라이언트 생성만 믿을 수는 없었다. 앞선 편들에서 봤듯이 브라우저 변환은 환경에 따라 실패할 수 있었고, 오래된 글이나 다른 경로로 들어온 글은 애초에 썸네일이 없을 수도 있었다. 그래서 클라이언트에서 만들되, 그게 없거나 실패한 경우를 서버가 뒤에서 메우는 이중 구조가 필요했다.
서버 백업은 주기적으로 도는 예약 작업이 맡았다. 이 작업은 썸네일이 없는 글을 찾아, 그 글의 첫 이미지를 가져다 서버측 인코더로 중앙을 잘라 썸네일을 만들어 채웠다. 클라이언트가 놓친 것을 서버가 나중에 조용히 보충하는 것이다. 사용자는 이 과정을 전혀 몰라도 목록에는 결국 썸네일이 떴다.
이 이중 구조의 이점은 즉시성과 완결성을 둘 다 얻는다는 데 있었다. 대다수 글은 클라이언트 생성 덕에 발행 즉시 썸네일이 붙어 빨랐고, 어쩌다 빠진 소수는 서버 백업이 결국 채워줘서 빈칸이 남지 않았다. 빠른 경로와 확실한 경로를 겹쳐 두는 것이 안정적인 파이프라인의 공통 패턴이었다. 어느 한쪽만으로는 즉시성과 완결성 중 하나를 포기해야 하지만, 둘을 겹치면 평소엔 빠르고 예외에선 확실한, 두 성질을 모두 가진 시스템이 됐다.
4. 실패를 표시하고 다시 시도하기
서버 백업이 돌려면 어떤 글에 썸네일이 없는지 알아야 했다. 그래서 썸네일이 없거나 만들기에 실패한 글에는 재처리가 필요하다는 표시를 남겼다. 이 표시는 예약 작업이 다음번에 무엇을 처리해야 하는지 알려주는 일종의 할 일 목록이었다. 표시가 있는 글만 골라 처리하니 매번 전체를 훑을 필요가 없었다.
표시를 남기는 방식은 실패를 숨기지 않는다는 앞선 원칙과 같은 맥락이었다. 썸네일 생성이 실패했을 때 그냥 넘어가면 그 글은 영영 썸네일 없이 남는다. 대신 실패를 명시적으로 기록해 두면, 나중에라도 반드시 다시 시도된다. 실패의 흔적을 남기는 것이 자동 복구의 출발점이었다. 조용히 넘어간 실패는 아무도 기억하지 못해 영영 방치되지만, 어딘가에 표시로 남은 실패는 언젠가 반드시 눈에 띄어 처리된다. 그래서 나는 실패를 지우기보다 기록하는 쪽을 늘 택했다.
여기에 더해 월말마다 전수조사를 도는 안전망을 뒀다. 표시가 어떤 이유로 잘못 지워지거나 누락되는 경우까지 대비해, 주기적으로 모든 글의 썸네일 상태를 점검하는 것이다. 평상시에는 표시 기반으로 효율적으로 처리하고, 가끔 전수조사로 빠진 구멍을 훑는 이중 안전망이었다.
이렇게 표시와 예약 처리와 전수조사가 맞물리니, 썸네일 누락은 시간이 지나면 저절로 메워지는 상태가 됐다. 한 번 실패해도 시스템이 계속 재시도하니 결국 채워지는 것이다. 완벽하게 한 번에 성공하는 시스템보다, 실패해도 스스로 회복하는 시스템이 운영에서는 훨씬 마음이 편했다. 혼자 운영하는 사이트에서는 모든 실패에 즉시 달려갈 수 없으니, 내가 자는 동안에도 스스로 구멍을 메우는 자가 복구 구조가 사실상 또 한 명의 운영자 역할을 해줬다.
5. 썸네일도 결국 webp로
썸네일 역시 저장 포맷은 webp로 통일했다. 목록에는 썸네일이 수십 개씩 한꺼번에 뜨니, 각 썸네일이 조금만 커도 목록 전체 로딩이 무거워졌다. webp의 압축 효율은 이 지점에서 특히 값졌다. 작은 이미지 하나하나의 절감이 수십 개가 모이면 목록 로딩 속도의 체감 차이로 이어졌다.
썸네일 품질은 본문 이미지보다 조금 더 공격적으로 낮춰도 괜찮았다. 어차피 작게 표시되니 세밀한 디테일이 덜 중요했고, 사용자가 관심 있는 글을 눌러 들어가면 본문에서 더 큰 이미지를 보게 되기 때문이다. 목록의 썸네일은 어디까지나 미리보기이지 최종 감상용이 아니라는 성격을 품질 설정에 반영했다.
클라이언트와 서버가 같은 인코더 계열을 쓴다는 앞선 이점도 여기서 다시 작용했다. 썸네일이 클라이언트에서 만들어지든 서버 백업으로 만들어지든 압축 특성이 비슷하니, 목록에 뜬 썸네일들의 화질이 고르게 유지됐다. 어느 경로로 만들어졌는지 사용자가 눈치챌 수 없는 게 좋은 신호였다.
여기까지가 이미지 썸네일 이야기다. 그런데 앞서 잠깐 미룬 문제, 즉 첫 미디어가 동영상인 경우가 남아 있었다. 동영상의 대표 썸네일은 첫 프레임을 뽑아 만들어야 하는데, 이게 이미지와는 비교도 안 되게 까다로웠다. 다음 편에서는 비디오 썸네일이 특정 환경에서 구조적으로 실패하던, 오래 나를 괴롭힌 벽을 다룬다.