PM의 API 실습 2편 – 검색 기능, 직접 뜯어보기

PM의 API 실습 2편 — 검색 기능

검색창에 ‘Product Manager’를 입력하고 Enter를 눌렀을 때, 구체적으로 뒤에서 무슨 일이 벌어지는지 궁금했습니다. 1편에서 피드 API를 관찰했으니 이번엔 ‘검색’으로 범위를 좁혀 같은 방식으로 따라가 봤습니다.

결론부터 말하면, 예상했던 것보다 훨씬 복잡했고 확인하지 못한 부분도 있었습니다. 그 과정 그대로 기록합니다.


왜 검색 API를 보려고 했는가

기획서에 ‘검색어 입력 → 결과 표시’라는 흐름 자체는 그릴 수 있습니다. 그런데 실제 서비스에서 검색이 어떤 파라미터를 주고받는지, 요청이 어떤 구조로 이루어지는지는 추상적인 흐름만으로는 정확한 내용을 파악하기 어려웠습니다. 기획 작업 시 좀 더 구체적인 근거를 갖고 싶어서 직접 API를 열어봤습니다.

검색어만이 아니라 필터 조건, 카테고리, 페이지 정보까지 서버에 전달됩니다. API를 직접 관찰하고 나서야 기획서에 어떤 항목을 명시해야 하는지 감이 생겼습니다.

개발팀과 대화할 때도 큰 도움이 될 것 같습니다. “검색 조건이 URL에 붙는 방식인가요, body로 가나요?”라는 내용을 이해할 수 있게 됐습니다.


실습 전에 먼저 알아야 했던 것 — URL 인코딩

Network 탭을 열었을 때 URL에 이런 문자들이 가득했습니다.

Product%20Manager
Product+Manager

처음 보면 암호처럼 느껴집니다. 찾아보니 공백이나 한글처럼 URL에 그대로 쓸 수 없는 문자를 변환한 것이라고 합니다.

Product Manager  →  Product%20Manager   (%20 = 공백)
Product Manager  →  Product+Manager     (+ 도 공백을 의미)

%20+ 둘 다 공백을 나타내는 인코딩 방식입니다. 이걸 모르면 URL을 봐도 어떤 검색어로 요청했는지 읽을 수가 없었습니다.


실습 순서

준비물: Chrome + LinkedIn 로그인 상태

  1. F12로 DevTools 열기
  2. Network 탭 선택 → Fetch/XHR 필터 클릭
  3. LinkedIn 검색창에 Product Manager 입력 후 Enter
  4. Network 탭 상단 Filter 입력란에 search 입력
search 필터 적용 후 요청 목록
search 필터 적용 후 요청 목록

목록에서 이 항목을 찾습니다.

all/?keywords=Product+Manager&origin=GLOBAL_SEARCH_HEADER

Headers 탭에서 확인한 것

항목을 클릭하고 Headers 탭을 봤습니다.

 Headers 탭 General 섹션
Headers 탭 General 섹션

General 섹션에서 확인한 값들

Request URL:    https://www.linkedin.com/flagship-web/search/results/all/
                       ?keywords=Product+Manager&origin=GLOBAL_SEARCH_HEADER
Request Method: POST
Status Code:      200 OK
Remote Address: 172.64.146.215:443

여기서 처음으로 막혔습니다. 검색 요청인데도 Request Method가 POST였습니다. 1편에서 피드 조회도 POST를 쓴다는 걸 확인했었는데, 검색도 마찬가지였습니다. “검색 = GET”이 원칙이지만 LinkedIn처럼 복잡한 서비스에서는 예외가 있다는 걸 다시 한번 직접 확인했습니다.

Authorization 헤더가 없는 이유

1편에서 확인했던 것처럼 LinkedIn은 Authorization 헤더 대신 Cookie 항목 안에 인증 정보를 담는 방식을 사용합니다. 검색 API에서도 동일하게 적용되어 있었습니다. li_at 쿠키가 로그인을 증명하는 값으로, 이 값이 없거나 만료되면 서버는 데이터를 돌려주지 않습니다.

헤더 방식이든 쿠키 방식이든 “로그인한 사용자만 쓸 수 있는 API”라는 구조 자체는 동일합니다. 1편에서 정리했듯 비로그인 사용자에게 어디까지 데이터를 열어줄지는 기획 단계에서 명확하게 정의해야 할 부분입니다.


Payload 탭에서 확인한 것 — 예상과 달랐던 부분

headers request payload

실행 전에는 필터 조건이 URL 파라미터에 담길 거라고 예상했습니다. 실제로 열어보니 URL에는 keywordsorigin 두 개뿐이었고, 실제 조건들은 전부 Request Payload 안에 담겨 있었습니다.

Query String Parameters (URL에 붙은 값)

keywords: Product+Manager
origin:   GLOBAL_SEARCH_HEADER

origin은 어디서 검색을 시작했는지를 나타내는 값입니다. GLOBAL_SEARCH_HEADER는 상단 검색창에서 검색했다는 뜻입니다. LinkedIn이 사용자 행동을 분석하는 용도로 쓰는 값으로 보여서, 기획서에 따로 명시할 필요는 없을 것 같습니다.

Request Payload — 실제 검색 조건이 담긴 곳

pageKey: "search_srp_all"
requestedArguments:
  payload:
    keywords: "Product Manager"
    origin:   "GLOBAL_SEARCH_HEADER"
url: "/search/results/all/?keywords=Product+Manager&origin=GLOBAL_SEARCH_HEADER"

requestedArguments 안의 payload서버에 전달되는 실제 검색 조건입니다. URL에서는 Product+Manager로 붙어 보이던 검색어가 여기서는 Product Manager로 띄어쓰기가 살아난 형태로 표시됩니다.

pageKey: "search_srp_all"에서 srp는 Search Results Page, all은 통합 검색이라는 의미입니다. 이 값이 검색 카테고리를 구분하는 기준이 됩니다. 실제로 채용공고 탭으로 이동하면 이 값이 nlsearch_srp_jobs로 바뀌는 것을 확인할 수 있었습니다.


채용공고 필터 선택 후 Payload 변화

채용공고 카테고리로 전환하고 Payload를 다시 확인했습니다.

채용공고필터적용 payload

달라진 값들

# 처음 검색
pageKey: "search_srp_all"
url: "/search/results/all/..."

# 채용공고 카테고리 선택 후
pageKey: "nlsearch_srp_jobs"
url: "/jobs/search-results/..."

URL에 filters=... 같은 파라미터가 추가될 거라고 예상했는데, URL은 거의 그대로였습니다. 대신 Payload 안의 pageKeysearch_srp_all에서 nlsearch_srp_jobs로 바뀌었습니다. 경로에 /jobs/가 추가된 것도 확인했습니다.

필터 조건이 URL이 아닌 Payload 안에서 처리된다는 걸 이때 처음 이해했습니다.

기획 관점에서 중요한 포인트가 있었습니다. 필터를 적용했는데 URL이 바뀌지 않으면 그 상태에서 링크를 공유했을 때 필터가 유지되지 않을 수 있습니다. “필터 적용 상태에서 URL 공유가 가능해야 하는가”를 기획 단계에서 결정해야 한다는 걸 처음 인식했습니다.

states 값도 눈에 들어왔습니다

states: [{key: "JobSearchResultsPage_SemanticJobFirstJobIdBindingKey", ...}]

채용공고 검색 결과에서 첫 번째 항목의 ID를 미리 메모리에 저장해두는 구조입니다. 쉽게 말해, 검색 결과가 열리기 전에 첫 번째 채용공고가 무엇인지 미리 받아두는 값입니다.

LinkedIn 채용공고 화면을 보면 왼쪽 목록과 오른쪽 상세가 동시에 보이는데, 아무것도 클릭하지 않아도 첫 번째 공고가 이미 열려 있습니다. 이 구조 덕분입니다. “검색 결과 진입 시 첫 번째 항목을 자동 노출된 상태로 보여준다”는 것이 기획 사항이 된다는 걸 이해했습니다.


기간 필터 선택 후 — Payload에서 필터 값 확인

채용공고 상단 올린 날 ▼ 을 클릭하면 정렬 옵션 대신 기간 필터가 나왔습니다.

지난 1개월
지난 1주
최근 24시간

실습 전 예상했던 RELEVANCE vs DATE_POSTED 같은 정렬 옵션은 채용공고 검색에서는 존재하지 않았습니다. LinkedIn은 정렬 대신 기간 필터로 대체하고 있었습니다.

지난 1주를 선택하고 Network 탭에서 요청 목록을 확인했습니다. 처음에는 상단에 있는 요청을 열어봤는데 필터 값이 보이지 않아 한참 헤맸습니다. 목록을 내려보니 origin=JOB_SEARCH_PAGE_JOB_FILTER가 붙은 요청이 따로 있었고, 이걸 열었더니 Payload에서 기간 필터 값을 확인할 수 있었습니다.

기간 필터 선택 후 — Payload에서 필터 값 확인
기간 필터 선택 후 — Payload에서 필터 값 확인

정확한 공식 문서가 없어서 확실하지는 않지만, 업계에서 일반적으로 통용되는 용어 정의를 바탕으로 해석해 봤습니다.

  • f — filter
  • TPR — Time Posted Range
f_TPR: r604800        → 지난 1주 (604800초 = 7일)
origin: JOB_SEARCH_PAGE_JOB_FILTER  → 필터 영역에서 요청됐다는 의미

합치면 “게시된 시간 범위 필터” 정도의 의미로 추정됩니다. r604800에서 r은 range 또는 recent를 뜻하는 것으로 보이고, 604800은 7일을 초 단위로 환산한 값으로 보여집니다.

60초 × 60분 × 24시간 × 7일 = 604800

f_TPR의 숫자 값이 기간을 초 단위로 표현한다는 것도 이때 알게 됐습니다. 기간 필터를 바꾸면 이 숫자만 달라지는 구조입니다.

기획 관점에서는 필터 조건 하나하나가 별도 파라미터로 서버에 전달된다는 것을 확인했습니다. “필터를 추가한다”는 기획 사항이 실제로는 이런 파라미터를 추가하는 작업으로 이어진다는 걸 이해할 수 있었습니다.


Response 탭 — 1편과 같은 한계

LinkedIn Response는 protobuf 인코딩이라 1편과 마찬가지로 JSON을 읽을 수 없었습니다. JSONPlaceholder로 JSON 구조만 확인했습니다.

https://jsonplaceholder.typicode.com/posts?userId=1
JSONPlaceholder Response JSON 화면
JSONPlaceholder Response JSON 화면

userId, id, title, body 필드가 배열로 반복되는 구조입니다. 화면에 뭔가를 표시하려면 그 값이 Response에 반드시 존재해야 한다는 연결고리는 이해했습니다.

다만 실제 검색 API에서 total(전체 결과 수), paging(페이지 정보) 같은 필드를 직접 확인하지 못한 건 아쉬운 부분으로 남았습니다. 다음 편에서 공개 API를 대상으로 보완할 예정입니다.


실습에서 새로 인식한 기획 포인트

실습 전에는 구체적으로 인지하지 못했지만 실습 후 직접 확인한 내용을 바탕으로 정리했습니다.

필터 조건은 URL이 아닌 Payload에 담길 수 있습니다. 이 경우 필터 적용 상태에서 링크 공유가 안 되는 문제가 생길 수 있습니다. 기획 단계에서 “필터 적용 상태를 URL로 공유할 수 있어야 하는가”를 결정해야 합니다.

카테고리 전환 방식도 결정해야 합니다. LinkedIn은 pageKeyscreenId 값을 바꾸는 방식으로 카테고리를 전환합니다. 카테고리별 검색을 별도 화면으로 만들 건지, 하나의 화면에서 조건만 바꿀 건지가 기획 사항입니다.

첫 번째 검색 결과 자동 노출 여부도 명세에 담아야 합니다. 명시하지 않으면 따로 클릭해야만 상세가 뜨는 방식으로 구현될 수 있습니다.

예외 케이스들

  • 검색어 없이 Enter를 눌렀을 때 → 전체 목록? 오류 메시지? 정의 필요
  • 결과가 0개일 때 → empty state 화면을 별도로 기획
  • 네트워크 오류 → 오류 문구 + 재시도 버튼
  • 필터 적용 시 URL이 바뀌지 않는 구조라면 → 링크 공유 가능 여부 결정 필요
  • 카테고리 전환 시 검색어 유지 여부 → 명세에 명시 필요
  • 첫 번째 결과 자동 노출이라면 → 모바일에서 목록과 상세를 어떻게 처리할 건지 별도 기획 필요

정리

이번 실습에서 예상과 실제가 달랐던 부분이 여러 곳 있었습니다. 솔직하게 기록합니다.

예상했던 것과 달랐던 것들

  1. URL에 filters=...가 붙을 거라고 예상했는데 Payload 안에서 처리됐습니다.
  2. Authorization 헤더로 로그인을 확인할 거라고 했는데 1편에서 확인했듯 Cookie 방식으로 되어 있었고, 검색 API에서도 동일하게 적용되어 있었습니다.
  3. 정렬 옵션(RELEVANCE, DATE_POSTED)을 확인할 거라고 했는데 채용공고에서는 기간 필터로 대체되어 있었습니다.
  4. 기간 필터 조건은 처음에 잘못된 요청을 열어봐서 한참 찾지 못했는데, origin=JOB_SEARCH_PAGE_JOB_FILTER가 붙은 요청을 열었더니 f_TPR 값으로 확인할 수 있었습니다.

이번 실습에서 실제로 확인한 것들

  1. +%20 둘 다 공백 인코딩이라는 것
  2. 검색인데 POST를 쓰는 구조
  3. 필터 조건이 URL이 아닌 Payload 안에서 처리되는 방식
  4. pageKey로 카테고리 전환을 처리하는 구조
  5. 첫 번째 검색 결과를 states로 미리 받아두는 구조
  6. 기간 필터가 f_TPR: r604800처럼 초 단위 숫자값으로 전달된다는 것

다음 편에서는 대형 서비스가 아닌 공개 API를 대상으로 Response JSON을 직접 읽어봅니다. 이번에 확인하지 못했던 total, paging 같은 필드를 직접 눈으로 보는 게 목표입니다.

— Lane

Lane
Lanehttps://protolane.kr
프로덕트 기획·UX/UI 역량과 디자인 기반 사고를 바탕으로, 실무 인사이트와 커리어 성장 경험을 공유합니다.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

인기 글 보기

ProtoLane, 디자인 기반 문제 해결형 서비스 기획자

👋 About Me 안녕하세요. 프로토레인(ProtoLane)입니다. 저는 제약 속에서 문제를 정의하고, 실행 가능한 해결책을 설계하는 서비스 기획자입니다. 7년간 웹 디자이너로 쌓은 시각적 사고와 구조...

실제 기획 프로젝트 경험 이후 검증된 Practical UX/Product 사고 프레임

이 글은 웹 디자이너에서 기획자로 전환하던 시점으로 작성된 뉴스레터 내용을, 이후 실제 기획 업무를 경험한 뒤 실전 UX·Product 사고...

대기업 IT 계열사 오피스 플랫폼 구축 및 고도화 프로젝트

Enterprise Digital Workspace Platform: 대기업 IT 계열사 오피스 플랫폼 구축 및 고도화 프로젝트 *비공개 프로젝트로, 비밀유지계약...

IT 스타트업 ‘AI 인증 보안 솔루션’ MVP 서비스

*본 프로젝트는 공개된 전시 시연용 프로토타입을 기반으로 작성되었으며, 기업명과 상세 기술 사양은 일부 대체 처리되었습니다. 📌 프로젝트...