📚 경기도 지역서점 인증제 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
경기도데이터드림 바로가기:
경기도 인증 지역서점 현황 | 데이터셋 상세 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) 로직 흐름
- 최초 마운트되면 fetchAllBookstores()가 실행돼, 경기도 OpenAPI(인증 지역서점)에서 페이지 단위로 전부 수집한다.
- 수집이 끝나면 allData에 통으로 담고, 지역 목록을 useMemo로 추출해 드롭다운 옵션을 만든다.
- 사용자가 지역을 선택하면, 해당 지역으로 allData를 필터링해 filteredData를 렌더링한다.
- 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” 같은 종료 조건 설계가 필요하다는 걸 체득
'개발로 사고하기' 카테고리의 다른 글
| 🚨 Spring Boot Kafka 개발 중 “No qualifying bean” 에러 해결기 (0) | 2025.09.07 |
|---|---|
| 📊 경기도 인증 지역서점 데이터 시각화 — 지역별 분포 Bar Chart 구현기 (1) | 2025.08.25 |
| 🦖 미니 공룡 게임으로 배우는 방향 벡터 완성 (3) | 2025.08.19 |
| 미니 공룡 게임에 방향 전환 기능 추가 (0) | 2025.08.19 |
| 방향 전환 수학 이해를 위한 미니 게임 (3) | 2025.08.14 |