개발새발 로그

공공 데이터 오픈 API 요청하는 법 본문

React

공공 데이터 오픈 API 요청하는 법

이즈흐 2024. 1. 23. 00:13

공공 데이터 포털에 있는 농사로 오픈 API를 이용하면서  생긴 일들을 적어보려고 한다.

 

 

먼저 오픈 API 요청하는 방법!

https://www.data.go.kr/

여기 들어가서 원하는 데이터를 찾는다

이때 되도록이면 JSON을 지원하는 곳이면 좋다.

나는 XML만 가능한 API를 선택해서 XML을 JSON으로 바꿔주는 로직이 더 필요했다.

 

그리고 원하는 오픈 API 데이터에서 보통은 API 인증키를 신청하거나 받아야한다.

https://www.nongsaro.go.kr/portal/ps/psn/psnj/openApiLst.ps?menuId=PS65428&pageIndex=1&pageSize=&sLclasCode=&sText=%EC%95%BD%EC%B4%88%EC%A0%95%EB%B3%B4

나는 이 사이트에서 아래와 같이 공공데이터 신청을 누르고 양식을 작성했다.

양식은 저 정도로만 적어도 승인시켜준다.

 

이제 Open API를 요청하는 방법을 알아보자!

나는 이런 메뉴얼을 다운받아서 읽었다..

처음에는 도저히 어떻게하는지 감을 못잡았다.

그냥 적힌 url에 apikey넣으면 되는 거 아닌가..? 했다.

 

하지만 세부적인 설정이 필요했다.

나처럼 고생하지말라고 이렇게 블로그 글을 쓰게 됐다.

 

그럼 이제 어떻게 하는지 한번 알아보자

 

 

CORS에러는 무조건 만난다!

나는 위에서 말했던 것처럼 그냥 axios로 대충 요청하면 되는거 아닌가? 했다.

근데 CORS에러가 떴다.

 

일단 이 문제를 해결하기 전에 SOP와 CORS를 알아보자

 

먼저 출처란 무엇일까?

CORS와 SOP에서의 O는 Origin이다.

출처라고 한다.

이 Origin은 프로토콜,호스트,포트번호를 합친 것 이다.

출처를 판단할 때는 이 Origin으로 판단한다.

 

SOP는 Same-Origin-Policy, 즉 같은 출처 정책이다.

그니까 같은 출처끼리만 요청을 보낼 수 있다는 것이다.

 

CORS Cross-Origin Resource Sharing,다른 출처 자원 공유 이다.

그니까 다른 출처라도 요청을 보낼 수 있게 해준다는 뜻이다.

 

그래서 SOP 정책으로 막혔던 다른 출처로의 요청을 
CORS 설정을 해줘서 정상적으로 요청을 보낼 수 있게 한다.

 

즉, SOP정책에 막혀서 CORS 에러가 뜨는 것이다. 

브라우저가 다른 출처 요청이 들어왔을 때 CORS 설정을 해주면 요청을 허용해준다는 뜻으로 CORS에러가 나는 것이다.

(헷갈리면 안된다.)

 

브라우저를 거쳐서 요청을 하는 프론트엔드 개발자는 CORS문제를 해결하기 위해 CORS 응답 헤더 설정이라는 해결책을 찾게 된다.

원래는 이 상황에서 백엔드 개발자가 CORS 응답 설정을 해줘야 한다.

 

그래서 아무튼 프론트에서는 CORS 응답 헤더 설정으로 요청 허용 해줘라고 보낸다.

 

요청을 받은 서버에서 CORS설정을 해준다는 것은 

응답 헤더에 Access-Control-Allow-Origin에다가 허용하는 출처를 지정해준다는 것을 의미한다.

따라서 서버에서 출처를 지정해주면 브라우저에서는 

요청 헤더의 Origin과

응답 헤더의 Access-Control-Allow-Origin가 같은지 판단 후에 

같다면 요청을 허용하고 다르면 CORS에러를 반환한다.

 

자 여기까지 장황한 CORS 설명이었다.

그럼 우리는 어떻게 해야할까?

OpenAPI 신청할 때 도메인을 적어서보내긴했는데 당연히 안되지 않겠는가?

그래서 서버를 만들어줘야한다.

 

서버를 만들어서 프론트에서 만든 서버에 요청을 보내고,

만든 서버에서는 실제 API 요청을 하는 것이다.

이때 중간에 cors에러를 해결하는 것이다.

 

 

정말 간단하게 서버를 만들어보자

먼저 index,js다.

import express from 'express'
import cors from 'cors'
import test from './test.js'

const app = express()

app.use(cors())

app.use('/api', test)

app.get('/', (req, res) => {
  res.send('server.open')
})

const port = 5000
app.listen(port, () => {
  console.log(`${port}가 열렸어요`)
})

express를 불러오고 cors라이브러리를 불러와서 cors를 사용한다.

이를 통해 CORS에러를 해결할 수 있다.

 

그리고 app.use('/api')로

/api 경로로 들어오는 모든 요청에 대해 test 모듈을 사용한다.

이는 /api 경로로 들어오는 모든 요청이 test 모듈에서 처리됨을 의미한다.

 

app.get('/')는 루트 경로(/)로 들어오는 GET 요청에 대해 'server.open'을 응답으로 보냅니다.

 

app.listen으로 port번호를 설정해준다.

서버가 특정 포트(여기서는 5000번 포트)에서 리스닝하도록 설정하고, 서버가 시작되면 콘솔에 메시지를 출력한다.

import express from 'express'
import converter from 'xml-js'
import dotenv from 'dotenv'
import axios from 'axios'

dotenv.config()
const router = express.Router()
const url = 'http://api.nongsaro.go.kr/service/prvateTherpy/prvateTherpyList'
const apiKey = process.env.VITE_API_KEY

router.get('/', async (req, res) => {
  try {
    const pageNo = req.query.pageNo
    const numOfRows = req.query.numOfRows

    const response = await axios.get(url, {
      params: {
        apiKey,
        pageNo,
        numOfRows
      }
    })
    const xmlToJson = converter.xml2json(response.data)
    res.send(xmlToJson)
  } catch (error) {
    console.error(error)
    res.status(500).send('Internal Server Error')
  }
})

export default router

그 다음은 실제 Open API를 요청하는 파일이다.

간단하게 url 주소로 axios요청을하고 

params에 apiKey를 넣어준다.

이때 주의할 점!
나는 쿼리문과 파라미터를 헷갈려했다.
params면 파라미터아니야?
apiKey는 쿼리문으로 날려야하는데?
라고 생각했는데 
axios에서는 파라미터를 "~~~/:postId" 이런식으로 하는 것을 파라미터라고 하고,
params에 넣는 것을 쿼리라고한다.
왜 헷갈렸는지는 모르겠다.. params니까 파라미터 아닌가? 했나..

아무튼 axios요청을 하고, xml-js 라이브러리를 설치해서 xml데이터를 json으로 바꿔줘야한다.

그리고 그 데이터를 res.send로 응답한다.

어떻게 보면 간단하다.

 

여기서 발생한 문제는

api키를 환경변수로 사용하려는데에 문제가 생겼었다.

 

나는 Vite를 사용하니까 vite에서 제공하는 import.meta.env를 사용해야지 했는데

계속 환경변수를 못찾았다.

찾아보니 아래의 이유 때문이었다.

Vite는 기본적으로 클라이언트 사이드 코드에서 사용되는 환경 변수를 지원합니다.
그러나 서버 사이드 코드(예: Express 앱)에서는 import.meta.env를 사용하여 Vite 환경 변수에 직접 액세스하는 것이 기본적으로 지원되지 않습니다.
서버 사이드 코드에서 환경 변수를 사용하려면 별도의 방법을 사용해야 합니다. 예를 들어, 서버 환경 변수를 설정하고 해당 변수를 서버 코드에서 사용하는 것이 일반적인 접근 방법입니다.
그래서 dotenv를 설치해서 사용해야한다.

아무튼 그래서 dotenv를 설치하고 간단하게 문제를 해결했다..(1시간이나 걸렸다..)

 

그럼 이제 서버를 구축했으니까 데이터를 프론트엔드에서 요청해보자

 

 

클라이언트에서 데이터 요청하기

import axios from 'axios'
import { HerbListPageParams } from '~/types/herbList'

export const getHerbList = async ({
  pageNo,
  numOfRows
}: HerbListPageParams) => {
  const response = await axios.get('http://localhost:5000/api', {
    params: {
      pageNo,
      numOfRows
    }
  })
  return response.data.elements[0].elements[1].elements[0].elements
}

이렇게만 하면된다.

정말 간단하지않나?

우리가 만든서버에 데이터요청만 보내면 된다.

 

근데 만들면서든 생각인데

그럼 배포하게되면 어떻게 하는거지..? 라는 생각이 들었다.

localhost만 바꾸면 되는건가하고 일단 생각을 멈췄다.

 

아무튼 reture데이터가 조금 이상하게 긴데 

오픈API의 데이터가 조금 이상하게 형성되어있어서 위와 같이 했다.

 

TanStack Query도 사용해보자!

import { useQuery } from '@tanstack/react-query'
import { getHerbList } from '~/api/herbList'
import { HerbListPageParams } from '~/types/herbList'

const useGetHerbList = ({ pageNo, numOfRows }: HerbListPageParams) => {
  const result = useQuery({
    queryKey: ['herb', { pageNo, numOfRows }],
    queryFn: () => getHerbList({ pageNo, numOfRows })
  })

  return result
}

export default useGetHerbList

나는 TanStack Query도 사용했다.

isLoading이 있어서 너무 편했다.

근데 이걸 사용하면서 잠깐 깨달은 점이 있다.

 

내가 간단하게 구현하려고 버튼을 만들고 state를 만들어서 버튼을 누르면 다음 페이지를 요청하게끔 간단하게 만들려고 했다(추후에는 useInfiniteQuery 를 이용할 것이다.)

 

근데

나는 원래 useQuery로 요청하고,

state를 변경하게 되면 쿼리를 invalidateQueries로 무효화한 다음(캐싱을 제거한다고 생각했음)

컴포넌트가 재호출 될 때

바뀐 인수를 넣어서 다시 useQuery를 실행 후

바뀐 page 값으로 api 요청을 보내면 최신화된 데이터를 출력하겠지?

라고 생각을 했다.

 

하지만 

useQuery를 요청하고 state를 변경한 후에 invalidateQueries로 무효화를 한다고 해도

캐싱 데이터를 쓰는 게 아닐 뿐 useQuery는 처음에 했던 요청을 똑같이 수행하게 된다.

즉 캐싱데이터를 갖고오는 것도 아니고, 이전 요청을 똑같이 또 수행하는 것이었다.

 

그래서 버튼을 눌러서 state가 변경되도 1page 데이터만 불러오고 있었다.

이해가 안가서 검색해보니

아래와 같았다.

실제로 invalidateQueries()는 해당 쿼리의 캐시를 무효화하는 것이지, 자동으로 다시 요청하는 것은 아닙니다.
invalidateQueries()를 호출하면 해당 쿼리의 캐시가 무효화되어, 다음에 해당 쿼리에 대한 요청이 발생할 때 React Query가 다시 데이터를 가져오게 됩니다.
이때 해당 쿼리의 queryFn이 호출되어 실제로 서버에 요청을 보냅니다.
따라서, invalidateQueries()를 호출하면 해당 쿼리에 대한 데이터가 무효화되지만, 이로 인해 즉시 다시 서버에 요청이 보내지는 것은 아닙니다.
대신, 해당 쿼리에 대한 요청이 다시 발생할 때에 React Query가 서버에 요청을 보내게 됩니다.

queryKey는 React Query에서 캐시를 관리하고 데이터를 가져오기 위한 고유 식별자입니다.
기본적으로 queryKey는 정적인 값이어야 하지만, 특정 경우에 동적으로 변경되는 매개변수가 있다면, 이를 반영해 주어야 합니다.
그렇지 않으면 React Query가 동일한 queryKey를 사용하여 요청을 캐시하고, queryClient.invalidateQueries()를 호출할 때도 동일한 키로 캐시된 데이터를 무효화할 것입니다.
매개변수가 동적으로 변경될 때마다 React Query가 자동으로 해당 매개변수를 반영하여 고유한 queryKey를 생성하게끔 하려면, queryKey에 동적 매개변수를 포함시켜야 합니다.
이렇게 하면 동일한 쿼리이지만 매개변수에 따라 다른 queryKey가 생성되어 React Query가 올바른 데이터를 다시 가져올 수 있습니다.
물론, 만약 동적으로 변경되는 매개변수가 쿼리의 결과에 영향을 미치지 않는 경우라면, 동적인 queryKey를 사용하지 않고도 쿼리를 성공적으로 다시 불러올 수 있습니다.
하지만 일반적으로는 매개변수가 변경될 때마다 해당 매개변수를 반영하여 queryKey를 업데이트하는 것이 좋습니다.

즉 쉽게 말하면 쿼리의 캐시가 무효화 되는 거지 useQuery는 해당 키  값으로된 요청을 똑같이 보낸다는 것이다.

나는 무효화 되면 캐시가 완전히 삭제되어서 새롭게 시작하는 건줄 알았다.

아래 내용이 중요하다.

 

동적 매개변수가 없는 경우, invalidateQueries()를 호출하면 해당 쿼리의 캐시가 무효화되어도,

React Query는 이전의 queryKey를 기반으로 한 요청을 다시 보내게 됩니다.

이는 queryKey가 변경되지 않았다고 판단하기 때문입니다.

예를 들어, 동적 매개변수가 없는 쿼리의 경우:

const { data } = useQuery('exampleQuery', fetchDataFunction);

 

 

이 때 queryKey는 'exampleQuery'이며, invalidateQueries('exampleQuery')를 호출하면 이전과 동일한 'exampleQuery'로 쿼리가 다시 보내집니다.

동적 매개변수가 없는 경우, React Query는 invalidateQueries() 호출 이후에도

queryKey의 변화를 감지하지 않으므로, 이전에 사용된 queryKey로 다시 데이터를 요청하게 됩니다.

만약 동적 매개변수를 사용하고 있다면,

queryKey에 동적으로 변하는 값을 포함시켜서 React Query가 변화를 감지할 수 있도록 해야 합니다.

 

아무튼 그래서 query키를 무효화하는 함수를 호출할 게 아니라

쿼리키 값에 동적 매개변수를 넣어줘야하는 것이었다.

 

 

아무튼 성공!

import { useState } from 'react'
import useGetHerbList from '~/hooks/queries/useGetHerbList'
import { HerbInfos } from '~/types/herbList'

const CardListPage = () => {
  const [page, setPage] = useState(1)

  const herbList = useGetHerbList({ pageNo: page, numOfRows: 10 })

  if (herbList.isLoading) return <div>로딩중...</div>
  return (
    <>
      <ul>
        {herbList.data.slice(0, -3).map((herb: HerbInfos) => (
          <li key={herb.elements[1].elements[0].cdata}>
            {herb.elements[2].elements[0].cdata}
          </li>
        ))}
      </ul>
      <button
        onClick={() => {
          setPage(prev => prev + 1)
        }}>
        다음페이지로
      </button>
    </>
  )
}
export default CardListPage

위 로직으로 데이터를 성공적으로 출력하고 페이지 호출도 성공했다.

일단은 간단하게 만든 거니까  이후 리팩토링을 할 예정이다.

 

 

간단하게 서버를 구축해봤는데

나중에 또 한번 공부해서 해봐야 할 것 같다..

 

728x90
반응형
LIST