source.unsplash.com이 사라졌다: 부검 보고서와 모든 대체 방법

2021년 “기존 사용은 계속 작동합니다”라는 말과 함께 지원 중단되었고, 2024년 6월 완전히 꺼졌으며, 오늘날에도 여전히 새 코드에 쓰이고 있습니다. 냉정한 부검 보고서 — 그리고 키 없는 무작위성을 되돌려주는 프록시를 포함한 세 가지 대체 방법.

옅은 색 나무 표면 위에 놓인 화면이 깨진 스마트폰의 흑백 사진, 손상된 디스플레이에 Unsplash 로고가 보인다.
사진 출처: Unsplash

스톡 사진 서비스 하나가 서브도메인을 종료했고, 2년이 지난 지금도 여전히 문서 사이트, 로그인 화면, 강의 실습, 방금 생성된 코드까지 망가뜨리고 있습니다. 이 글은 하나의 URL에 대한 사후 분석입니다 — 그것이 무엇을 했는지, 무엇이 그것을 죽였는지, 그 자리에 무엇을 넣어야 하는지 — 그리고 이 이야기에서 더 이상한 부분, 즉 우리 코드를 작성하는 기계들이 아직 그것이 사라진 것을 알아채지 못했다는 점도 함께 살펴봅니다.

HTTP 응답, 두 개의 changelog 항목을 한 자 한 자 인용한 내용, API 제한, 문제가 발생했던 프로젝트들의 이슈 트래커, GitHub·npm·Stack Overflow의 수치 — 이 모든 것은 1차 출처에서 가져온 것이며, 각각의 확인 방법은 각주에 있습니다. 해결책만 필요하다면 마이그레이션 표로 바로 이동하세요.

지금 요청하면 무엇을 받게 되는가

명령 하나, 키 없이, 어떤 머신에서든 재현 가능합니다:

terminal
curl -I https://source.unsplash.com/random

HTTP/2 503
cache-control: no-cache, no-store
content-type: text/html; charset=utf-8
server: Heroku
via: 2.0 heroku-router
# 본문: herokucdn.com/error-pages/application-error.html을 가리키는 iframe

여기서 일어난 것은 DNS 실패가 아닙니다. source.unsplash.com은 여전히 정상적으로 확인되며 — herokudns.com 호스트에 대한 CNAME입니다 — 요청은 애플리케이션이 아니라 그저 응답을 받는 것뿐입니다. 이 세부사항은 겉보기보다 중요합니다. HTML 본문이 담긴 빠른 503을 받은 브라우저는 깨진 이미지 자리표시자를 그리고, response.ok나 여러분이 작성한 적 없는 onerror 핸들러를 읽는 코드는 한 번도 테스트하지 않은 실패 경로를 타게 됩니다.

URL 패턴 이전에는 무엇을 반환했는가 현재
source.unsplash.com/random무작위 사진, 크기 무관503
source.unsplash.com/random/1600x900무작위 사진, 지정된 크기로 크롭503
source.unsplash.com/1600x900/?apple,desk검색어와 일치하는 무작위 사진503
source.unsplash.com/featured/1600x900?nature무작위 추천(featured) 사진503
source.unsplash.com/collection/190727/800x600컬렉션에서 무작위 사진503
source.unsplash.com/user/scottwebb/1600x900특정 사진작가의 무작위 사진503
source.unsplash.com/daily오늘의 사진503

각각 curl -o /dev/null -w "%{http_code}"로 개별 확인했습니다. 발표된 대로 검색 기능이 먼저 중단되었고, 지금은 애플리케이션 전체가 꺼져 있어 그 구분 자체가 더 이상 의미가 없습니다.

“지원 중단”에서 “종료”까지 3년

두 공지 모두 한 곳, unsplash.com/documentation/changelog에서 여전히 읽을 수 있습니다. 전문을 인용합니다. 문구 자체가 이야기의 전부이기 때문입니다:

2021년 11월 25일 — “Unsplash Source being deprecated”
“Unsplash Source is being deprecated. Existing uses will continue to work, however for new projects use the full Unsplash API.”

2024년 6월 11일 — “Unsplash Source sunset”
“Unsplash Source has been officially unsupported since its deprecation in 2021. As part of the final sunsetting, we will first wind down by disabling the search feature, and in the coming weeks turn off the application entirely. Existing uses of Source — particularly production-level ones — should migrate as soon as possible to the full Unsplash API.”

두 공지를 순서대로 읽으면 실패 양상이 명확해집니다. 2021년 공지에는 약속(기존 사용은 계속 작동합니다)이 담겨 있었고 날짜는 없었습니다. 2021년에 이 공지를 읽은 개발자는 작동 중인 코드를 그대로 둘 충분한 이유가 있었습니다. 2022년에 합류한 개발자는 애초에 이 공지를 읽은 적이 없었습니다. 2024년 공지는 “앞으로 몇 주 안에”라고 했지만, 그것은 3년이 지난 뒤였고, 아무도 북마크해두지 않은 페이지에서였습니다.

Unsplash는 실제로 지원 중단 정책을 공개하고 있으며, 그 자체는 합리적입니다 — 문서에 따르면 공개적으로 문서화된 필드 및 엔드포인트의 변경 사항은 최소 3주 전에 changelog에 공지되며, 지원 중단 기간 동안 엔드포인트는 Warning 헤더를 반환합니다. 바로 그 문단에 왜 이 정책이 Source를 보호하지 못했는지 설명하는 문장도 있습니다: “공개적으로 문서화되지 않은 필드나 엔드포인트에 대해서는 사전 경고 없이 변경할 수 있습니다.” Source는 애초에 문서화된 API의 엔드포인트가 아니었습니다. 그 정책이 적용될 범위 밖에 있었던 것입니다.

  • 여기서 얻어야 할 실질적 교훈은 “Unsplash가 부주의했다”가 아닙니다. 문서를 전혀 읽지 않고도 쓸 수 있는 URL은, 그 지원 중단 정책 역시 읽은 적 없는 URL이라는 것입니다.
  • 장애는 공지보다 먼저 시작되었습니다. 2022년 11월 28일에 접수된 한 Drupal 이슈는 이미 “항상 Heroku application error가 발생한다”고 보고하고 있습니다 — 종료 공지보다 18개월 앞선 시점입니다. 이런 서비스가 죽는 방식이 바로 이렇습니다: 서서히, 그러다가 여러분이 절대 보지 못할 공지 하나로.

공지 자체를 찾는 것이 장애 재현보다 어렵다

2021년 지원 중단 공지는 changelog.unsplash.com에 게시되었고, 위의 Drupal 이슈를 포함해 당시의 모든 버그 보고서가 이 URL을 링크하고 있습니다. 세 가지 측정 결과:

  1. HTTPS 엔드포인트가 깨져 있습니다. openssl s_client -connect changelog.unsplash.com:443은 tlsv1 alert internal error를 반환합니다 — 인증서가 제시되기도 전에 핸드셰이크가 실패합니다. 따라서 2021년 당시의 https:// 링크는 전부 브라우저에서 죽어 있습니다.
  2. 일반 HTTP로는 리디렉션되지만, 쓸모는 없습니다. http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.html을 따라가면 두 홉 뒤에 unsplash.com/@documentation/changelog/deprecations/2021/11/25/source-deprecation/html에서 400으로 끝납니다 — 경로가 사이트의 사용자명 라우트에 흡수되어버린 것입니다.
  3. 보관소에는 종료 시점에 정확히 구멍이 나 있습니다. Wayback Machine에서 이전 changelog를 마지막으로 성공적으로 캡처한 시점은 2024년 3월 24일이고, 새 changelog를 처음 캡처한 시점은 2024년 8월 23일입니다. 종료 공지는 2024년 6월 11일에 나왔습니다 — 바로 이 다섯 달의 공백 안에서입니다.

이 중 어느 것도 음모가 아닙니다. 그저 일반적인 CMS 이전 작업일 뿐입니다. 하지만 그 결과는 실재하며, 이 글이 두 공지문 전체를 인용하는 이유가 바로 여기에 있습니다: 지원 중단에 대한 1차 기록은 그것이 종료시킨 대상보다 더 오래 살아남아야 하는데, 여기서는 거의 그러지 못했습니다.

실제로 무엇이 망가졌는가

사이드 프로젝트가 아닙니다. 아래 장애들은 모두 공개된 이슈 트래커 항목이며, 제목·날짜·상태는 GitHub와 drupal.org의 API에서 가져왔습니다.

프로젝트 이슈 등록일 내용
MUI (Material UI) #42736 2024년 6월 24일 “[docs] Random Unsplash photo URL is no longer functional” — 공식 Sign-in side 템플릿이 깨진 이미지를 그대로 배포했습니다. 3일 뒤 종료.
Nextcloud #115 2023년 1월 17일 “Migrate to Unsplash API” — 배경화면 앱이 Source URI 위에 구축되어 있었습니다. 18개월간 열려 있다가 2024년 7월 16일 종료.
sindresorhus/Actions #248 2024년 5월 28일 “Get Unsplash Image: 503 Error” — iOS/macOS 단축어(Shortcuts) 액션이 종료 공지가 나오기 2주 전에 이미 깨져 있었습니다.
Drupal — Gin Login #3324054 2022년 11월 28일 “Unsplash has deprecated source.unsplash.com — this delays reCAPTCHA from loading, preventing users from logging in.”

마지막 행을 다시 읽어보세요. 이것이 새겨둘 만한 내용입니다. 로그인 폼 옆에 있던 장식용 이미지 — 페이지에서 가장 명백하게 부수적인 자산 — 가 인증 장애로 번졌습니다. 느린 서드파티 요청이 로그인 폼에 필요한 CAPTCHA보다 앞에 위치했기 때문입니다. 누구도 이렇게 되도록 설계하지 않았습니다. 브라우저가 리소스를 로드하는 순서에서 그냥 그렇게 발생한 것입니다.

코딩 어시스턴트는 이 소식을 받지 못했다

바로 이 부분이 2024년의 장애를 2026년의 문제로 바꿔놓습니다. source.unsplash.com은 대략 8년 동안 문서화되고, 블로그에 소개되고, 강의에서 가르쳐지고, 복사되어 왔습니다. 그 모든 텍스트가 지금 우리의 스타터 코드를 작성하는 모델들의 학습 데이터에 들어 있고, 텍스트는 만료되지 않습니다. 세 가지 수치:

측정 항목2026년 8월 30일 기준 값측정 방법
source.unsplash.com을 포함한 파일 수 3,344 GitHub 코드 검색 API, q=source.unsplash.com (색인된 공개 코드만 — 최솟값이지 전체는 아님)
100개 파일 샘플 중 종료 이후 생성된 저장소 77개 중 12개 동일 쿼리, 결과 100개, 중복 제거 후 77개 저장소, created_at을 2024년 6월 11일과 비교
…그리고 해당 샘플 중 최근 12개월 이내 push된 저장소 77개 중 20개 보관 저장소가 아닌 활성 저장소 — 데모 파일에 여전히 imageUrl: 'https://source.unsplash.com/64x64/?dingo'가 남아 있는 elastic/kibana 포함
unsplash-source-es6의 월간 다운로드 수 23 npm 레지스트리 API — 2022년에 마지막으로 게시된, 죽은 서비스용 래퍼가 여전히 설치되고 있음
이를 언급한 Stack Overflow 게시물 수 1,459 Stack Exchange API, /search/excerpts 총계

가장 직접적인 증거는 애플리케이션 코드가 아니라 프롬프트 안에 있습니다. 해당 검색의 최상위 결과는 GPT 시스템 프롬프트 모음집으로, 여기에는 “please use unsplash API( https://source.unsplash.com/1280x720/?<PUT YOUR QUERY HERE>”라는 문구가 담긴 지시가 있습니다. 이 지시는 오늘날에도 새 어시스턴트에 계속 복사되고 있습니다. 모델은 URL을 검증하지 않습니다. 그것을 사용하라고 지시받았고, 자신이 학습 과정에서 본 모든 예시가 그것에 동의했을 뿐입니다.

따라서 생성된 코드의 실패에는 서로 독립적인 두 가지 원인이 있고, 하나를 고친다고 다른 하나가 고쳐지지 않습니다: 오래된 학습 데이터, 그리고 그 위에 사람이 작성한 오래된 지시문입니다. 어느 쪽이든 증상은 로드되지 않는 이미지라는 같은 부류로 나타납니다:

grep으로 찾아볼 가치가 있는 죽은 이미지 패턴들
source.unsplash.com/random/1200x800   # 2024년 중반부터 503 — 절대 돌아오지 않음
images.unsplash.com/photo-…           # 진짜 CDN이지만, 암기된 ID가 존재하지 않을 수 있음
via.placeholder.com/400               # 회색 사각형, 그대로 프로덕션에 배포됨
placehold.co/800x600                  # 의도적인 회색 사각형
picsum.photos/800/600                 # 실제 사진이지만 페이지 내용과 무관
/placeholder.png                      # 저장소에 애초에 추가된 적 없는 파일

위 목록 둘째 줄에 관한 각주 하나: 이 글을 쓰는 도중, 저희 테스트 네트워크에서도 via.placeholder.com은 TLS 핸드셰이크를 완료하지 못했고, 일반 HTTP에서는 403으로 응답했습니다. 신뢰하기 전에 여러분의 네트워크에서 직접 확인해보세요 — 이런 도구들이 fallback으로 사용하는 대상도 자체적으로 장애 이력을 갖고 있을 수 있습니다.

이 중 첫 번째만 깨져 있습니다. 나머지는 더 미묘한 방식으로 나쁩니다: 로드는 되고, 레이아웃은 완성된 것처럼 보이지만, 페이지가 아무 상관 없는 이미지로 꾸며져 있다는 것을 아무도 알아채지 못합니다. 그리고 이는 이미지에만 국한된 문제가 아닙니다 — 문제의 일반적 형태입니다. 모델이 갖고 있는 웹에 대한 그림은 스냅샷이며, 엔드포인트, CLI 플래그, 패키지 이름, 무료 요금제는 셔터가 닫힌 뒤에도 계속 변화합니다.

마이그레이션 표

목적지는 정확히 세 곳뿐이며, 이를 정직하게 제시하는 방법은 각각에서 무엇을 포기해야 하는지 보여주는 것입니다. 먼저 열을 고르고, 그다음 여러분의 행을 읽으세요.

기존 Source URL A. 고정 CDN URL키 없음 · 무작위성 없음 B. Unsplash API키 필요 · 서버 사이드 호출 C. 직접 운영하는 프록시키 숨김 · 무작위성 복원
/random images.unsplash.com/photo-… — 직접 고른 사진 한 장 GET /photos/random /?w=1600
/random/1600x900 …?w=1600&h=900&fit=crop /photos/random + 반환된 URL에 Imgix 파라미터 추가 /?w=1600&h=900&fit=crop
/1600x900/?apple,desk 대응 방법 없음 — 사진을 직접 골라야 함 /photos/random?query=apple,desk /?query=apple,desk&w=1600
/featured/1600x900?nature 대응 방법 없음 /photos/random?query=nature “featured”에 대응하는 후속 기능 없음 /?query=nature&w=1600
/collection/67920491/1600x900 대응 방법 없음 /photos/random?collections=67920491 /?collections=67920491&w=1600
/user/scottwebb/1600x900 대응 방법 없음 /photos/random?username=scottwebb /?username=scottwebb&w=1600
/daily 사진 한 장을 고정하고 빌드 시 교체 대응 방법 없음 — 무작위 사진 하나를 직접 24시간 캐시해야 함 동일하지만 캐시가 프록시 안에 있음

대부분의 사람들이 실제로 원하는 것은 옵션 A입니다. 이미지가 장식용이었다면 — 히어로 이미지, 로그인 사이드 패널, 카드 배경 — 애초에 요청마다 다른 사진이 필요하지 않았을 것입니다. 하나를 고르고 CDN URL을 유지하면, 페이지는 더 이상 무작위적인 것에 의존하지 않게 됩니다:

고정된, 크기 조절 가능한 Unsplash URL — 키도, API 호출도 필요 없음
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&h=900&fit=crop&auto=format"
     width="1600" height="900" alt="…">
# 공식 지원 파라미터: w, h, crop, fit, fm, auto=format, q, dpr.
# API가 제공한 ixid 파라미터는 그대로 유지할 것 — 조회수 집계에 사용됨.

옵션 B는 공식 경로이며, 호출을 서버 사이드로 옮깁니다. 프런트엔드 JavaScript에 노출된 Client-ID는 공개된 자격 증명이기 때문입니다. 사람들이 자주 걸려 넘어지는 두 가지 규칙에 유의하세요: collections/topics는 같은 요청 안에서 query와 결합할 수 없으며, count(최대 30)를 지정하면 값이 1이더라도 응답 형태가 배열로 바뀝니다.

/random의 공식 대체재
curl "https://api.unsplash.com/photos/random?query=nature&orientation=landscape" \
  -H "Authorization: Client-ID YOUR_ACCESS_KEY" \
  -H "Accept-Version: v1"

# → JSON. 이미지는 .urls.regular / .urls.raw에 있음 (w/h/fit은 직접 추가).
# → X-Ratelimit-Limit: 1000   X-Ratelimit-Remaining: 999

옵션 C: Source를 약 40줄로 재구축하기

여러분이 잃은 것이 정말로 그 동작 방식 자체 — 키 없이, 요청마다 다른 사진을 반환하고, <img> 태그나 CMS 필드, 혹은 서버가 없는 정적 사이트에서 바로 쓸 수 있는 URL — 이었다면, 그 엔드포인트를 직접 운영하는 수밖에 없습니다. API 앞단에 작은 워커 하나를 두면 되며, 실제 프로덕션 환경에서 살아남게 해주는 세 가지 요소는 캐시, 리퍼러 검사, 그리고 나머지 쿼리 문자열을 CDN으로 그대로 전달하는 것입니다.

worker.js — /photos/random 위에 만든 키 없는 Source 형태의 엔드포인트
// Cloudflare Workers. 다른 곳(Deno Deploy, Val Town 등)에서도 형태는 동일하지만,
// caches.default 대신 caches.open()으로 이름 있는 캐시를 열어야 함.
// UNSPLASH_KEY는 서버 사이드에만 존재. 호출자는 절대 이를 볼 수 없음.
const ALLOWED = ["example.com", "www.example.com"];   // 자신의 도메인만
const API_PARAMS = ["query", "collections", "topics", "username", "orientation"];
const TTL = 60;                                       // 초 단위 — 시간당 할당량 보호

const host = (value) => { try { return new URL(value).hostname; } catch { return null; } };

export default {
  async fetch(req, env, ctx) {
    // 0. GET만 허용: Cache API는 그 외의 것을 저장하지 않고,
    //    이미지 엔드포인트에는 응답할 다른 메서드도 없음.
    if (req.method !== "GET")
      return new Response("Method not allowed", { status: 405 });
    const url = new URL(req.url);

    // 1. 자신의 페이지만 이를 삽입할 수 있어야 함 — 공개 인터넷상의
    //    공개 무작위-사진 엔드포인트는 남의 rate limit을 소모시키는 대상이 됨.
    const ref = req.headers.get("referer");       // 정상적인 클라이언트 상당수에서도 없는 값
    if (ref && !ALLOWED.includes(host(ref)))
      return new Response("Forbidden", { status: 403 });

    // 2. 파라미터 조합별로 캐시하여, 이미지 12개가 있는 페이지가
    //    렌더링당 12번이 아니라 분당 1번의 API 호출만 쓰도록 함.
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    // 3. 공식 API에 무작위 사진 요청.
    const api = new URL("https://api.unsplash.com/photos/random");
    for (const p of API_PARAMS)
      if (url.searchParams.has(p)) api.searchParams.set(p, url.searchParams.get(p));

    const r = await fetch(api, { headers: {
      Authorization: "Client-ID " + env.UNSPLASH_KEY,
      "Accept-Version": "v1",
    }});
    // 여기서의 403은 보통 잘못된 키가 아니라 시간당 할당량 문제 — 당황하지 말고 TTL을 늘릴 것.
    if (!r.ok) return new Response("Upstream " + r.status, { status: 502 });
    const photo = await r.json();

    // 4. 이미지 URL 재구성: ixid는 유지하고, 호출자의 크기 파라미터를 덧붙임.
    const img = new URL(photo.urls.raw);          // .raw에는 이미 ixid가 포함되어 있음
    for (const [k, v] of url.searchParams)
      if (!API_PARAMS.includes(k)) img.searchParams.set(k, v);  // w, h, fit, q…

    const res = new Response(null, { status: 302, headers: {
      Location: img.toString(),
      "Cache-Control": "public, max-age=" + TTL,
      // 크레딧은 리디렉션과 함께 전달됨. 헤더 값은 ASCII여야 하므로 인코딩함.
      "X-Photo-Credit": encodeURIComponent(photo.user.name + " on Unsplash"),
      "X-Photo-Link": photo.links.html,
    }});
    ctx.waitUntil(cache.put(req, res.clone()));
    return res;
  },
};

이렇게 해두면 URL 마이그레이션은 단순한 검색-치환 작업이 됩니다. 바로 이 점이 이 옵션에 20분을 들일 가치가 있게 만듭니다:

1대1 치환
- https://source.unsplash.com/collection/67920491/1600x900
+ https://img.example.com/?collections=67920491&w=1600&h=900&fit=crop

설계상의 메모 두 가지가 있는데, 둘 다 미리 적어두지 않았다면 누군가가 나쁜 오후를 보내게 만들었을 것들입니다. 바이트를 프록시하는 대신 리디렉션(302)을 쓰면 대역폭 부담을 지지 않아도 되고, 동시에 Unsplash의 CDN에서 조회수가 집계되도록 유지할 수 있는데, 이는 가이드라인이 요구하는 바이기도 합니다. 그리고 Referer 검사는 헤더가 없을 때 의도적으로 관대하게 처리됩니다 — 상당수의 정상적인 클라이언트가 이를 제거하기 때문입니다 — 그러면서도 여러분의 엔드포인트가 남의 무료 이미지 API가 되어버리는 뻔한 경우는 여전히 막아줍니다.

API를 사용하는 순간 물려받는 규칙들

Source는 계정이 없었기 때문에 규칙도 없었습니다. API에는 아키텍처 설계 방식을 바꾸는 다섯 가지 규칙이 있으며, 모두 현재 문서에서 가져온 것입니다:

  • 속도 제한은 시간 단위이며, 초기에는 낮습니다. 데모 모드에서는 시간당 50회, 애플리케이션이 프로덕션 승인을 받으면 시간당 1,000회입니다. api.unsplash.com에 대한 호출만 집계되며 — images.unsplash.com에 대한 이미지 요청은 집계되지 않습니다. 모든 응답에서 X-Ratelimit-Remaining을 확인하세요.
  • 핫링킹(hotlinking)은 허용 수준이 아니라 의무입니다. Unsplash는 API가 반환하는 이미지 URL을 직접 삽입할 것을 요구합니다. 그래야 사진 조회수가 사진작가에게 귀속될 수 있기 때문입니다. 파일을 자신의 CDN으로 미러링하는 것은 마음대로 할 수 있는 최적화가 아닙니다.
  • ixid 파라미터를 유지하세요. 반환된 URL의 크기 조절이나 크롭은 예상된 사용이지만, 여러분의 애플리케이션을 식별하는 파라미터를 제거하는 것은 그렇지 않습니다.
  • 저작자 표시와 다운로드 추적도 계약의 일부입니다 — 사진작가와 Unsplash가 크레딧을 받으며, 사용자가 파일을 받을 때는 사진의 다운로드 엔드포인트를 통해 “다운로드”가 보고됩니다. 이는 여러분이 직접 발생시켜야 하는 이벤트입니다.
  • 배포되는 제품에는 동적 클라이언트 등록이 필요합니다. 플러그인, 테마, 자체 호스팅 CMS를 배포한다면, 하나의 공유 키는 정책 위반이자 단일 장애점이 됩니다. API에는 바로 이런 경우를 위한 등록 절차가 마련되어 있습니다.

이쯤에서 범위에 대해 솔직해질 필요가 있습니다: 어차피 API와 키를 연동해야 한다면, 어떤 이미지 API를 쓸지는 이제 완전히 열린 선택지가 되고, 클라이언트를 작성하기 전에 5분 정도 투자할 가치가 있습니다. 저희는 무료 API들을 — 할당량, 규칙, 검색 동작, 응답 형태 — 비교해서 무료 스톡 사진 API 비교에 정리해두었습니다.

자리표시자만 필요했다면, 그렇게 말하세요

Source 사용량의 상당 부분은 애초에 Unsplash와 무관했습니다. “레이아웃을 만드는 동안 이미지 모양의 뭔가를 여기 놓아두자”는 것이었습니다. 이런 용도라면 키 없는 서비스가 여전히 존재하며, 그것이 올바른 답입니다:

서비스키 필요?제공하는 것한계
Lorem Picsum 아니요 실제 사진: picsum.photos/800/600, /id/237/…나 /seed/xxx/…로 고정된 이미지, 그리고 ?grayscale과 ?blur=1..10. /v2/list 엔드포인트는 각 사진의 Unsplash 페이지와 작가를 표시합니다. 피사체 타겟팅이 전혀 없음. 사진이 페이지 내용과 무관합니다.
placehold.co 아니요 원하는 크기의 레이블 붙은 사각형 — 정직한 와이어프레임용 채움 이미지. 그냥 회색 상자이고, 클라이언트에게 공유하는 스크린샷에서도 그렇게 보입니다.
Openverse 아니요 WordPress.org가 운영하는, 공개 API를 갖춘 오픈 라이선스 카탈로그. 키워드 매칭 방식이며, 라이선스가 항목마다 다릅니다 — 직접 확인해야 합니다.

중요한 구분점은 이것입니다: 자리표시자는 정의상 임시적입니다. 이미지가 프로덕션까지 살아남는다면 그것은 더 이상 자리표시자가 아니라, 아무도 고르지 않은 삽화이며, 독자는 그것을 알아챌 수 있습니다.

세 개 대신 키 하나

누구도 계획하지 않는 마이그레이션 부분이 여기 있습니다: Source를 떠나는 사람들이 하나의 API로 정착하는 경우는 드뭅니다. 페이지 하나에 히어로 이미지, 섹션 이미지 두 개, 카드 그리드용 이미지가 필요하다면, 정직한 답은 대개 Unsplash 더하기 Pexels 더하기 Pixabay입니다 — 같은 <img> 태그를 채우기 위해 등록 3건, 인증 방식 3가지, JSON 형태 3가지, 페이지네이션 모델 3가지, 저작자 표시 규칙 3세트가 필요합니다. 그 통합 작업이야말로 키 없는 URL이 사라진 것에 대한 진짜 청구서이며, 그것을 유발한 장애가 발생한 몇 주 뒤에 도착합니다.

이를 하나의 통합으로 압축하기 위해 저희는 Pexafy를 만들었습니다: 9개의 무료 라이선스 라이브러리를 하나의 스키마 아래, 하나의 키로 다루며, 문장 단위 의미 검색을 지원합니다 — “위에서 내려다본 나무 책상 위의 금 간 휴대폰 화면”처럼 완전한 설명을 입력하면 아무 결과도 없는 대신 순위가 매겨진 결과가 반환됩니다. 두 가지 제약을 분명히 밝힙니다. 이 글 전체가 다시는 놀라지 말자는 취지이기 때문입니다: 이것은 키가 필요하므로 Source가 하던 것을 그대로 복원하지는 않습니다 — 위에서 언급한 공식 Unsplash API와 같은 범주에 속합니다. 그리고 취급하는 것은 무료 라이선스 사진이지, 편집용이나 브랜드 이미지가 아닙니다.

진짜로 새로운 부분은 위에서 다룬 어시스턴트들을 겨냥하고 있습니다: mcp.pexafy.com/mcp에 있는 MCP 서버 덕분에, 그렇지 않았다면 기억 속의 이미지 URL을 그대로 읊었을 모델이 실제 카탈로그를 검색해서 실제로 존재하는 사진을, 저작자 표시와 함께 반환할 수 있습니다. 이는 기계가 작성한 죽은 URL 문제에 대해 어떤 lint 규칙보다도 나은 답이며, 그 근거는 AI 에이전트를 위한 이미지 검색 인프라에 정리되어 있습니다.

10분 만에 여러분의 자산을 감사하기

무엇으로 마이그레이션하든, 이 부분을 먼저 하세요 — 찾지 못한 URL은 고칠 수 없습니다. Source는 오늘의 사례일 뿐이며, 동일한 세 단계가 여러분이 삽입하는 모든 외부 자산에 적용됩니다.

죽은 참조를 모두 찾은 다음, 다시 들어오지 못하게 막기
# 1. 문서, 테스트, 픽스처, README를 포함해 저장소 안의 모든 것.
grep -rn --binary-files=without-match \
  -e "source.unsplash.com" -e "via.placeholder.com" -e "/placeholder.png" .

# 2. 데이터베이스가 갖고 있는 모든 것 — CMS 본문이야말로 이런 것들이 가장 오래 숨어 있는 곳.
psql -c "SELECT id FROM posts WHERE body LIKE '%source.unsplash.com%'"

# 3. 빌드된 사이트가 실제로 요청하는 모든 것: 크롤링해서 실패 목록을 뽑기.
#    파일 확장자가 아니라 속성 자체를 매칭할 것 — 이미지 URL이 .jpg로 끝나는 경우는 드묾.
grep -rhoE 'src="[^"]+"' dist/ \
  | cut -d'"' -f2 | grep -E '^https?://' | sort -u \
  | xargs -P8 -I{} curl -s -o /dev/null -w "%{http_code} {}\n" {} \
  | grep -v "^200"

# 503 https://source.unsplash.com/random/1200x800   ← 찾고 있던 것

그런 다음, 외부 이미지에 얼마만큼의 비용을 지불할지 한 번에 정하세요. 다음 종료를 누가 일으키든 살아남을 네 가지 규칙입니다:

  1. 요청 시점이 아니라 빌드 시점에 가져오세요. 빌드 중에 해석되는 이미지는 새벽 3시 사용자 앞이 아니라 CI에서, 개발자 앞에서 실패합니다.
  2. 장식용 자산이 절대 핵심 경로를 막지 않게 하세요. 로그인 폼 위에 외부 리소스를 미리 로드하지 마세요. 모든 서드파티 <img>에는 onerror 대체 처리와 명시적인 width/height를 지정해서, 실패가 레이아웃 이동이나 스크립트 정지가 아니라 빈 상자 하나로 끝나게 하세요.
  3. 이 검사를 CI에 추가하세요. 위의 3단계를 빌드 산출물에 대해 실행하면, “누군가 결국 알아챘다”가 빨간색 빌드로 바뀝니다. 재발을 막는 유일한 단계입니다.
  4. 외부 의존성도 다른 것과 마찬가지로 예산을 잡으세요. 여러분의 페이지가 어떤 호스트에 의존해도 되는지, 각각이 다운되면 어떻게 되는지 적어두세요. 등록할 필요조차 없었던 URL도 여전히 하나의 의존성입니다 — Source는 그것이 그저 아무도 소유하지 않은 의존성이라는 것을 증명했을 뿐입니다.

참고 자료 및 각주

1 이 글에 나오는 모든 상태 코드, 수치, 인용문은 2026년 8월 30일에 각각의 1차 출처에서 가져온 것입니다. HTTP 상태 코드는 각 URL 패턴에 대해 curl로 확인했으며, 전부 server: Heroku와 herokucdn.com/error-pages/application-error.html이 포함된 본문과 함께 503을 반환했습니다. DNS 확인도 같은 날 확인했습니다(herokudns.com 호스트에 대한 CNAME).

2 두 changelog 항목 모두 unsplash.com/documentation/changelog에서 그대로 인용했습니다. 지원 중단 정책 문구(3주 전 공지, Warning 헤더, 공개적으로 문서화되지 않은 엔드포인트에 대한 예외)는 같은 날 unsplash.com/documentation에서 가져왔습니다.

3 TLS 실패는 openssl s_client -connect changelog.unsplash.com:443(tlsv1 alert internal error)로 재현했습니다. 리디렉션 체인은 curl -L로 추적했습니다. 아카이브 공백은 Wayback CDX API에서 가져왔습니다: changelog.unsplash.com의 마지막 200 캡처는 2024년 3월 24일, unsplash.com/documentation/changelog의 첫 캡처는 2024년 8월 23일입니다.

4 이슈 제목, 생성일과 종료일은 GitHub REST API(mui/material-ui#42736, nextcloud/unsplash#115, sindresorhus/Actions#248)와, 본문에 2022년 11월 “항상 Heroku application error가 발생한다”고 보고한 gin_login 이슈 3324054에 대한 drupal.org JSON API에서 가져왔습니다.

5 수치: GitHub 코드 검색 API (3,344개 파일; 100개 결과 샘플을 중복 제거해 77개 저장소로 만들었고, 이 중 12개는 2024년 6월 11일 이후 생성, 20개는 이전 12개월 안에 push됨); npm 레지스트리 다운로드 API(unsplash-source-es6, 이전 30일 동안 23회 다운로드); Stack Exchange /search/excerpts(1,459개 게시물). 코드 검색은 색인된 공개 저장소만 다루므로, 각 수치는 최솟값입니다.

자주 묻는 질문

source.unsplash.com이 다운된 건가요, 아니면 완전히 종료된 건가요?
완전히 종료되었습니다. Unsplash는 2024년 6월 11일에 서비스 종료를 발표했습니다 — “먼저 검색 기능을 비활성화하며 단계적으로 축소한 뒤, 앞으로 몇 주 안에 애플리케이션을 완전히 종료할 것”이라며, 이는 2021년 11월 25일 서비스가 지원 중단(deprecated)된 이후의 일입니다. 모든 패턴(/random, /1600x900/?query, /collection/…, /daily)이 Heroku의 일반적인 Application Error 페이지와 함께 HTTP 503을 반환합니다. 호스트명은 여전히 확인되므로, 장애는 네트워크 오류가 아니라 깨진 이미지로 나타납니다.
source.unsplash.com/random을 대체할 직접적인 방법은 무엇인가요?
키 없이 바로 대체할 방법은 없으며, 이 점을 일찍 받아들이는 것이 중요합니다. 대체 방법은 세 가지입니다. 고정된 CDN URL — images.unsplash.com/photo-…?w=1600&h=900&fit=crop — 은 키가 필요 없지만 항상 같은 사진을 반환하며, 이는 사실 대부분의 장식용 용도에 필요했던 전부입니다. 공식 API인 GET https://api.unsplash.com/photos/random은 Authorization: Client-ID 헤더와 함께 무작위성을 되살려주지만 반드시 서버 사이드에서 호출해야 합니다. 해당 엔드포인트 앞에 직접 구축한 작은 프록시를 두는 것만이 <img> 태그에 바로 넣을 수 있는 키 없는 URL을 돌려주는 유일한 방법입니다.
2026년에도 AI 코딩 도구가 왜 여전히 source.unsplash.com URL을 생성하나요?
이 URL이 약 8년간 문서화되고, 가르쳐지고, 복사되어 왔기 때문이며, 학습 데이터는 서비스가 종료된다고 해서 만료되지 않습니다. 2026년 8월 30일 기준 측정 결과: GitHub 코드 검색에서 여전히 3,344개 파일이 이를 포함하고 있으며, 100개 파일 샘플 중 77개 저장소 중 12개가 서비스 종료 이후에 생성되었습니다. 사람이 직접 작성한 프롬프트 라이브러리에서도 같은 지시가 반복됩니다 — 널리 복사된 한 GPT 프롬프트는 여전히 모델에게 “use unsplash API( https://source.unsplash.com/1280x720/?… )”를 사용하라고 지시합니다. 모델이 작성한 어떤 이미지 URL도 검증되지 않은 것으로 취급하고, CI에서 상태 코드를 확인하세요.
API 키 없이도 여전히 무작위 Unsplash 사진을 받을 수 있나요?
Unsplash에서 직접은 불가능합니다 — 무작위 선택은 이제 /photos/random 뒤에 있으며, 이는 Client-ID를 필요로 합니다. 키 없이 사용할 수 있는 두 가지 경로는, 키가 서버 사이드에 남고 공개 URL이 예전과 비슷하게 보이는 자체 호스팅 프록시, 또는 Lorem Picsum(picsum.photos/800/600) 같은 제3자 플레이스홀더 서비스입니다. 후자는 키 없이 실제 사진을 제공하지만 주제 지정 기능은 전혀 없습니다.
Unsplash API는 이미지를 다운로드해 직접 호스팅하는 것을 허용하나요?
아니요. 대부분의 API와 달리 Unsplash는 핫링킹을 요구합니다: API가 반환하는 이미지 URL은 반드시 직접 임베드되어야 하며, 이를 통해 사진 조회수가 사진작가에게 집계됩니다. 여기에는 세 가지 의무가 따릅니다 — URL을 리사이즈하거나 크롭할 때 ixid 매개변수를 유지할 것, 사진작가와 Unsplash를 명시할 것, 사용자가 파일을 받을 때 사진의 다운로드 엔드포인트를 호출할 것. 파일을 자체 CDN으로 미러링하는 것은 허용되지 않는 유일한 최적화입니다.
2021년의 지원 중단 공지는 왜 기존 사용자를 보호하지 못했나요?
공지와 정책이 서로 다른 것을 다루었기 때문입니다. 2021년 11월 25일자 변경 로그 항목은 “기존 사용은 계속 작동할 것”이라 약속했고 종료일을 명시하지 않았습니다. Unsplash가 공표한 지원 중단 정책 — 최소 3주 전 공지와 Warning 헤더 — 은 공개적으로 문서화된 필드와 엔드포인트에만 적용되며, 같은 단락에서 문서화되지 않은 것은 아무 경고 없이 변경될 수 있다고 명시하고 있습니다. Source는 API의 문서화된 엔드포인트였던 적이 없으므로, 그것을 보호해줄 정책의 범위 밖에 있었습니다.
내 프로젝트에서 죽은 source.unsplash.com URL을 모두 찾으려면 어떻게 해야 하나요?
세 번의 검사, 10분이면 충분합니다. 저장소를 grep하되 문서, 테스트, 픽스처, README까지 포함하세요 — grep -rn "source.unsplash.com" . — 이런 URL은 샘플 코드에서 가장 오래 살아남기 때문입니다. 데이터베이스를 쿼리하세요, CMS 게시글 본문이 숨어있는 곳이기 때문입니다(WHERE body LIKE '%source.unsplash.com%'). 그런 다음 빌드된 결과물을 크롤링하세요: 모든 이미지 URL을 추출해 각각 요청하고, 200이 아닌 항목을 모두 나열하세요. 이 마지막 단계를 CI에 추가하면, 죽은 제3자 자산이 페이지가 아니라 빌드를 실패시키게 됩니다.

키워드를 찾아 헤매지 마세요. 의미하는 바를 묘사하세요.

9M+개의 자유 이용 이미지를 의미로 검색하세요 — 어떤 언어로든, 100 ms 이내에.