App.jsx 라인별 상세 설명
이 문서는 App.jsx가 공공데이터포털 오픈 API를 호출하고,
응답받은 JSON 데이터를 React 화면에 출력하는 과정을 라인별로 설명합니다.
기준일: 20260908. 현재 코드의 API는
경찰청_전국 범죄 발생 및 검거 현황_20241231 데이터를
조회합니다.
전체 흐름
컴포넌트가 처음 화면에 나타나면 useEffect가 한 번 실행됩니다.
그 안에서 API 주소와 검색 조건을 합쳐 fetch로 요청하고,
응답 JSON의 data 배열을 React 상태에 저장합니다.
상태가 바뀌면 React가 화면을 다시 그려 각 항목을 pre 태그로 출력합니다.
라인별 설명
| 라인 | 코드 | 상세 설명 |
|---|---|---|
| 1 | import { useEffect, useState } from 'react' |
React에서 제공하는 Hook 두 개를 가져옵니다. useState는 화면에 표시할
데이터를 상태로 저장할 때 쓰고, useEffect는 컴포넌트가 처음 실행될 때
API 요청 같은 부수 효과를 처리할 때 씁니다.
|
| 3-9 | // 데이터포맷 // JSON+XML // Base URL // api.odcloud.kr/api // Swagger URL // https://infuser.odcloud.kr/oas/docs?namespace=15064217/v1 // 일반 인증키 // a73690547c7e3fc6101f1f2d6bd2dfc9f4a453270aa6287eb52feb203c92cb98 |
dataFormat.md에 있던 API 정보를 주석으로 옮겨둔 부분입니다.
주석은 실행되지 않지만, 이 코드가 어떤 공공기관 API를 기준으로 작성됐는지
개발자가 바로 확인할 수 있게 해 줍니다.
|
| 11-12 | const API_URL = 'https://api.odcloud.kr/api/15064217/v1/uddi:4f4a7f28-e408-4df4-9230-ebbe4c524dcf' |
실제 데이터를 요청할 API 주소를 상수로 저장합니다.
기존에 https://api.odcloud.kr/api/...처럼 점 세 개가 들어가 있으면
실제 endpoint가 아니기 때문에 데이터를 불러올 수 없습니다.
15064217/v1은 Swagger namespace이고,
uddi:4f4a7f28-e408-4df4-9230-ebbe4c524dcf는 2024년 기준 데이터의
고유 API 경로입니다.
|
| 14-15 | const SERVICE_KEY = 'a73690547c7e3fc6101f1f2d6bd2dfc9f4a453270aa6287eb52feb203c92cb98' |
공공데이터포털 API를 호출할 때 필요한 인증키를 상수로 저장합니다.
아래에서 serviceKey라는 쿼리 파라미터로 전달됩니다.
|
| 17 | function App() { |
React 컴포넌트 App을 선언합니다. 이 함수가 반환하는 JSX가 브라우저 화면에
렌더링됩니다.
|
| 18 | const [data, setData] = useState([]) |
API에서 받아온 목록 데이터를 저장하는 상태입니다. 처음에는 아직 데이터가 없으므로
빈 배열 []로 시작합니다. setData를 호출하면 화면이 다시
렌더링됩니다.
|
| 19 | const [loading, setLoading] = useState(true) |
API 요청이 진행 중인지 나타내는 상태입니다. 처음 화면이 열리면 바로 데이터를 가져와야
하므로 true로 시작합니다.
|
| 20 | const [error, setError] = useState(null) |
API 요청 중 문제가 생겼을 때 에러 메시지를 저장하는 상태입니다. 처음에는 에러가
없으므로 null입니다.
|
| 22 | useEffect(() => { | 컴포넌트가 렌더링된 뒤 실행할 코드를 작성합니다. 이 코드에서는 API 요청을 시작합니다. 아래쪽 의존성 배열이 비어 있기 때문에 최초 1회만 실행됩니다. |
| 23 | const controller = new AbortController() | API 요청을 중간에 취소할 수 있게 해 주는 객체를 만듭니다. 사용자가 화면을 빠르게 벗어나 컴포넌트가 사라질 때 불필요한 네트워크 요청을 정리하는 데 사용합니다. |
| 24-30 | const params = new URLSearchParams({ page: '1', perPage: '10', returnType: 'JSON', serviceKey: SERVICE_KEY, }) |
API 주소 뒤에 붙일 쿼리 문자열을 안전하게 만듭니다.
page는 조회할 페이지, perPage는 한 번에 가져올 개수,
returnType은 응답 형식, serviceKey는 인증키입니다.
직접 문자열을 더하는 것보다 오타와 인코딩 문제를 줄일 수 있습니다.
|
| 32 | // 20260908: dataFormat.md의 Swagger namespace(15064217/v1)에서 확인한 실제 공공데이터 endpoint로 호출합니다. | 수정 날짜와 수정 이유를 남긴 주석입니다. 나중에 코드를 보는 사람이 왜 이 endpoint를 쓰는지 추적할 수 있습니다. |
| 33-35 | fetch(`${API_URL}?${params.toString()}`, { signal: controller.signal, }) |
완성된 API URL로 HTTP 요청을 보냅니다.
params.toString()은 객체 형태의 조건을
page=1&perPage=10&returnType=JSON&serviceKey=... 형태로 바꿉니다.
signal은 위에서 만든 AbortController와 연결됩니다.
|
| 36 | .then((response) => { | 서버에서 응답이 도착하면 가장 먼저 실행되는 처리 단계입니다. 여기서 응답 상태가 정상인지 확인합니다. |
| 37-39 | if (!response.ok) { throw new Error(`API 요청 실패 (${response.status})`) } |
HTTP 상태 코드가 200번대가 아니면 실패로 처리합니다.
예를 들어 인증키가 틀리면 401, 서버 문제가 있으면 500 같은 값이 들어올 수 있습니다.
throw를 실행하면 아래 catch 블록으로 이동합니다.
|
| 41 | return response.json() | 응답 본문을 JSON 객체로 변환합니다. 공공데이터 API가 내려주는 원본 문자열을 JavaScript에서 다룰 수 있는 객체로 바꾸는 단계입니다. |
| 43-45 | .then((result) => { setData(result.data ?? []) }) |
변환된 JSON 결과를 받아 result.data 배열만 상태에 저장합니다.
공공데이터 API 응답은 보통 currentCount, data,
matchCount, page, perPage,
totalCount를 포함합니다. 화면에 필요한 목록은 data입니다.
?? []는 data가 없을 때 빈 배열로 대체해 화면 오류를 막습니다.
|
| 46-51 | .catch((error) => { if (error.name !== 'AbortError') { console.error(error) setError(error.message) } }) |
요청 실패, JSON 변환 실패, 네트워크 오류 등을 처리합니다.
단, 컴포넌트 정리 과정에서 의도적으로 요청을 취소한 AbortError는 사용자에게
에러로 보여주지 않습니다. 실제 오류만 콘솔에 출력하고 화면용 에러 상태에 저장합니다.
|
| 52-54 | .finally(() => { setLoading(false) }) |
성공하든 실패하든 요청이 끝나면 로딩 상태를 끕니다. 이 값이 false가 되면
더 이상 "불러오는 중..." 화면을 보여주지 않습니다.
|
| 56-58 | return () => { controller.abort() } |
useEffect의 정리 함수입니다. 컴포넌트가 사라질 때 아직 끝나지 않은
fetch 요청을 취소합니다. React 개발 모드에서 effect가 재실행될 때도
이전 요청을 정리하는 역할을 합니다.
|
| 59 | }, []) |
useEffect의 의존성 배열입니다. 빈 배열이면 컴포넌트가 처음 화면에 나타난
뒤 한 번만 API를 호출합니다.
|
| 61 | if (loading) return <p>불러오는 중...</p> |
API 요청이 아직 끝나지 않았으면 목록 대신 로딩 메시지를 보여줍니다.
이 줄에서 return이 실행되면 아래의 실제 목록 화면은 아직 렌더링되지 않습니다.
|
| 62 | if (error) return <p>에러: {error}</p> | 에러 상태에 메시지가 있으면 데이터 목록 대신 에러 메시지를 보여줍니다. 예를 들어 잘못된 URL, 인증 실패, 서버 오류가 있을 때 이 화면이 표시됩니다. |
| 64-66 | return ( <div> <h1>경찰청 전국 범죄 발생 및 검거 현황</h1> |
정상적으로 데이터 로딩이 끝났을 때 보여줄 JSX를 반환합니다.
가장 바깥의 div는 여러 요소를 묶는 컨테이너이고,
h1은 페이지 제목입니다.
|
| 68 | {data.map((item, index) => ( |
data 배열의 각 항목을 하나씩 화면 요소로 바꿉니다.
React에서 목록을 출력할 때 자주 사용하는 패턴입니다.
|
| 69 | <div key={`${item['범죄소분류']}-${index}`}> |
각 목록 항목을 감싸는 div입니다.
key는 React가 목록 변경을 효율적으로 추적하기 위해 필요합니다.
여기서는 범죄 소분류 값과 배열 인덱스를 합쳐 임시 고유값으로 사용합니다.
|
| 70 | <pre>{JSON.stringify(item, null, 2)}</pre> |
각 데이터 객체를 보기 좋게 문자열로 바꿔 출력합니다.
JSON.stringify(item, null, 2)의 세 번째 인자 2는 들여쓰기
칸 수입니다. pre 태그는 줄바꿈과 공백을 그대로 보여줍니다.
|
| 71-73 | </div> ))} |
목록 항목 하나의 JSX와 map 반복문을 닫습니다.
데이터가 10개라면 이 구조가 10번 반복되어 화면에 표시됩니다.
|
| 74-76 | </div> ) } |
전체 화면을 감싸던 div, JSX 반환식, App 컴포넌트 함수를
순서대로 닫습니다.
|
| 78 | export default App |
다른 파일에서 App 컴포넌트를 가져다 쓸 수 있게 내보냅니다.
main.jsx에서 이 컴포넌트를 import해 실제 DOM에 렌더링합니다.
|
API 응답 데이터 구조
| 필드 | 예상 의미 | 화면 사용 여부 |
|---|---|---|
currentCount |
현재 응답에 포함된 데이터 개수입니다. | 현재 화면에는 직접 출력하지 않습니다. |
data |
범죄 발생 및 검거 현황 목록 배열입니다. | setData(result.data ?? [])로 저장해서 화면에 출력합니다. |
matchCount |
검색 조건에 맞는 데이터 개수입니다. | 현재 화면에는 직접 출력하지 않습니다. |
page |
현재 페이지 번호입니다. | 요청값은 1이고, 화면에는 직접 출력하지 않습니다. |
perPage |
페이지당 데이터 개수입니다. | 요청값은 10이고, 화면에는 직접 출력하지 않습니다. |
totalCount |
전체 데이터 개수입니다. | 현재 화면에는 직접 출력하지 않습니다. |
각 범죄 데이터 항목의 주요 컬럼
| 컬럼 | 설명 |
|---|---|
범죄대분류 |
강력범죄, 절도범죄처럼 가장 큰 범죄 분류입니다. |
범죄중분류 |
대분류 아래의 중간 범죄 분류입니다. |
범죄소분류 |
살인, 영아살해, 존속살해처럼 더 구체적인 범죄 항목입니다. |
발생건수 |
해당 범죄가 발생한 건수입니다. |
검거건수 |
해당 범죄 중 검거된 건수입니다. |
검거인원(남자) |
검거된 인원 중 남자 인원입니다. |
검거인원(여자) |
검거된 인원 중 여자 인원입니다. |
검거인원(미상) |
성별 등 일부 정보가 확인되지 않은 검거 인원입니다. |
법인체 |
검거 대상 중 법인체로 분류된 수입니다. |
주의할 점
현재 SERVICE_KEY가 프론트엔드 코드에 그대로 들어 있습니다.
학습용 예제나 개인 실습에서는 괜찮지만, 실제 서비스에서는 브라우저 개발자 도구로
누구나 값을 볼 수 있습니다. 운영 서비스라면 서버를 거쳐 API를 호출하거나,
환경 변수와 백엔드 프록시를 사용하는 구조가 더 적절합니다.
App.jsx 전체 코드
import { useEffect, useState } from 'react'
// 데이터포맷
// JSON+XML
// Base URL
// api.odcloud.kr/api
// Swagger URL
// https://infuser.odcloud.kr/oas/docs?namespace=15064217/v1
// 일반 인증키
// a73690547c7e3fc6101f1f2d6bd2dfc9f4a453270aa6287eb52feb203c92cb98
const API_URL =
'https://api.odcloud.kr/api/15064217/v1/uddi:4f4a7f28-e408-4df4-9230-ebbe4c524dcf'
const SERVICE_KEY =
'a73690547c7e3fc6101f1f2d6bd2dfc9f4a453270aa6287eb52feb203c92cb98'
function App() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const controller = new AbortController()
const params = new URLSearchParams({
page: '1',
perPage: '10',
returnType: 'JSON',
serviceKey: SERVICE_KEY,
})
// 20260908: dataFormat.md의 Swagger namespace(15064217/v1)에서 확인한 실제 공공데이터 endpoint로 호출합니다.
fetch(`${API_URL}?${params.toString()}`, {
signal: controller.signal,
})
.then((response) => {
if (!response.ok) {
throw new Error(`API 요청 실패 (${response.status})`)
}
return response.json()
})
.then((result) => {
setData(result.data ?? [])
})
.catch((error) => {
if (error.name !== 'AbortError') {
console.error(error)
setError(error.message)
}
})
.finally(() => {
setLoading(false)
})
return () => {
controller.abort()
}
}, [])
if (loading) return <p>불러오는 중...</p>
if (error) return <p>에러: {error}</p>
return (
<div>
<h1>경찰청 전국 범죄 발생 및 검거 현황</h1>
{data.map((item, index) => (
<div key={`${item['범죄소분류']}-${index}`}>
<pre>{JSON.stringify(item, null, 2)}</pre>
</div>
))}
</div>
)
}
export default App