BioPlayground

🧬
목록으로

유전자 정보 검색기 — API와 JSON을 엮어 검색창 만들기

공개 유전자 데이터베이스 API를 fetch로 호출하고 JSON 응답을 파싱해 검색 위젯을 만드는 법을 단계별로 배웁니다.

중급
|
90
|
검증 완료 (2026-07)
유전자 검색REST APIJSONfetch비동기유전자 주석
진행률0/12 (0%)

유전자 정보 검색기 — API와 JSON을 엮어 검색창 만들기

이 토픽을 마치면

교과서에서 배운 APIJSON을 엮어서, 유전자 이름을 입력하면 공개 데이터베이스에서 기능·위치·별칭을 가져와 카드로 보여주는 검색 위젯을 직접 만들 수 있습니다. "남이 만든 거대한 DB를 내 앱에서 쓴다"는 게 실제로 어떤 코드인지 손에 익힙니다.

이 글은 교육용 일반 예제입니다. 공개된 무료 유전자 주석 API를 소재로, 웹에서 외부 데이터를 가져오는 보편적 패턴을 배웁니다.


"이 유전자 뭐였더라" — 매번 사이트를 헤매는 문제

실험하다 보면 유전자 이름은 아는데 세부 정보가 가물가물할 때가 많습니다. TP53이 몇 번 염색체였지? BRCA1의 공식 별칭이 뭐였지?

그때마다 우리는 브라우저를 열고, 공개 DB 사이트에 가서, 검색창에 치고, 결과 페이지를 뒤집니다. 한두 번이면 괜찮은데 유전자 50개를 확인해야 하면? 탭을 50번 열고 닫는 지옥이 시작됩니다.

여기서 개발자의 사고가 발동합니다. "저 사이트도 결국 어딘가의 DB에서 데이터를 꺼내 보여주는 거잖아. 그 DB에 내가 직접 물어볼 수는 없을까?" 있습니다. 그게 바로 API입니다.


먼저 완성품을 봅시다 (블랙박스 먼저 돌리기)

우리가 만들 위젯은 이렇게 동작합니다. 검색창에 유전자명을 치면 —

text
┌──────────────────────────────────────┐
│  🔍  [ TP53            ]  [검색]        │
├──────────────────────────────────────┤
│  TP53   (tumor protein p53)            │
│  ─────────────────────────────         │
│  📍 위치   : 17p13.1                    │
│  🏷️ 별칭   : p53, LFS1, BCC7            │
│  🧬 요약   : 세포 주기 조절, 종양 억제  │
└──────────────────────────────────────┘

사이트를 열지 않아도, 검색창 하나로 정보가 날아옵니다. 이걸 만드는 데 필요한 건 딱 두 개념 — 어디에 물어볼지(API), 답을 어떻게 읽을지(JSON) 입니다.


이 도구는 어떤 부품으로 조립되는가 (부품 분해도)

text
유전자 정보 검색 위젯
   ┌────────────────────────────────────────┐
   │  [질문] 요청 URL 조립 ──── 부품: API      │  ← 직접 만든다 ★
   │              │                           │
   │              ▼                           │
   │  [전송] fetch로 호출 ───── 부품: fetch    │  ← 완성 제공 (도구)
   │              │            + 비동기        │
   │              ▼                           │
   │  [해석] 응답 파싱 ──────── 부품: JSON     │  ← 직접 만든다 ★
   │              │                           │
   │              ▼                           │
   │  [출력] 카드 렌더링                       │
   └────────────────────────────────────────┘
부품어디서 배웠나이 도구에서 하는 일
APIapi-basics"무엇을, 어디에 물어볼지" URL 만들기
fetch/비동기ajax-fetch, sync-vs-async실제로 요청 보내고 기다리기
JSONjson-data-format돌아온 답에서 필요한 값 꺼내기

📌 이 개념들 처음 본다면 (상단 진입 링크)

직접 만드는 새 개념은 API와 JSON 딱 2개. 실제 통신을 하는 fetch는 도구라서 완성본으로 드립니다.

🔎 API를 한 문장으로 (드로어 — api-basics) API는 식당 메뉴판입니다. 주방(DB) 안에 직접 들어갈 순 없지만, 메뉴판(API)에 정해진 방식으로 주문하면 요리(데이터)가 나옵니다. 우리는 "이 유전자 정보 주세요"를 메뉴판 규칙에 맞춰 주문하는 법을 배웁니다.


만들기 1단계 — 무엇을 물어볼지 URL 조립하기 ★ (API)

✍️ 직접 채우는 구간. 부품 = API. 목표: "이 유전자 정보 줘"를 API가 알아듣는 URL로 만든다.

API에 주문하는 방법은 대개 URL입니다. "주소 + 무엇을 원하는지"를 규칙대로 붙이면 됩니다. 예를 들어 어떤 공개 유전자 API가 이런 규칙이라고 합시다.

text
https://예시-api.org/query?q={유전자명}&fields={원하는 항목들}
  • q= 뒤에 검색어(유전자명)
  • fields= 뒤에 받고 싶은 항목(쉼표로 나열)

이걸 손으로 만들면 오타·인코딩 사고가 납니다. 그래서 URL을 안전하게 조립하는 함수를 만듭니다. 여기서 중요한 게 encodeURIComponent — 검색어에 공백이나 특수문자가 있어도 URL이 깨지지 않게 감싸 줍니다.

javascript
const API_BASE = "https://example-gene-api.org/query";

function buildQueryUrl(geneName, fields = ["symbol", "name", "genomic_pos", "alias", "summary"]) {
  const q = encodeURIComponent(geneName.trim());
  const f = encodeURIComponent(fields.join(","));
  return `${API_BASE}?q=${q}&fields=${f}`;
}

// 검증
console.assert(
  buildQueryUrl("TP53") === "https://example-gene-api.org/query?q=TP53&fields=symbol%2Cname%2Cgenomic_pos%2Calias%2Csummary",
  "기본 URL 조립 실패"
);
// 공백이 있는 검색어도 안전하게 인코딩되어야 한다
console.assert(
  buildQueryUrl("  TP 53  ").includes("q=TP%2053"),
  "공백 인코딩 실패"
);

두 번째 assert가 핵심입니다. 사용자가 TP 53처럼 공백을 넣어도 TP%2053으로 안전하게 인코딩됩니다. encodeURIComponent를 안 쓰면 이 공백이 URL을 두 동강 내서 요청이 실패합니다. API 호출 사고의 절반은 이 인코딩을 빼먹어서 생깁니다.

🤔 자기설명 프롬프트 geneName.trim()을 왜 넣었을까요? 사용자가 검색창에 "TP53 "(뒤에 공백)을 붙여 입력했을 때, trim이 없으면 API가 이걸 어떻게 받아들일지 생각해 보세요. (앞의 프라이머 편에서 서열을 normalize한 것과 같은 정신입니다 — 보내기 전에 모양을 맞춘다.)


만들기 2단계 — 실제로 요청 보내기 (fetch, 완성 제공)

요청을 실제로 보내고 기다리는 부분은 도구라서 완성해서 드립니다. ajax-fetchsync-vs-async에서 배운 fetch + async/await 그대로입니다.

javascript
async function callGeneApi(geneName) {
  const url = buildQueryUrl(geneName);
  const response = await fetch(url);        // ← 네트워크 왕복. 기다린다(await)
  if (!response.ok) {
    throw new Error(`API 오류: ${response.status}`);
  }
  return await response.json();             // ← 응답 본문을 JSON 객체로 변환
}

🔎 왜 async/await인가? (드로어 — sync-vs-async) 네트워크 요청은 시간이 걸립니다(서버 왕복). 그 동안 브라우저가 멈추면 안 되죠. await는 "이거 끝날 때까지 기다리되, 그동안 화면은 얼지 않게" 해 줍니다. 식당에서 주문하고 진동벨을 받아 자리에 앉아 있는 것과 같습니다 — 음식 나올 때까지 서서 기다리지 않죠.

이 함수의 마지막 줄 response.json()이 다음 단계로 넘어가는 다리입니다. 서버가 보낸 글자 덩어리(텍스트) 를, 우리가 다룰 수 있는 자바스크립트 객체로 바꿔 주죠. 그 객체를 요리하는 게 3단계입니다.


만들기 3단계 — 답에서 필요한 것만 꺼내기 ★ (JSON)

✍️ 직접 채우는 구간. 부품 = JSON. 목표: 복잡하게 생긴 응답에서 우리가 쓸 값만 안전하게 골라낸다.

API가 돌려주는 JSON은 보통 우리가 원하는 것보다 훨씬 크고 복잡합니다. 실제 응답은 대략 이렇게 생겼다고 합시다.

json
{
  "hits": [
    {
      "symbol": "TP53",
      "name": "tumor protein p53",
      "genomic_pos": { "chr": "17", "start": 7668402, "end": 7687550 },
      "alias": ["p53", "LFS1", "BCC7"],
      "summary": "세포 주기 조절 및 종양 억제에 관여하는 유전자."
    }
  ]
}

여기서 우리가 카드에 쓸 건 이름, 위치, 별칭, 요약 정도입니다. JSON을 뒤져서 필요한 값만 꺼내 깔끔한 객체로 정리하는 함수를 만듭니다. 이때 반드시 신경 쓸 것 — 없을 수도 있는 값을 안전하게 처리하는 겁니다. 어떤 유전자는 별칭이 없고, 어떤 응답은 아예 결과가 없습니다.

javascript
function parseGeneResponse(data) {
  const hit = data.hits && data.hits[0];
  if (!hit) {
    return null;                          // 결과 없음 — 호출한 쪽이 처리
  }
  const pos = hit.genomic_pos || {};
  return {
    symbol: hit.symbol || "?",
    name: hit.name || "",
    location: pos.chr ? `chr${pos.chr}:${pos.start}-${pos.end}` : "정보 없음",
    aliases: Array.isArray(hit.alias) ? hit.alias : [],   // 없으면 빈 배열
    summary: hit.summary || "요약 정보가 없습니다.",
  };
}

이제 가짜 응답(mock)으로 검증합니다. 실제 네트워크 없이도 파싱 로직이 맞는지 확인할 수 있죠. 이게 좋은 설계입니다 — 순수한 데이터 처리(파싱)를 네트워크와 분리해 두면 테스트가 쉬워집니다.

javascript
const mockResponse = {
  hits: [{
    symbol: "TP53",
    name: "tumor protein p53",
    genomic_pos: { chr: "17", start: 7668402, end: 7687550 },
    alias: ["p53", "LFS1", "BCC7"],
    summary: "세포 주기 조절 및 종양 억제에 관여하는 유전자.",
  }],
};

const parsed = parseGeneResponse(mockResponse);
console.assert(parsed.symbol === "TP53", "심볼 파싱 실패");
console.assert(parsed.location === "chr17:7668402-7687550", "위치 조립 실패");
console.assert(parsed.aliases.length === 3, "별칭 파싱 실패");

// 결과가 없는 응답도 안전하게 처리되어야 한다
console.assert(parseGeneResponse({ hits: [] }) === null, "빈 결과 처리 실패");
console.assert(parseGeneResponse({}) === null, "빈 객체 처리 실패");

// 별칭이 없는 유전자도 터지지 않아야 한다
const noAlias = parseGeneResponse({ hits: [{ symbol: "X", genomic_pos: { chr: "1", start: 1, end: 2 } }] });
console.assert(Array.isArray(noAlias.aliases) && noAlias.aliases.length === 0, "별칭 없음 처리 실패");

마지막 세 개의 assert가 초보와 고수를 가릅니다. 잘 되는 경우만 처리하면 초보고, 없는 경우·빈 경우·깨진 경우까지 처리하면 고수입니다. hit.alias가 없을 때 그냥 쓰면 undefined.length로 앱이 터집니다. Array.isArray(...) ? ... : [] 한 줄이 그 사고를 막습니다.

🤔 자기설명 프롬프트 data.hits && data.hits[0] 에서 &&를 왜 썼을까요? 만약 data.hits가 아예 없는(undefined) 응답이 왔을 때, 이 방어가 없으면 무슨 에러가 날지 설명해 보세요. (앞 프라이머 편의 "없을 수도 있는 값"과 같은 주제입니다.)


부품을 하나로 — 완성된 위젯

이제 URL 조립(API) → 호출(fetch) → 파싱(JSON) → 렌더를 잇습니다.

javascript
function renderGeneCard(gene) {
  if (!gene) return "검색 결과가 없습니다.";
  return [
    `${gene.symbol}  (${gene.name})`,
    `📍 위치 : ${gene.location}`,
    `🏷️ 별칭 : ${gene.aliases.join(", ") || "없음"}`,
    `🧬 요약 : ${gene.summary}`,
  ].join("\n");
}

async function searchGene(geneName) {
  try {
    const raw = await callGeneApi(geneName);
    const gene = parseGeneResponse(raw);
    return renderGeneCard(gene);
  } catch (e) {
    return `오류가 발생했습니다: ${e.message}`;
  }
}

// 렌더 로직도 mock으로 검증 (네트워크 불필요)
const card = renderGeneCard(parseGeneResponse(mockResponse));
console.assert(card.includes("TP53"), "카드 렌더 실패");
console.assert(card.includes("chr17:7668402-7687550"), "카드 위치 렌더 실패");
console.assert(renderGeneCard(null) === "검색 결과가 없습니다.", "빈 결과 렌더 실패");

searchGene 하나에 try/catch가 감싸고 있는 걸 보세요. 네트워크는 언제든 실패할 수 있습니다(서버 다운, 인터넷 끊김). 그때 앱이 통째로 죽는 대신 "오류가 발생했습니다"를 보여주는 것 — 이게 실전 도구와 장난감의 차이입니다.


다른 길도 있다 (멀티패스 성찰)

  • 여러 유전자 동시 검색: 유전자 50개를 하나씩 await하면 느립니다(순차). Promise.all([...])동시에 쏘면 훨씬 빠릅니다. 트레이드오프: 서버에 부담을 줄 수 있어, 공개 API는 초당 요청 수 제한(rate limit)이 있습니다.
  • 캐싱: 같은 유전자를 또 검색하면 API를 또 부르지 말고 저장해 둔 결과를 씁니다. 앞 편의 딕셔너리(서열→값)와 똑같은 발상 — 여기선 유전자명 → 결과.
  • 백엔드 프록시: API 키가 필요한 서비스는 키를 브라우저에 두면 노출됩니다. 그래서 실무에선 우리 서버를 한 단계 거칩니다. → 응용편 유전자 검색 백엔드로 이어집니다.

핵심: 프런트에서 외부 API를 직접 부르는 건 빠르게 만들기 좋지만, 속도(동시 요청)·재사용(캐싱)·보안(키) 세 축에서 각각 더 나은 선택이 있습니다.

다음 단계로 (하단 출구 링크)


직접 해보기 (독립 문제)

  1. 로딩 상태: 검색 중일 때 "검색 중..."을 보여주고, 끝나면 결과로 바꾸세요. (비동기의 "기다리는 시간"을 UI로 표현)
  2. 캐시 추가: 이미 검색한 유전자는 API를 다시 부르지 않도록 객체({})에 저장해 재사용하세요. console.assert로 두 번째 호출이 캐시를 쓰는지 확인.
  3. 여러 유전자: 쉼표로 구분한 여러 유전자명을 받아 Promise.all로 동시에 검색하세요.
  4. 도전: API 응답 형식이 바뀌어 genomic_pos가 배열(여러 위치)로 올 수도 있습니다. parseGeneResponse가 배열이든 객체든 안전하게 처리하도록 고쳐 보세요.

정리

우리는 "유전자 정보를 매번 사이트에서 찾는" 번거로움을, API 하나로 내 앱에 끌어오는 검색 위젯으로 바꿨습니다.

  • API는 "무엇을 어디에 물어볼지"를 URL로 표현했습니다. (질문)
  • fetch/비동기는 요청을 보내고, 화면을 얼리지 않고 기다렸습니다. (전송)
  • JSON은 복잡한 응답에서 필요한 값만, 없는 값까지 안전하게 꺼냈습니다. (해석)

API와 JSON은 따로 배울 땐 추상적이지만, 엮이는 순간 세상의 모든 공개 데이터베이스가 내 앱의 재료가 됩니다. 유전자 DB든, 논문 DB든, 단백질 구조 DB든 — 패턴은 언제나 같습니다: 주문 URL 만들고, 보내고, 답을 파싱한다.

이 글은 일반 교육 예제입니다. 실제 서비스에서는 인증, 요청 제한, 에러 재시도, 페이지네이션 등이 더해집니다. 그 상세한 버전은 이 뼈대 위에 여러분이 붙이면 됩니다.