JS 에어비앤비 캘린더 연동 기능 구현기

박한영·2023년 3월 1일
5
post-thumbnail

💡 글 속 등장하는 HAU(하우)는 제주도 감성숙소 예약 플랫폼으로, 제가 개발에 참여하고 있는 서비스입니다.

구현 배경

HAU에서는 호스트분들께서 모든 예약 플랫폼의 예약 내역을 쉽게 관리하실 수 있도록 통합 캘린더 UI를 제공했다.
그런데 HAU 예약 내역은 캘린더에 자동으로 연동되지만, 타 플랫폼 예약 내역은 연동이 되지 않았다.
호스트분들은 타 플랫폼을 통해 예약이 들어올 때마다 HAU에 들어와 예약 내역을 일일이 등록해야 했다.
호스트분들께서 수동으로 타 플랫폼 예약 내역을 등록하는 과정에 불편함을 호소하셨다.

사실 더 큰 문제는, 종종 중복 예약 문제가 발생한 것이었다.
예약 내역 연동이 지연되거나 일부 누락될 경우 중복 예약이 발생했다.
HAU 예약이 차 있는 날짜에 에어비앤비에서 예약이 들어 오거나, 그 반대의 케이스가 발생한 것이었다.
이럴 경우, 더 나중에 예약을 신청하신 게스트분께 불가피하게 양해를 구하고 예약 취소를 해 드렸다.
이는 단순히 불편한 것을 넘어 서비스의 신뢰도에 영향을 주는 문제였다.

이런 문제가 반복되자 HAU 캘린더와 에어비앤비의 캘린더를 자동으로 동기화하는 기능을 개발하기로 결정했다.
캘린더 자동 연동이 되면 HAU 캘린더 사용이 편리해지고 동시에 중복 예약 문제를 해결할 수 있다고 판단했다.


에어비앤비 캘린더 연동 구현 🛠

HAU 캘린더와 에어비앤비 캘린더를 연동하는 데에는 크게 두 가지 기능이 필요했다.
첫 번째는, 에어비앤비의 예약 내역을 HAU로 연동하는 것이었다.
두 번째는, 반대로 HAU 예약 내역을 에어비앤비로 연동하는 것이었다.
다행히 두 기능 모두 에어비앤비에서 제공하고 있는 기능을 이용해 어렵지 않게 구현할 수 있었다.

캘린더 데이터 형식인 iCalendar에 대해 간략하게 짚은 뒤, 두 기능의 구현 과정을 순서대로 이야기해보겠다.

iCalendar?

캘린더 데이터는 일반적으로 iCalendar라고 하는 데이터 형식으로 다뤄진다.
iCalendar는 인터넷 상에서 일정 공유를 할 때 서로 다른 프로그램 간의 소통을 위해 만들어졌으며 RFC 5545에 기술된 인터넷 표준이다.

iCalendar는 크게 달력에 해당하는 캘린더 컴포넌트와 이벤트(VEVENT) 컴포넌트로 이루어져 있다.
캘린더 컴포넌트는 달력의 캘린더 스케일(달력의 시간 척도), 달력 작성자 정보와 같은 달력 자체에 관한 정보를 담고 있다.
이벤트 컴포넌트는 일정 시작 날짜, 끝 날짜, 일정 내용 등 일정에 대한 정보를 담고 있다.
캘린더 컴포넌트가 이벤트 컴포넌트를 포함하는 구조이다.

1. 에어비앤비 -> HAU 캘린더 연동

첫 번째로 에어비앤비의 예약 내역을 HAU 캘린더에 연동하기 위해서 아래와 같은 구현 과정을 거쳤다.

1) 에어비앤비에서 iCalendar 데이터 받아오기
2) 받아온 일정 데이터를 HAU 예약 스케줄 데이터 형식에 맞게 가공하기
3) 가공된 데이터를 HAU 데이터베이스에 알맞게 저장하기
4) 위의 과정을 일정 시간마다 자동 반복 수행되게 만들기

1) 에어비앤비에서 iCalendar 데이터 받아오기

다행히도 에어비앤비 호스트 모드에서는 iCalendar 형식으로 예약 내역 정보를 내보낼 수 있는 기능을 지원하고 있다.

에어비앤비에서 호스트 모드로 전환한 뒤, 숙소를 선택하고 "요금 및 예약 가능일 조정"을 선택해 들어간다.
해당 페이지에서 아래로 스크롤하다 보면 있는 달력 내보내기라는 항목을 클릭한다.
달력 내보내기 url에 GET 요청을 보내면 iCalendar 형식의 예약 일정 데이터가 반환된다.

2) 받아온 일정 데이터를 HAU 예약 스케줄 데이터 형식에 맞게 변환하기

에어비앤비로부터 받아온 iCalendar 형식의 데이터를 json으로 변환한다.
변환된 데이터에서 필요한 정보만 꺼낸 후, 예약 스케줄 생성을 위해 HAU 백엔드 api로 넘긴다.

*iCalendar 데이터를 json으로 변환하기 위해 ical2json이라는 모듈을 활용했다

import axios from "axios"
import ical2json from "ical2json";

const getRefinedAirbnbReservationData = async (airbnbUrl) => {
  // 주어진 airbnb URL에 GET요청을 보내 iCalendar 데이터를 받아온다
  const response = await axios.get(airbnbUrl);
  const { data } = response;

  // iCalendar 데이터를 JSON 형태로 변환한다.
  const icalData = ical2json.convert(data);

  // iCalendar 데이터로부터 예약 내역 정보를 추출한다.
  const reservations = icalData.VCALENDAR[0].VEVENT.map((event) => {
    const start = convertAirbnbDateToISO(event["DTSTART;VALUE=DATE"]);
    const end = convertAirbnbDateToISO(event["DTEND;VALUE=DATE"]);
    const status = event["SUMMARY"];
    return { start, end, status };
  });

  // 예약이 차 있거나, 예약이 불가능한 일정만 골라낸다(예약 가능 일정을 제외하여 blocking할 스케줄만 골라낸다).
  const reserved = reservations.filter(({ status }) => {
    return (
      status === AIRBNB_DATA_STATUS.RESERVED ||
      status === AIRBNB_DATA_STATUS.IS_UNAVAILABLE
    );
  });

  return reserved;
};


const synchronizeWithAirbnb = async ({ lodgmentId, lodgmentName, airbnbUrl }) => {
  const airbnbReservationData = await getRefinedAirbnbReservationData(airbnbUrl);

  // lodgment는 숙소를 가리키는 변수명이다
  const syncData = {
    lodgmentId,
    lodgmentName,
    data: airbnbReservationData,
  };

  // 숙소 id와 숙소 명, 가공된 예약 일정 데이터를 담은 post 요청(HAU 백엔드 api에 대한 요청)을 반환한다.
  return axios.post(AIRBNB_SYNC_URL, syncData);
};

const synchronizeHAUWithAirbnb = async () => {
  // 제휴 숙소들을 대상으로 연동을 진행한다.
  const airbnbSynchronizingRequests = partnerLodgments.map(
    ({ _id: lodgmentId, name: lodgmentName, airbnbUrl }) =>
      synchronizeWithAirbnb({ lodgmentId, lodgmentName, airbnbUrl })
  );
  await Promise.all(airbnbSynchronizingRequests);
}

3) 가공된 데이터를 HAU 데이터베이스에 알맞게 저장하기

백엔드에서는 넘겨 받은 가공된 데이터를 이용하여 숙소 스케줄 데이터에 반영한다.
데이터를 저장하는 로직은 서비스의 데이터 구조에 따라 천차만별일 테니 생략하겠다.

const synchronizeWithAirbnb = async (airbnbReservations) => {
 await updateLodgmentScheduleWithAirbnbReservations(airbnbReservations);
}

4) 위의 과정을 일정 시간마다 자동 반복 수행되게 만들기

마지막으로 새로운 변동 사항이 지속적으로 동기화될 수 있도록 "2 ~ 3"의 과정을 일정 시간마다 반복한다.
이때는 scheduler를 이용하면 손쉽게 구현할 수 있다.
지정된 시간이 될 때마다 작업이 실행되기를 원해서 node-scheduler이라는 모듈을 사용했다.
cron 표현식에 따라 시간을 설정하면 지정된 시간에 작업이 실행되도록 명령할 수 있다.
이를 테면 "매 시간의 30분이 되면 실행하라", "매일 2시 20분에 실행하라" 등의 명령을 할 수 있다.
나는 매 정각에 실행되도록 설정했다.

import schedule from "node-schedule";

const synchronizeHAUWithAirbnb = async () => {
  // 생략
}

const startScheduling = () => schedule.scheduleJob("0 * * * *", synchronizeHAUWithAirbnb)

이로써 에어비앤비 -> HAU 캘린더 연동 기능 구현이 끝났다.

2. HAU -> 에어비앤비 캘린더 연동

다음으로, HAU의 예약 내역을 에어비앤비 캘린더에 연동하기 위해서 아래와 같은 구현 과정을 거쳤다.

1) iCalendar 형식으로 예약 내역을 내보낼 수 있는 api 생성
2) 에이비앤비 캘린더에 api 주소 등록

에이비앤비 캘린더에 "달력 가져오기" 기능을 이용하면 주기적으로(2시간 간격) 예약 내역 변동 사항을 반영할 수 있다.
우리가 할 일은 iCalendar 형식으로 예약 내역 데이터를 내보내는 일뿐이다.

1) iCalendar 형식으로 예약 내역을 내보낼 수 있는 api 생성

ical-generator를 이용하여 해당 숙소의 예약 내역을 iCalendar로 변환하여 반환하는 api를 만들었다.

import LodgmentReservation from '@model/lodgmentReservation';
import ical from 'ical-generator';

const getLodgmentICal = async lodgmentId => {
  const calendar = ical({
    name: 'Hau Reservation Calendar',
    scale: 'GREGORIAN',
    prodId: { company: 'HAU', product: 'Reservation Calendar' },
  });
  
  const reservations = await LodgmentReservation.find({ lodgment: lodgmentId });
  reservations.forEach(reservation => {
  const event = {
      start: reservation.start, 
      end: reservation.end,
      productId: 'hau-reservation',
      uid: reservation._id.toString(),
      description: 'HAU 예약 내역 정보 연동',
      summary: `[HAU 캘린더] ${reservation.username}${reservation.start} ~ ${reservation.end}`,
    };
    
     calendar.createEvent(event);
  });
  
  return calendar;
}

2) 에이비앤비 캘린더에 api 주소 등록

에어비앤비에서 호스트 모드로 전환한 뒤, 원하는 숙소를 선택하고 "요금 및 예약 가능일 조정"을 선택해 들어간다.
설정 페이지에서 아래로 스크롤하다 보면 달력 가져오기라는 항목이 있다.
달력 주소 칸에 api의 주소를 입력하고 달력 이름을 지정한 뒤 달력 가져오기를 누르면 된다.

이로써 HAU -> 에어비앤비 캘린더 연동 기능 구현이 끝났다.

👏🏻 HAU와 에어비앤비 간 양방향 캘린더 연동 기능이 완성됐다!

소감

일을 하다보면 그리고 고객의 소리를 듣다 보면, 해야 할 것 같은 일들이 정말 많아 보인다.
이것도 중요해 보이고 저것도 중요해 보인다.
초기 스타트업에게 중요한 스탯은 빠른 속도라지만, 그보다 우선한 것이 문제를 현명하게 정의하고 우선순위를 잘 정렬하는 일인 듯하다.

해야만 하는 일과 하지 않아도 되는 일을 구분할 줄 알아야 한다.
액션은 막연한 기대감 혹은 불안감보다는 명확한 문제 의식에서 시작되어야 한다.

사실 아직까지도 문제를 충분히 영리하게 정의하며 일하지 못하고 있다는 생각이 든다.
앞으로는 지표와 데이터를 기반으로 문제를 뾰족하게 정의하여 가시적인 성과를 내고 싶은 바람이 크다 🌟

profile
개발을 위한 개발보다는 필요한 개발을!

2개의 댓글

comment-user-thumbnail
2023년 3월 1일

문제를 잘 정의하고 이를 해결하는 과정이 고스란히 담겨있네요! iCalendar 개념도 알게 돼서 정말 도움이 많이 됐습니다.

1개의 답글