Desktop, mobile
사건의 발단은 간단하다. 친구 짐정리를 도우러갔다가 헌옷수거함을 찾기가 어려웠던것에서부터 이 프로젝트는 시작되었다. 흔히 보인이던 헌옷수거함이 막상 찾으니까 없었다.
헌옷 수거함을 찾기 어려웠다는 경험에서 시작된 프로젝트로, 각 자치구별 헌옷 수거함의 위치를 한눈에 확인할 수 있는 웹사이트를 제작하였다.
🚮 https://ohbin.vercel.app/
📒 Notion
🐙 github
Next.js의 API Routes를 사용해 서버 사이드에서 필요한 데이터를 가져왔다.
따라서 api/area/gu 폴더 내에 서울시의 다양한 구역에 대한 주소 정보를 API를 통해 가져와서, 각 구역별로 주소 데이터를 반환하는 기능을 구현한 함수 fetchArea를 작성하였다.
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, '데이터를 가져오는 중 오류가 발생했습니다.');
}
};
단일 키를 사용하여 쿼리의 변경 최소화
자동 캐싱
useQuery는 자동으로 데이터를 캐시한다. 이는 동일한 queryKey를 사용하는 요청에 대해 서버 호출을 방지하고, 캐시된 데이터를 빠르게 반환한다. 캐시된 데이터는 staleTime이 경과할 때까지 유지된다.
// 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 상태로 유지됨
});
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['도로명주소'];
}
}
대표적인 예로 검색한 주소를 중심으로 상단부에만 마커들이 나타나고 아래에는 나타나지 않는경우이다. 이유는 아래 사진에서 보이듯이 상단부는 강서구 하단부는 양천구이기떄문에, 헌옷수거함은 검색한 주소가 속한 강서구의 정보만 가져오기때문이다.
각 자치구별 의류수거함 정책을 정리한 이용가이드 페이지이다.
사용자가 읽고 싶은 페이지 + 보고 이해하기 쉬운 페이지가 되려면 개선이 필요해보인다.

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