Post

공공 API 활용하기: 옷체통

done_oh_de Desktop, mobile

done_oh_mo

프로젝트 개요

사건의 발단은 간단하다. 친구 짐정리를 도우러갔다가 헌옷수거함을 찾기가 어려웠던것에서부터 이 프로젝트는 시작되었다. 흔히 보인이던 헌옷수거함이 막상 찾으니까 없었다.

  • ‘자치구’ 마다 헌옷수거에 대한 정책이 달랐다. 👉🏻 자치구별 정책 정리
  • 송파구는 지도로 찾기쉽게 알려주던데.. 강서구는 없더라구요?

헌옷 수거함을 찾기 어려웠다는 경험에서 시작된 프로젝트로, 각 자치구별 헌옷 수거함의 위치를 한눈에 확인할 수 있는 웹사이트를 제작하였다.

주요기능

  • 주소 기반 현재 위치 표시
  • 자치구별 헌옷 수거함 위치 표시
  • 자치구별 정책 설명

🚮 https://ohbin.vercel.app/ 📒 Notion 🐙 github

문제사항 및 해결 방안

1. 데이터에 위도경도가 없는경우

문제

  • 지도에 마커를 표기하기위해서는 해당 주소의 위도,경도 데이터가 필요했다. 하지만 공공데이터 포털에서 제공하는 일부 주소API에는 위도경도가 없는 경우가 있었다.

해결방안

  • 위도, 경도 데이터 추가 요청: 공공 데이터 제공 담당자에게 요청하여 필요한 데이터를 확보했다.

샤라웃 투 각 자치 구 공공데이터 제공담당자님🙏🏻

done_oh_de

done_oh_mo

2. 내가 검색한 주소가 어디에 위치한지 모른다

문제

  • 주소 api는 한 번의 요청으로 모든 데이터를 반환하지 않고, 페이지네이션(pagination)을 사용한다. 즉, 데이터를 여러 페이지로 나누어 제공하며, 각 페이지는 지정된 수의 항목(perPage)을 포함한다. 하지만 나는 내가 검색한 주소가 어느 구의 전체 데이터에서 몇페이지에 위치하는지 모른다. 즉, 호출한 api의 결과에 내가 검색한 주소가 포함되지 않을 수 있다.
  • 위 문제를 해결하기 위해 모든 데이터를 불러오기 위해 임의의 페이지수(11개)를 넣어서 모든 페이지를 호출하였다.
  • 그러면 무거운 API를 자주 호출하게 되는건 아닐까?

해결방안

1. API Routes 활용:

Next.js의 API Routes를 사용해 서버 사이드에서 필요한 데이터를 가져왔다.

  • API Routes는 Next.js에서 서버 사이드 로직을 작성할 수 있게 해주는 기능이다. 이는 클라이언트와 서버 사이의 통신을 단순화하며, 서버 사이드 렌더링 및 데이터 페칭에 유용하다.
    • 폴더 구조: /app/api 폴더 내의 모든 파일은 /api/* 경로에 대응되며, API 엔드포인트로 취급
    • 서버 사이드 번들링: API Routes는 서버 사이드에서만 실행되며, 클라이언트 사이드 번들 크기를 증가시키지 않음.
    • 서버 사이드 데이터 처리: API Routes를 사용하여 서버 측에서 데이터 처리 및 변환을 수행하고, 클라이언트로 데이터를 전달 가능.

따라서 api/area/gu 폴더 내에 서울시의 다양한 구역에 대한 주소 정보를 API를 통해 가져와서, 각 구역별로 주소 데이터를 반환하는 기능을 구현한 함수 fetchArea를 작성하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export const fetchArea = async (area: SeoulGuType, perPage: number) => {
  const apiKey = process.env.NEXT_PUBLIC_GO_DATA_DECODING_KEY;

  if (!apiKey) {
    throw new ApiError(403, 'API 키가 설정되지 않았습니다. 환경 변수를 확인하세요.');
  }

  if (area === 'Gwangjin' || area === 'Guro' || area === 'Seongdong' || area === 'Mapo' || area === 'Eunpyeong') {
    throw new ApiError(404, '해당지역은 서비스 준비중입니다🐥');
  }

  if (
    area === 'Gangdong' ||
    area === 'Gangbuk' ||
    area === 'Nowon' ||
    area === 'Dobong' ||
    area === 'Yongsan' ||
    area === 'Jung' ||
    area === 'Dongjak'
  ) {
    throw new ApiError(404, '해당지역은 관련정보를 제공하고 있지 않습니다. 해당 구청에 문의 바랍니다.');
  }

  const areaUrl = SeoulAreas[area];
  if (!areaUrl) {
    throw new ApiError(400, `해당 지역(${area})의 URL이 설정되어 있지 않습니다.`);
  }

  const req = Array.from({ length: 11 }, (_, idx) => {
    return API.get<PageDto<SeoulGuAddressType>>(areaUrl, {
      params: { page: idx + 1, perPage, serviceKey: apiKey },
    });
  });

  try {
    const res = await Promise.all(req);

    const result: AddressDto[] = [];

    res.forEach(({ data }) => {
      data.data.forEach((d) => {
        if (d) {
          let converted: AddressDto = {
            gu: area,
            lat: d.위도,
            lon: d.경도,
            fullAddress: getFullAddress(d) ?? '-',
          };
          result.push(converted);
        }
      });
    });

    return result;
  } catch (err: any) {
    throw new ApiError(500, '데이터를 가져오는 중 오류가 발생했습니다.');
  }
};
  • 에러 처리:
    • API 키 미설정: API 키가 설정되지 않았을 때 403 에러
    • 서비스 준비 중: 서비스 준비 중인 지역이나 정보 제공이 없는 지역에 대해 404 에러
    • URL 미설정: 설정되지 않은 URL의 경우 400 에러
    • 데이터 요청 오류: 데이터 요청 중 오류가 발생하면 500 에러

2. React-Query 사용: 데이터 캐싱과 자동 갱신을 통해 불필요한 API 호출을 줄였다.

  • 단일 키를 사용하여 쿼리의 변경 최소화
    • React Query는 각 쿼리를 식별하기 위해 쿼리 키(query key)를 사용한다. 이 키를 통해 쿼리의 상태를 관리하고, 캐시를 효율적으로 업데이트할 수 있다.
    • 쿼리 키를 고유하게 정의하면 React Query는 해당 키에 대해 데이터를 캐싱하고, 필요한 경우에만 새로 데이터를 가져온다. 이렇게 하면 같은 쿼리에 대해 불필요한 API 호출을 줄일 수 있다.
  • 자동 캐싱 useQuery는 자동으로 데이터를 캐시한다. 이는 동일한 queryKey를 사용하는 요청에 대해 서버 호출을 방지하고, 캐시된 데이터를 빠르게 반환한다. 캐시된 데이터는 staleTime이 경과할 때까지 유지된다. ```ts // use-markers.ts const fetchMarkers = useCallback(async (gu: SeoulGuType) => { try { const res = await fetch(/api/area/gu?type=${encodeURIComponent(gu)});

    } catch (err: any) { console.error(‘Fetch error:’, err); return []; } }, []);

const markersQuery = useQuery({ queryKey: [markers-seoul-${gu}], // 단일 키를 사용하여 쿼리의 변경 최소화 queryFn: () => gu && fetchMarkers(gu), staleTime: 1000 * 60 * 2, // 5분간 데이터가 fresh 상태로 유지됨 });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
## 3. 주소 데이터 변환
### 문제
- 자치구별로 제공하는 데이터 형식이 달라 일관된 데이터 형식으로 변환이 필요하다.
👉🏻 [자치구별 API 및 데이터 정리](https://www.notion.so/don9z/API-e2f84a9007ce466c938bb168dd85cbb4)

### 해결방안
- AddressDto로 변환하여 일관된 형식의 데이터를 반환하였다.
- getFullAddress 함수를 통해 주소 형식을 표준화.

```ts
res.forEach(({ data }) => {
  data.data.forEach((d) => {
    if (d) {
      let converted: AddressDto = {
        gu: area,
        lat: d.위도,
        lon: d.경도,
        fullAddress: getFullAddress(d) ?? '-',
      };
      result.push(converted);
    }
  });
});


function getFullAddress(data: SeoulGuAddressType) {
  switch (data.type) {
    case 'Dongdaemun':
      return data.주소 + ' ' + data.상세주소;

    case 'Seodaemun':
      return data['설치장소(도로명)'];

    case 'Gangnam':
      return data['도로명 주소'];

    case 'Gangseo':
      return data['설치장소(도로명주소)'];

    case 'Songpa':
      return data['설치장소'];

    case 'AddressBasicInfo':
    /* falls through */
    default:
      return data['도로명주소'];
  }
}

한계점

1. 인접 자치구의 위치정보 탐색 불가

  • 문제: 현재 로직에서는 검색한 주소가 속한 자치구의 헌옷 수거함만 표시됨.

대표적인 예로 검색한 주소를 중심으로 상단부에만 마커들이 나타나고 아래에는 나타나지 않는경우이다. 이유는 아래 사진에서 보이듯이 상단부는 강서구 하단부는 양천구이기떄문에, 헌옷수거함은 검색한 주소가 속한 강서구의 정보만 가져오기때문이다.

  • 해결방안:범위 검색 현재 주소를 기준으로 일정 반경 내의 헌옷 수거함을 검색하도록 로직을 수정하여 인접 자치구의 정보도 포함될 수 있도록 개선 필요.

map_disable_search

2. UI/UX 개선

윽!

각 자치구별 의류수거함 정책을 정리한 이용가이드 페이지이다. 사용자가 읽고 싶은 페이지 + 보고 이해하기 쉬운 페이지가 되려면 개선이 필요해보인다. map_disable_search

해당 페이지는 UI/UX 디자이너 지인에게 개선을 부탁했다.

help

참고사이트

This post is licensed under CC BY 4.0 by the author.