본문 바로가기

개발자 강화/프론트엔드

[개발][BFF 도전기] 부동산 공공데이터 API 뜯어보고 BFF API 설계하기

 

참 많은 일이 있었어요

 

부동산 공공데이터 API를 단독/다가구만 가져와보려고 했는데

단독/다가구 형태의 주택은 전체 건물의 주인이 1명인 경우를 지칭한다고 하더라구요

(진짜 API 까면서 부동산 공부 강제로 하게됨 완전 럭키비키잖아)

 

그래서 제가 구해야 하는 집은 이런 형태보다 뭔가 오피스텔에 가깝지 않나...API 추가할까...하다가

 

API 여러 개를 다 가져와서 데이터 처리하는 게 BFF API 연습 취지에 더 맞을 것 같아서

그냥 해보기로 했습니다

 

오피스텔, 단독/다가구, 연립다세대, 아파트

이렇게 4가지를 불러올게요

 

공공데이터 부동산 전월세 실거래가 API

 

1. 국토교통부_단독/다가구 전월세 실거래가 자료

https://www.data.go.kr/data/15126472/openapi.do

2. 국토교통부_연립다세대 전월세 실거래가 자료

https://www.data.go.kr/data/15126473/openapi.do

3. 국토교통부_아파트 전월세 실거래가 자료

https://www.data.go.kr/data/15126474/openapi.do

4. 국토교통부_오피스텔 전월세 실거래가 자료

https://www.data.go.kr/data/15126475/openapi.do

 

공공데이터 API 문서 까보기

자 그럼 API 문서를 볼까요?

 

API Docs인데 hwp를 곁들인...

 

WA!!! API Docs가 hwp라니!!!!!!

 

정말 신나는군요... 4가지 hwp를 모두 다운받아 봅시다

 

벌써 두려워

 

그럼 이제 API reponse 구조를 볼까요?

 

와!!! 속성 포함 여부랑 순서가 조금씩 다 다르잖아!!!!

 

아니 솔직히 같은 종류 API인데 속성도 비슷하면 순서만 조금 맞춰 주시면 안될까요...엉엉슨

 

bold처리한 건 눈으로 보면서

공통 속성은 스킵하고

4개 중 1개라도 미포합하고 있는 속성이면, bold 처리 했습니다

 

근데 네이밍 좀 보고 있으면 신박함

 

umdNm = 읍면동 네임

sggNm = 시군구 네임

jibun = 지번

 

이거 보고 혼자서 좀 낄낄거림..ㅎ

안웃겼음 ㅈㅅ합니다

 

와 무려 채신기술 스웨거 도입한 정부 서비스? 정말 칭찬해

 

공공데이터 페이지를 전체 화면으로 전환하면

하단에 swagger가 나타납니다

 

저기요 api docs랑 schema가 다른데요.  원만한 합의 부탁드립니다

 

하지만 응답 예상 스키마 속성이 hwp에 써있는 것과 좀 다른 경우가 있습니다

믿고 있다가 뒤통수를 맞았어요

 

요즘은 gpt 시켜도 자기 맘대로 데이터 바꿔버리는 경우가 좀 있어서

결국 휴먼 검수를 하고 있음

 

그냥 제 눈과 손으로 해보겠습니다

 

AI야 솔직히 요즘 환율도 너무 올랐는데 밥값을 해야지...

나는 밥 사먹을 돈 아껴서 니 친구비 낸다

 

 

부동산 공공데이터 API 응답 분석 & BFF API 설계하기

공공 데이터 API의 응답은 굉장히 날것으로 옵니다

 

 

이걸 프론트엔드가 먹기 좋게 잘라봅시다

그게 BFF가 존재하는 이유니까(끄덕)

 

일단 4개 API 다 공통으로 응답하는 결과를 모아봅시다

api 공통 속성

원본 공공데이터 API 응답 구조

export interface RentBaseItem {
  sggCd: "string"; //지역코드(시군구 코드라는 뜻) ex: 11110
  umdNm: "string"; //법정동 ex: 청운동

  dealYear: "string"; //계약년 ex: 2025
  dealMonth: "string"; //계약월 ex: 02
  dealDay: "string"; //계약일 ex: 15

  deposit: "string"; //보증금 ex: 10000
  monthlyRent: "string"; //월세세 ex: 100

  contractTerm: "string"; // 계약기간 ex: 25.03~27.03
  contractType: "string"; // 계약구분 ex: 신규

  useRRRight: "string"; // 갱신요구권 사용
  preDeposit: "string"; // 종전계약보증금
  preMonthlyRent: "string"; // 종전계약월세
}

 

속성 값이랑 예시 데이터는 api docs("그" 한글 파일) 및 실제 api 응답 참고했습니다

 

속성값을 비슷한 카테고리로 묶어봤어요

 

위치 정보: 지역코드+법정동
계약 날짜: 계약연+월+일
가격 정보: 보증금+월세
계약 정보: 계약 기간+구분
계약 만료 관련 정보: 갱신 요구권+종전계약보증금+종전계약월세

 

 

여기에서 계약 날짜랑 계약 정보는 같은 "계약" 카테고리로 묶을 수 있겠군요

그리고 계약 만료 관련 정보는 굳이 FE에서 안보여줘도 될 것 같아요

BFF 설계 할 때 날려도 될 듯

 

BFF API 가공 후 응답 구조

export interface FrontendRentBase {
  id: string;
  price: {
    deposit: string; //deposit
    monthlyRent: string; //monthlyRent
  };
  location: {
    district: string; //umdNm
  };
  contract: {
    date: string; //dealYear + dealMonth + dealDay
    term: string; // contractTerm
    type: string; // contractType
  };
}

 

방금 말한대로 정보를 묶어보면 이렇습니다

계약 연+월+일 데이터는 한 스트링으로 묶어서 date에 할당했어요

 

BFF API에서 위 공공 데이터 공통 속성을 가공해서 이 구조로 바꿔서 프엔에 쏴줄겁니다

 

이렇게 공통 interface을 extends해서 각 4개의 BFF API 응답 형태로 가공한다는 걸 도식화?한...

 

그리고 오늘의 토막 상식 알려드릴게요

전용면적이란?
공동 주택에서 소유자가 독점하는 면적
(복도식 아파트에서 문 앞 공간까지 면적으로 치는 그런거)

연면적이란?

각 층 바닥 면적의 합(단독 주택에서 주로 쓰는 듯)

 

api 속성 까면서 공부도 하구 완전 럭키 어쩌구!~!~!

 

오피스텔 실거래가 api

원본 공공데이터 API 응답 구조

// 오피스텔
export interface RentOfficetelItem extends RentBaseItem {
  sggNm: "string"; //시군구 ex: 종로구

  jibun: "string"; // 지번: ex: 1425

  //api 문서에 offiname이라 되어있는데 실제론 offiNm임
  offiNm: "string"; // 오피스텔명: 한라운종가
  excluUseAr: "string"; // 전용면적: 59.97

  floor: "string"; // 층: 9
  buildYear: "string"; // 건축년도: 2016
}

자 이제 오피스텔 api에만 있는 속성입니다

 

문서랑 실제 응답 속성 다른거 땜에 빡쳐서 주석 달아놨었네여..ㅋㅋ

 

이 속성 중 오피스텔 이름, 면적, 층수, 건축년도는 '건물 정보'로 묶을 수 있겠네요

 

시군구네임(sggNm)과 지번은 공통 속성 중 위치 정보에 때려넣음 되겠군요

 

BFF API 가공 후 응답 구조

export interface FrontendRentOfficetel {
  id: string;
  buildingInfo: {
    name: string; //offiNm
    area: string; //excluUseAr
    floor: string; //floor
    year: string; //buildYear
  };
  price: {
    deposit: string; //deposit
    monthlyRent: string; //monthlyRent
  };
  location: {
    district: string; //sggNm + umdNm + jibun
  };
  contract: {
    date: string; //dealYear + dealMonth + dealDay
    term: string; // contractTerm
    type: string; // contractType
  };
}

 

위치 속성 값 맨 뒤에 지번을 끼워넣었어요

 

건물 정보 속성 아래에

전용면적은 area, 오피스텔 건물명은 name, 층 floor, 건축년도 year로 맵핑해줬습니다

 

BFF API 내부, 응답 구조에 맞게 데이터 가공

const transformedData: FrontendRentOfficetel[] =
  rawData.data.items.item.map((item, index) => ({
    id: `${item.dealYear}${item.dealMonth}${item.dealDay}-${index + 1}`,
    buildingInfo: {
      name: item.offiNm,
      area: item.excluUseAr,
      floor: item.floor,
      year: item.buildYear,
    },
    price: {
      deposit: item.deposit,
      monthlyRent: item.monthlyRent,
    },
    location: {
      district: `${item.sggNm} ${item.umdNm} ${item.jibun}`,
    },
    contract: {
      date: `${item.dealYear}${item.dealMonth}${item.dealDay}`,
      term: item.contractTerm,
      type: item.contractType,
    },
  }));

 

실제로 BFF API 내부에서는 이렇게 맵핑해줬습니다

 

아파트 실거래가 api

원본 공공데이터 API 응답 구조

export interface RentApartmentItem extends RentBaseItem {
  jibun: "string"; // 지번 ex: 596

  aptNm: "string"; //아파트명 ex: 삼성
  excluUseAr: "string"; // 전용면적: 59.97
  floor: "string"; // 층 ex: 9
  buildYear: "string"; // 건축년도 ex: 2016
}

 

지번은 아까처럼 위치 속성에 묶고

 

아파트 이름+전용면적+층+건축년도는 건물 정보에 넣음 되겠네요

 

BFF API 가공 후 응답 구조

export interface FrontendRentApartment {
  id: string;
  buildingInfo: {
    name: string; //aptNm
    area: string; //excluUseAr
    floor: string; //floor
    year: string; //buildYear
  };
  price: {
    deposit: string; //deposit
    monthlyRent: string; //monthlyRent
  };
  location: {
    district: string; //umdNm + jibun
  };
  contract: {
    date: string; //dealYear + dealMonth + dealDay
    term: string; // contractTerm
    type: string; // contractType
  };
}

 

건물 정보 중 name 속성에 아파트명이 할당되었습니다

여기는 시군구 속성이 없어서 지번만 주소 값 뒤에 껴넣었습니다

 

BFF API 내부, 응답 구조에 맞게 데이터 가공

const transformedData: FrontendRentApartment[] =
  rawData.data.items.item.map((item, index) => ({
    id: `${item.dealYear}${item.dealMonth}${item.dealDay}-${index + 1}`,
    buildingInfo: {
      name: item.aptNm,
      area: item.excluUseAr,
      floor: item.floor,
      year: item.buildYear,
    },
    price: {
      deposit: item.deposit,
      monthlyRent: item.monthlyRent,
    },
    location: {
      district: `${item.umdNm} ${item.jibun}`,
    },
    contract: {
      date: `${item.dealYear}${item.dealMonth}${item.dealDay}`,
      term: item.contractTerm,
      type: item.contractType,
    },
  }));

 

BFF API 내부에서는 이렇게 맵핑했습니다

 

단독/다가구 실거래가 api

원본 공공데이터 API 응답 구조

export interface RentSingleMultiFamilyItem extends RentBaseItem {
  houseType: "string"; // 주택 유형(단독 or 다가구)

  totalFloorAr: "string"; // 연면적 ex: 55

  buildYear: "string"; // 건축년도 ex: 2016
}

 

주택유형+연면적+건축년도 다 건물 정보네요

 

BFF API 가공 후 응답 구조

export interface FrontendRentSingleMultiFamily {
  id: string;
  buildingInfo: {
    type: string; // houseType
    area: string; //totalFloorAr
    year: string; //buildYear
  };
  price: {
    deposit: string; //deposit
    monthlyRent: string; //monthlyRent
  };
  location: {
    district: string; //umdNm
  };
  contract: {
    date: string; //dealYear + dealMonth + dealDay
    term: string; // contractTerm
    type: string; // contractType
  };
}

 

주택 유형은 type 속성에 맵핑 해줬습니다

 

BFF API 내부, 응답 구조에 맞게 데이터 가공

const transformedData: FrontendRentSingleMultiFamily[] =
  rawData.data.items.item.map((item, index) => ({
    id: `${item.dealYear}${item.dealMonth}${item.dealDay}-${index + 1}`,
    buildingInfo: {
      type: item.houseType,
      area: item.totalFloorAr,
      year: item.buildYear,
    },
    price: {
      deposit: item.deposit,
      monthlyRent: item.monthlyRent,
    },
    location: {
      district: item.umdNm,
    },
    contract: {
      date: `${item.dealYear}${item.dealMonth}${item.dealDay}`,
      term: item.contractTerm,
      type: item.contractType,
    },
  }));

 

실제로 BFF API 내에서 이렇게 데이터를 맵핑했습니다

 

연립다세대 전월세 실거래가 api

 

원본 공공데이터 API 응답 구조

export interface RentMultiHouseholdItem extends RentBaseItem {
  houseType: "string"; //주택 유형(연립 or 다세대)
  floor: "string"; //층 ex: 2
  mhouseNm: "string"; //연립다세대명 ex: 북악더테라스2단지
  excluUseAr: "string"; //전용면젹 ex: 84.99
  buildYear: "string"; //건축년도

  jibun: "string"; //지번 ex: 211-11
}

 

주택유형+층+연립다세대명+전용면적+건축년도 다 건물 정보에 넣고

지번만 위치 정보에 넣음 되겠네용

 

BFF API 가공 후 응답 구조

export interface FrontendRentMultiHousehold {
  id: string;
  buildingInfo: {
    type: string; // houseType
    name: string; //mhouseNm
    area: string; //execluUseAr
    floor: string; //floor
    year: string; //buildYear
  };
  price: {
    deposit: string; //deposit
    monthlyRent: string; //monthlyRent
  };
  location: {
    district: string; //umdNm + jibun
  };
  contract: {
    date: string; //dealYear + dealMonth + dealDay
    term: string; // contractTerm
    type: string; // contractType
  };
}

 

건축 정보에 속성을 추가해줬습니당

(윗부분과 겹치는 설명은 일부러 생략하고 있습니다!)

 

const transformedData: FrontendRentMultiHousehold[] =
  rawData.data.items.item.map((item, index) => ({
    id: `${item.dealYear}${item.dealMonth}${item.dealDay}-${index + 1}`,
    buildingInfo: {
      type: item.houseType,
      name: item.mhouseNm,
      area: item.excluUseAr,
      floor: item.floor,
      year: item.buildYear,
    },
    price: {
      deposit: item.deposit,
      monthlyRent: item.monthlyRent,
    },
    location: {
      district: `${item.umdNm} ${item.jibun}`,
    },
    contract: {
      date: `${item.dealYear}${item.dealMonth}${item.dealDay}`,
      term: item.contractTerm,
      type: item.contractType,
    },
  }));

 

요렇게 응답 구조에 맞게 가공해줍니당

 


앞으로 개선해나갈 내용

보시다시피 BFF API 응답 구조에 맞게 가공하는 함수에서

4개의 API 응답에 겹치는 속성이 있다보니

중복되는 코드가 있어요

그래서 이걸 공통적으로 사용하는 함수를 만들어서 최대한 최적화를 해보려구 합니다!

 

전체 코드를 깃허브에서 보고 싶다믄

 

https://github.com/Shimsuyeon/jip-finder-ssr-bff

 

GitHub - Shimsuyeon/jip-finder-ssr-bff: 이 넓은 서울땅에 내 집 하나는 있겠지...

이 넓은 서울땅에 내 집 하나는 있겠지... Contribute to Shimsuyeon/jip-finder-ssr-bff development by creating an account on GitHub.

github.com

 

여기에서

src/app/bff/routes/rent.routes.ts (BFF 응답 형태에 맞게 가공)

src/app/bff/types/rent.types.ts (공공데이터 API 원본 응답 구조, BFF API 가공 후 구조)

 

이 두 파일을 확인하세용😊🍀

 


 

즐거운 코딩 되세요~~

저는 더 최적화하러 갑니당

 

2025.02.20. 오전 2시 14분