개발로 사고하기

경기데이터드림- 경기도 지역서점 데이터 OpenAPI 연동기

tues 2025. 8. 25. 14:15

📚 경기도 지역서점 인증제 OpenAPI 연동기

 

내 지역에서 포인트를 쓸 수 있는 서점 찾기:

https://bookstore-search.vercel.app/

 

Vite + React + TS

 

bookstore-search.vercel.app

 

코드 바로 보기:

https://github.com/veryyounng/bookstore-search

 

GitHub - veryyounng/bookstore-search

Contribute to veryyounng/bookstore-search development by creating an account on GitHub.

github.com

 

 

경기도데이터드림 바로가기:

https://data.gg.go.kr/portal/data/service/selectServicePage.do?page=1&rows=10&sortColumn=&sortDirection=&infId=EOE8RQ0K99WC72WJ754O28934129&infSeq=1&order=&loc=&searchWord=%EC%84%9C%EC%A0%90

 

경기도 인증 지역서점 현황 | 데이터셋 상세 Sheet | 경기데이터드림

「경기도 지역서점 인증제」에 따른 '경기도 인증 지역서점' 현황입니다. ※ 본 데이터셋은 지역서점 인증을 받은 서점들의 목록을 제공합니다. 서점 업종은 신고제로 운영되기 때문에 경기도

data.gg.go.kr

 

1. 만들게 된 계기

얼마 전 엄마가 저에게 물어보셨습니다.
이 독서포인트, 어디 서점에서 쓸 수 있는 거야?

막상 찾아보려 했더니 정보가 흩어져 있고, 바로 확인하기가 쉽지 않았습니다.
그래서 저는 엄마를 비롯해 사용자들이 한눈에 ‘내 지역에서 포인트를 쓸 수 있는 서점’을 확인할 수 있도록
경기도 공공데이터 OpenAPI를 연동한 작은 웹 서비스를 만들기로 했습니다.

 

👉 단순한 개발 연습이 아니라, 가족이 실제로 쓰고 싶어 했던 기능을 직접 구현했다는 점에서 의미가 있었습니다.


2. 개발 목표

  • 경기도 인증 지역서점을 지역별로 검색할 수 있는 기능 구현
  • API로 내려오는 데이터(서점명, 주소, 연락처, 인증 여부)를 사용자 친화적으로 표시

3. 사용 기술 스택

  • Frontend: React + TypeScript (검색창, 결과 리스트 UI)
  • Infra: Vercel

4. 개발 과정

(1) 공공데이터 API 분석

경기도에서 제공하는 인증 지역서점 API의 기본 구조는 다음과 같았습니다.

GET https://api.gg.go.kr/RegionBookStore?KEY=발급받은키&SIGUN_NM=수원시
  • SIGUN_NM: 시/군 이름 (예: 수원시, 용인시)
  • 응답: 서점명, 주소, 전화번호, 인증 여부, 등록일 등

Postman으로 먼저 테스트하면서 정상적으로 응답이 오는지 확인했습니다.

 

(2) 로직 흐름

  1. 최초 마운트되면 fetchAllBookstores()가 실행돼, 경기도 OpenAPI(인증 지역서점)에서 페이지 단위로 전부 수집한다.
  2. 수집이 끝나면 allData에 통으로 담고, 지역 목록을 useMemo로 추출해 드롭다운 옵션을 만든다.
  3. 사용자가 지역을 선택하면, 해당 지역으로 allData를 필터링해 filteredData를 렌더링한다.
  4. isLoading으로 로딩/완료 상태를 반영해 UX를 안정화한다.

핵심 컨셉: 초기 한 번 전체 로딩 → 프론트에서 지역 필터링
(API의 페이징 특성상, 서버에서 다 끌어와 한 번에 쓰는 전략을 선택)

 

(3) 핵심 코드

페이징루프

let allRows: Bookstore[] = [];
let page = 1;

while (true) {
  const res = await axios.get('https://openapi.gg.go.kr/GgCertflyRegionBkstr', {
    params: { KEY: '…', pIndex: page, pSize: 50, type: 'json' },
  });
  const rows = (res.data as any)?.GgCertflyRegionBkstr?.[1]?.row ?? [];
  if (rows.length === 0) break;        // ✅ 더 이상 데이터 없으면 종료
  allRows.push(...rows);
  page += 1;
}
setAllData(allRows);
  • 의미: 공공데이터 포털의 응답 구조(GgCertflyRegionBkstr → [1] → row)에 맞춰 빈 페이지가 나올 때까지 순회
  • 장점: API가 제공하는 전체 데이터를 누락 없이 모을 수 있다.

지역 목록 추출(중복 제거 + 정렬)

const regions = useMemo(() => {
  const setOfRegions = new Set(allData.map((x) => x.SIGUN_NM).filter(Boolean));
  return Array.from(setOfRegions).sort();
}, [allData]);

 

  • 의미: 응답 데이터에서 SIGUN_NM만 모아 중복 제거 후 정렬
  • 장점: 데이터가 바뀌지 않는 한 재계산 방지(useMemo) → 렌더 최적화

지역 선택 → 필터링

const handleRegionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  const region = e.target.value;
  setSelectedRegion(region);
  const filtered = allData.filter((item) => item.SIGUN_NM === region);
  setFilteredData(filtered);
};

 

  • 의미: 전체 데이터에서 지역 일치 항목만 뽑아 리스트 렌더
  • 장점: 단순하고 빠르다. (데이터 규모가 큰 경우엔 서버 집계/페이지네이션 고려)

타입/스키마 핸들링

interface Bookstore {
  SIGUN_NM: string;               // 시·군·구
  BKSTR_NM: string;               // 서점명
  REFINE_ROADNM_ADDR: string;     // 도로명주소
  CAFTRI_TELNO?: string;          // 전화번호 (옵션)
  [key: string]: any;
}
  • 의미: OpenAPI 응답 필드명을 그대로 유지해 매핑 비용 최소화
  • 주의: 응답 필드가 변경될 수 있으니, 실서비스에선 DTO 정규화가 유리

(4) API 호출 방식

  • 엔드포인트: GET https://openapi.gg.go.kr/GgCertflyRegionBkstr
  • 쿼리 파라미터:
    • KEY: 발급 API 키
    • pIndex: 페이지 번호
    • pSize: 페이지 크기(50)
    • type: 응답 포맷(json)
  • 응답 구조:
    • 루트: GgCertflyRegionBkstr
    • 리스트 노드: 인덱스 [1] 내부의 row 배열
    • 필드 예:
SIGUN_NM(지역), BKSTR_NM(서점명), REFINE_ROADNM_ADDR(주소), CAFTRI_TELNO(전화)

 

빈 row가 나오면 더 이상 데이터 없음,  while(true) + break 패턴으로 끊는 로직 선택

(5) 예외/경계 케이스 처리

  • 로딩 상태: isLoading으로 초반 UX 저하 방지
  • 에러 로깅: 네트워크 실패 시 console.error로 즉시 확인
  • 빈 지역 선택: selectedRegion === "" 일 때 리스트 미노출로 혼동 방지
  • 전화번호/주소 Optional: UI에서 안전하게 처리(옵셔널 체이닝)

 

5. 배운 점

  • 공공데이터 API의 특성 이해
    • 문서가 불친절하거나 구조가 복잡할 수 있어서, 직접 호출해보고 응답 스키마를 분석하는 과정이 필수라는 걸 배움
    • GgCertflyRegionBkstr → [1] → row 같은 계층 구조를 파악해야 데이터 활용이 가능하다는 점을 경험
  • 데이터 수집 전략 설계
    • 한 번에 다 불러올 수 없는 API라서 페이지 단위 반복 호출을 통해 전체 데이터를 확보
    • “빈 row가 나오면 stop” 같은 종료 조건 설계가 필요하다는 걸 체득