[마이 프로젝트] 블로그스팟 수익형 테마: 그림 안 뜰 때 대신 띄우기

포스터 이미지가 비었을 때 다른 곳에서 대신 가져오는 구조를 나타낸 대표 이미지
한 곳만 믿으면 화면이 자주 빕니다.

영화 목록을 만들었더니 포스터가 비어 있는 칸이 생각보다 많았습니다. 데이터는 왔는데 이미지 주소가 없거나, 있어도 열리지 않는 경우였습니다. 이 글은 한 곳에서 못 찾으면 다른 곳을 차례로 찔러보게 만든 구조입니다.

이미지 주소가 여러 개 붙어 온다

먼저 알아야 할 게 있습니다. 영화 정보 API는 포스터 주소를 하나만 주지 않습니다. 여러 개를 세로 막대로 이어 붙여서 한 칸에 담아 줍니다.

function getPosterCandidates(rawPosters){  return String(rawPosters || '').split('|')       // 막대로 자른다    .map(function(v){ return normalizePosterUrl(v); })    .filter(function(v){ return /^https:\/\//i.test(v); });  // https만 남긴다}

마지막 줄이 중요합니다. http로 시작하는 주소는 버립니다. 블로그가 https로 서비스되는데 이미지만 http면 브라우저가 막습니다. 처음에 이걸 몰라서, 주소는 멀쩡한데 이미지가 안 뜨는 걸 한참 봤습니다.

세 곳을 순서대로 찔러보고 그래도 없으면 기본 이미지를 쓰는 흐름
순서를 정해 두면 실패해도 화면은 채워집니다.

세 곳을 순서대로 찌른다

후보 주소가 하나도 안 남으면 다른 곳에 물어봅니다. 순서는 이렇게 정했습니다.

  • 원래 응답에 들어 있던 주소 — 있으면 이게 제일 정확합니다.
  • KMDb — 한국영화 데이터베이스. 국내 영화 포스터가 잘 있습니다.
  • TMDB — 해외 영화까지 폭이 넓습니다.
  • 기본 이미지 — 셋 다 없으면 회색 판을 깔았습니다.

기본 이미지를 넣은 이유가 있습니다. 빈 칸을 그대로 두면 목록 전체가 흐트러집니다. 내용이 없어도 자리는 채워 둬야 줄이 맞습니다.

제목이 조금씩 달라서 못 찾는다

여기서 제일 오래 걸렸습니다. 다른 곳에 물어보려면 제목으로 검색해야 하는데, 같은 영화인데 표기가 다릅니다. 괄호가 붙거나, 부제가 콜론으로 이어지거나, 띄어쓰기가 다릅니다.

function normalizeTitleForMatch(value){  return String(value || '')    .toLowerCase()    .replace(/\s*\([^)]*\)\s*/g, ' ')      // 괄호와 그 안을 통째로 제거    .replace(/[::,\-–—_]/g, ' ')           // 문장부호를 공백으로    .replace(/\s+/g, ' ')                   // 공백을 하나로    .trim();}

비교할 때만 이 함수를 씁니다. 화면에는 원래 제목을 그대로 보여줍니다. 비교용으로 다듬은 제목을 화면에 쓰면 이상해집니다.

포스터를 채우기 위해 정한 세 가지 규칙을 정리한 설명 이미지
실패를 전제로 만들어야 화면이 안 깨집니다.

실패를 전제로 만든다

이 구조에서 배운 게 하나 있습니다. 외부에서 받아오는 것은 실패한다고 보고 만들어야 합니다. 되는 경우만 생각하고 짜면, 안 될 때 화면이 통째로 깨집니다.

각 단계마다 “여기서 실패하면 다음은 뭐냐”를 정해 두니 화면이 안정됐습니다.

한계

세 곳을 다 찌르면 그만큼 느립니다. 포스터가 없는 영화가 많은 목록은 눈에 띄게 느려집니다. 지금은 결과를 저장해 두지 않아서, 같은 영화를 다시 봐도 또 물어봅니다.

그리고 제목 정규화가 완벽하지 않습니다. 원제와 국내 제목이 아예 다른 영화는 여전히 못 찾습니다.

https 페이지에서 http 이미지가 안 뜨는 이유

앞에서 http로 시작하는 주소를 버린다고 했는데, 이 규칙의 배경은 “혼합 콘텐츠(mixed content)”입니다. MDN 문서는 이를 https로 안전하게 불러온 페이지가 http 같은 안전하지 않은 방식으로 자원을 가져오는 상황이라고 설명합니다. 그런 자원은 공격자가 보거나 바꿀 수 있기 때문입니다.

MDN에 따르면 브라우저는 이미지, 영상, 소리처럼 비교적 위험이 작은 자원은 http를 https로 올려서 다시 요청하는 “업그레이드”를 시도하고, 스크립트, 스타일시트, iframe, fetch() 요청처럼 페이지를 바꿀 수 있는 자원은 막습니다. 그리고 img의 src는 업그레이드 대상이지만, srcset이나 picture로 불러오는 이미지는 막힐 수 있다고 적혀 있습니다. 숫자로 된 IP 주소를 호스트로 쓰는 이미지도 업그레이드하지 않고 막습니다. 같은 http 주소라도 쓰는 방식에 따라 뜨기도 하고 안 뜨기도 하는 이유가 여기에 있습니다.

자원 종류http로 가져오면이 글의 대응
img의 srchttps로 업그레이드를 시도합니다.처음부터 https 주소만 후보로 남깁니다.
srcset, picture 이미지막힐 수 있습니다.후보에서 http를 제외하므로 영향이 없습니다.
IP 주소 호스트의 이미지업그레이드하지 않고 막습니다.https 정규식에서 걸러집니다.
스크립트, iframe, fetch()막습니다.외부 API는 https 주소만 씁니다.

그래서 후보 주소에서 ^https://로 시작하는 것만 남기는 필터는 거추장스러운 규칙이 아니라, 브라우저가 어차피 막거나 바꿀 주소를 미리 걸러 내는 일입니다. 업그레이드가 성공하더라도 상대 서버가 https를 지원하지 않으면 결국 실패하므로, 후보에서 빼고 다음 곳으로 넘어가는 편이 예측하기 쉽습니다.

W3C의 혼합 콘텐츠 명세도 같은 구분을 합니다. 명세는 이미지, 영상, 소리를 요청하는 경우를 자동 업그레이드 대상으로 보되, srcset이나 picture를 쓰는 이미지는 제외합니다. 그리고 업그레이드할 수 없는 경우에는 그 요청을 막는다고 적고 있습니다. 즉 “http라도 알아서 https로 바뀌겠지”라는 기대는 상대 서버가 https를 받아 주는 경우에만 맞고, 그렇지 않으면 포스터 칸이 비어 보이게 됩니다. 사용자가 브라우저 설정으로 이 동작을 바꿀 수도 있어서, 내 화면에서 보이는 것과 방문자 화면에서 보이는 것이 다를 수 있다는 점도 명세에 언급돼 있습니다.

이미지가 실패했을 때를 코드로 잡는 방법

주소가 https여도 이미지가 열리지 않는 경우는 남습니다. 파일이 지워졌거나 서버가 응답하지 않는 경우입니다. MDN의 error 이벤트 문서는 이 이벤트를 자원이 로드에 실패했거나 쓸 수 없을 때 요소에서 발생하는 것으로 설명합니다. 이 이벤트는 취소할 수 없고 위로 전달되지도 않아서, 이미지 요소마다 직접 addEventListener(“error”, ...)나 onerror를 달아야 합니다.

이 글의 구조에 이 이벤트를 적용하면 흐름이 단순해집니다. 첫 후보 주소로 이미지를 그리고, 실패 이벤트가 오면 다음 후보로 바꾸고, 후보가 떨어지면 다른 곳에 묻는 단계로 넘어갑니다. 이벤트 자체가 “이 곳은 안 된다”는 신호가 되므로, 시간을 재거나 주소를 미리 점검하는 코드를 따로 두지 않아도 됩니다. 이 이벤트는 2015년 7월부터 주요 브라우저에서 두루 쓸 수 있는 기능으로 표시돼 있습니다.

주소를 점검하는 간단한 순서

포스터가 안 뜰 때는 원인을 세 단계로 나눠 보면 빨리 찾습니다. 먼저 주소가 비어 있는지 봅니다. 응답에 값이 없다면 이미지 문제가 아니라 데이터 문제입니다. 다음으로 주소가 http로 시작하는지 봅니다. 앞의 설명처럼 https 페이지에서는 막히거나 바뀔 수 있습니다. 마지막으로 주소가 https인데도 열리지 않는지 봅니다. 이 경우는 파일이 사라졌거나 서버 문제이므로 실패 이벤트를 받아 다음 후보로 넘기는 처리가 필요합니다.

MDN 문서가 알려 주는 error 이벤트의 또 다른 특징도 기억해 둘 만합니다. 이 이벤트는 위로 전달되지 않으므로 목록 전체에 한 번만 리스너를 달아서는 개별 이미지의 실패를 잡을 수 없습니다. 카드를 그리는 함수 안에서 이미지 요소를 만들 때 곧바로 리스너를 붙이는 방식이 가장 단순합니다. 이미 화면에 그려진 이미지에 나중에 리스너를 달면 실패가 그 전에 일어났을 수 있으니, 요소를 만든 직후 src를 지정하기 전에 붙이는 순서도 중요합니다.

정리하면, 포스터가 비는 문제는 데이터가 없는 경우, 주소가 안전하지 않은 경우, 주소는 맞지만 불러오기에 실패하는 경우로 갈립니다. 첫 번째는 다른 출처에 묻는 것으로, 두 번째는 https 후보만 남기는 것으로, 세 번째는 실패 이벤트를 받아 다음 후보로 넘기는 것으로 각각 대응합니다. 한 가지 처리로 세 경우를 모두 덮으려 하지 않고 원인별로 나눠 두면 어느 단계에서 문제가 생겼는지 추적하기 쉽습니다.

순서를 정할 때 같이 정해 둘 것

후보를 차례로 시도하는 구조를 만들 때는 순서와 함께 세 가지를 정해 두면 좋습니다. 하나는 한 곳당 기다리는 시간입니다. 기다림이 길면 앞의 “한계”에서 말한 느림이 커집니다. 둘째는 같은 영화를 다시 물을지 여부입니다. 결과를 저장하지 않는 지금 구조에서는 반복 질문이 그대로 비용이 됩니다. 셋째는 마지막 기본 이미지의 크기입니다. 실제 포스터와 같은 비율로 맞춰 두어야 실패한 칸 때문에 목록의 줄이 틀어지지 않습니다.

확인 자료