me:mory 프로젝트 — 소중한 사람을 기억해주는 iOS 앱을 혼자 만들고 있습니다.
① 문제 정의
② 가입자 절반이 첫날 떠난 이유
③ 네이버 API가 사라진 날
1. 증상 — 잘 되던 기능이 조용히 죽어 있었다
제가 만드는 앱에 선물 추천 기능이 있습니다. 사용자가 담아둔 기억(예: "캠핑 의자 갖고 싶다고 했음")을 근거로 실제 상품을 추천해주는 기능이에요.
어느 날 "선물 추천이 계속 실패한다"는 제보를 받고 로그를 봤더니, 네이버 쇼핑 검색 API가 404를 뱉고 있었습니다.
GET https://openapi.naver.com/v1/search/shop.json?query=캠핑의자&display=20
HTTP/1.1 404 Not Found
{
"errorMessage": "존재하지 않는 검색 api 입니다.",
"errorCode": "SE05"
}
SE05. 네이버 검색 API 문서에서 이 코드는 "존재하지 않는 검색 api"입니다. 요청 경로가 틀렸을 때 나오는 코드인데, 저는 그 URL을 몇 주 동안 바꾼 적이 없었습니다.
로그를 거슬러 올라가 보니 마지막 성공 호출이 7월 25일이었습니다. 그 이후는 전부 404. 공식 공지를 찾아봤지만 제가 확인한 범위에서는 명시적인 종료 안내를 발견하지 못했습니다.
2. 진단 — 내 키 문제인가, API가 없어진 건가
이때 판단이 갈립니다. 키가 만료됐거나 권한이 빠진 거라면 복구할 수 있고, API 자체가 없어진 거라면 기능을 다시 설계해야 하니까요.
그래서 같은 키로 네이버 검색 API 13종을 전부 호출해봤습니다. 임시 진단용 함수를 하나 만들어서 돌리고 바로 지웠습니다.
검색 API 엔드포인트 결과
| 블로그 | blog.json | ✅ 200 |
| 뉴스 | news.json | ✅ 200 |
| 백과사전 | encyc.json | ✅ 200 |
| 카페글 | cafearticle.json | ✅ 200 |
| 지식iN | kin.json | ✅ 200 |
| 지역 | local.json | ✅ 200 |
| 오타변환 | errata.json | ✅ 200 |
| 웹문서 | webkr.json | ✅ 200 |
| 이미지 | image.json | ✅ 200 |
| 성인검색어 | adult.json | ✅ 200 |
| 쇼핑 | shop.json | ❌ 404 SE05 |
| 책 | book.json | ❌ 404 SE05 |
| 전문자료 | doc.json | ❌ 404 SE05 |
결론이 명확해졌습니다. 키·권한은 정상이고, 세 개의 API만 사라졌습니다.
한 가지 더 헷갈렸던 것: 네이버 개발자센터의 API 사용 통계에도 호출 수가 잡히지 않았습니다. 분명 호출은 하고 있는데 통계는 0이에요. 폐지된 API는 애초에 라우팅 전에 걸러지는지, 집계 대상에서 빠진 것으로 보입니다. "통계가 0이니 우리 앱이 호출을 안 하고 있나?" 하고 한참 엉뚱한 곳을 봤습니다.
💡 같은 증상을 겪고 계시다면: 키를 재발급하거나 권한 설정을 뒤지는 데 시간 쓰지 마세요. blog.json을 같은 키로 한 번 호출해보면 5초 만에 판별됩니다. 200이 오면 키는 멀쩡한 겁니다.
3. 왜 일주일이나 늦게 알았나 — 에러 코드를 뭉뚱그린 대가
저는 이 사건을 약 일주일 늦게 인지했습니다.
원인은 제 코드에 있었습니다. 당시 서버는 이런 구조였어요.
const products = await searchNaverShop(query);
if (products.length === 0) {
return json(404, { error: "no_products" }); // ← 문제의 코드
}
no_products라는 하나의 코드가 두 가지 완전히 다른 상황을 덮고 있었습니다.
- 검색은 성공했는데 조건에 맞는 상품이 없음 (정상 상황)
- 검색 API 호출 자체가 실패함 (장애)
앱은 두 경우 모두 "추천할 상품을 찾지 못했어요"라는 안내를 띄웠습니다. 그래서 API가 죽은 게 "이 사람 취향에 맞는 상품이 마침 없나 보다"로 위장됐던 겁니다.
지금은 실패 원인별로 상태 코드를 나눴습니다. "결과 없음"과 "호출 실패"는 절대 같은 코드를 쓰면 안 됩니다. 모니터링을 안 붙이더라도 이것만은 지켜야 장애를 장애로 볼 수 있습니다.
4. 대체 API 전수 조사 — 국내에 쓸 게 없었다
기능을 살리려면 실존 상품 데이터를 주는 다른 공급원이 필요했습니다. 이틀 동안 조사한 결과입니다. (조건은 모두 2026년 8월 기준)
공급원 상태 서비스 적용 가능?
| 쿠팡 파트너스 | 제휴 마케팅 API 제공 | ❌ 최종 승인 조건이 누적 매출 15만 원, 게다가 상품 검색(Search) API가 시간당 10회 제한. 서비스 트래픽을 감당할 수 없음 |
| 11번가 오픈API | 셀러(판매자) 전용으로 전환 | ❌ 개인 셀러 가입 자체가 막혀 있음 (직접 시도) |
| 다나와 API | 서비스 종료 | ❌ |
| 카카오 쇼핑 | 판매자용 API만 존재 | ❌ 상품 검색 공개 API 없음 |
| 링크프라이스 등 제휴 네트워크 | 링크 생성·정산용 | ❌ 상품 검색 API가 아님 |
| 알리익스프레스 / Amazon PA-API | 사용 가능 | ❌ 국내 사용자 대상 선물 추천에 부적합(배송·가격대) |
정리하면 국내 커머스 상품 검색을 서비스에서 자유롭게 쓸 수 있는 공개 API는 현재 사실상 없습니다. 네이버가 그 마지막 창구였던 셈이고, 그게 닫힌 겁니다.
(참고로 카카오 선물하기도 검색 API는 없습니다. 기프티콘 발송 API는 존재하지만 사업자 등록과 제휴 계약이 필요한 B2B 상품이라 개인 개발자가 접근할 수 없습니다.)
5. 해결 — API를 버리고 "검색 결과 페이지"로
공급원이 없으니 기능 정의 자체를 바꿨습니다.
기존 구조 (3단계)
① LLM #1: 기억 → 검색 키워드 추출
② 네이버 쇼핑 API: 실존 상품 후보 20개 검색 ← 여기가 사라짐
③ LLM #2: 후보 안에서만 선별 + 추천 이유 작성
④ 서버 후처리: productId 실재 여부 검증
②가 있었던 이유는 할루시네이션 방지였습니다. LLM에게 그냥 "선물을 추천해줘"라고 하면 존재하지 않는 상품명과 가격을 그럴듯하게 지어냅니다. 그래서 "지어내지 말고 주어진 후보 중에서 고르기만 해"라는 구조를 만든 거였죠. ④는 그마저도 못 믿어서 넣은 검증 단계고요.
새 구조 (1단계)
① LLM 1회: 기억 → 선물 아이디어(상품군) + 검색어 + 추천 이유
② 서버: 검색어 → 네이버쇼핑 검색 결과 페이지 URL 생성
핵심은 LLM이 "상품"이 아니라 "상품군"까지만 말하게 한 것입니다.
- ❌ 기존: "OO브랜드 캠핑 체어 89,000원" (실존 여부 검증 필요)
- ✅ 변경: "캠핑 경량 체어" + 탭하면 네이버쇼핑 검색 결과로 이동
상품군은 지어낼 수가 없습니다. "캠핑 경량 체어"는 검증할 대상이 아니라 카테고리니까요. 할루시네이션 표면 자체가 사라졌습니다.
링크 생성 코드는 이렇게 단순합니다. 예산 필터도 URL 파라미터로 그대로 걸립니다.
const BUDGET_URL_RANGE: Record<string, [number="" |="" null,="" number="" null]=""> = {
"3만원 이하": [null, 30000],
"3~5만원": [30000, 50000],
"5~10만원": [50000, 100000],
"10만원 이상": [100000, null],
};
function searchLink(query: string, budget: string | null): string {
const u = new URL("https://search.shopping.naver.com/search/all");
u.searchParams.set("query", query);
if (budget && BUDGET_URL_RANGE[budget]) {
const [min, max] = BUDGET_URL_RANGE[budget];
if (min !== null) u.searchParams.set("minPrice", String(min));
if (max !== null) u.searchParams.set("maxPrice", String(max));
}
return u.toString();
}
</string,>
LLM 프롬프트에서 검색어를 만들 때 성별·연령대·사이즈 같은 맥락을 넣게 했습니다. "여성 운동화 240"처럼요. 결국 사용자가 직접 검색창에 칠 법한 문장을 대신 만들어주는 것이 기능의 본질이 된 셈입니다.
잃은 것과 얻은 것
| 잃음 | 상품 사진·가격·판매처가 박힌 카드 UI. 시각적으로 확실히 밋밋해졌다 |
| 얻음 ① | 공급 중단 리스크 0. API가 아니라 일반 웹 링크라 네이버가 뭘 정리하든 안 죽는다 |
| 얻음 ② | 할루시네이션 표면 0. 검증 로직(④)이 통째로 필요 없어졌다 |
| 얻음 ③ | LLM 호출 2회 → 1회. 응답 속도와 비용이 절반 |
| 얻음 ④ | 외부 API 키 의존 하나 제거 (시크릿 관리 대상 감소) |
6. 남는 교훈 세 가지
① 외부 API에 기능의 뼈대를 맡길 땐 "그 부분만 갈아끼울 수 있는지" 먼저 보라.
운이 좋았던 건 API 호출부가 LLM 파이프라인과 분리돼 있어서, 공급원 부분만 들어내고 하루 만에 재구성할 수 있었다는 점입니다. 만약 검색 응답 형태가 프롬프트와 응답 스키마 전체에 얽혀 있었다면 기능을 통째로 버려야 했을 겁니다.
② "결과 없음"과 "호출 실패"를 같은 에러 코드로 묶지 마라.
이 하나 때문에 장애를 일주일 늦게 발견했습니다. 정상 상황과 장애 상황이 사용자에게 같은 화면으로 보이면, 그 기능은 죽어도 조용히 죽습니다.
③ 때로는 "데이터를 가져오는 것"보다 "사용자를 보내는 것"이 낫다.
처음엔 상품 카드를 포기하는 게 기능 후퇴라고 생각했는데, 만들고 나니 오히려 정직한 구조였습니다. 우리가 잘할 수 있는 건 "이 사람에게 무엇이 어울리는지 기억을 근거로 판단하는 것"*이지 "상품 DB를 흉내내는 것"이 아니었으니까요. 각자 잘하는 걸 하고 그 사이를 링크로 잇는 게 맞았습니다.
나중에 쓸 만한 상품 검색 공급원이 생기면 아이디어→후보 공급부만 다시 끼우면 됩니다. 그때까진 이대로 갑니다.
이 글은 제가 만들고 있는 iOS 앱 me:mory 개발기의 일부입니다.
소중한 사람에 대해 알게 된 걸 담아두면, 필요한 순간에 알아서 꺼내주는 앱이에요.
👉 App Store: https://apps.apple.com/kr/app/me-mory/id6790031020?ct=blog
'Side project > me:mory' 카테고리의 다른 글
| #5 유저들은 내가 두 달 공들인 기능을 쓰지 않았다. (0) | 2026.08.30 |
|---|---|
| #4. 유저 19명, 검색 유입 0 — 그래서 스레드 계정을 새로 팠습니다 (1) | 2026.08.04 |
| #2 신규 가입자 절반이 첫날 이탈했다 — 코호트 분석으로 찾은 진짜 원인 (0) | 2026.08.02 |
| #1 아몬드 알레르기에서 시작한 사이드 프로젝트 (0) | 2026.08.02 |