에브리타임 시간표를 원타임으로 🚚💨

Sangho Han·2025년 3월 17일
40

⏰ OneTime

목록 보기
3/5
post-thumbnail

⏰ OneTime?

원타임에 대해서 궁금하다면 아래를 참고해주세요!

⏰ OneTime 서비스 바로가기
📝 OneTime 소개글
🧑🏻‍💻 GitHub
📸 Instagram


🎬 서론

얼마 전, 고대하던 원타임의 신기능이 배포되었다!

신기능 소개 👇🏻

🐮 에브리타임 시간표 연동 기능 소개

📸 에브리타임 연동 기능 인스타그램 게시물

또한 3월 31일까지 에브리타임 시간표 연동 기능을 활용하고,
인스타그램 스토리에 올리기만 하면 추첨을 통해 배달의 민족 2만원 상품권도 제공하는 이벤트를 진행중이다!! 👇🏻👇🏻
🎁 이벤트 인스타그램 게시물

이번 글에서는 위 기능을 만들게 된 배경과 구현 과정에 대해서 자세히 다루어보려고 한다.


🧐 '왜' 만들었을까?

원타임에는 내 스케줄 이라는 기능이 존재한다. (* 로그인 유저 전용)

왼쪽과 같이 본인의 고정적인 스케줄, 즉 안 되는 시간을 미리 등록해두면,
오른쪽처럼 해당 시간을 제외하고 모두 되는 시간으로 불러와 진다.

로그인 한 유저에게 더욱 편리함을 제공하기 위해서 위 기능을 만들었지만...
정작 사용자는 우리 팀 밖에 없다는 슬픈 사실을 알게 되었다 😢

막상 써보면 정말 유용한 기능인 것을 우리는 알기 때문에 어떻게든 해당 기능을 살려 보고 싶었다.
그러던 어느 날 에브리타임 시간표 를 추출해서 가져와보자는 의견이 나오게 되었다.

팀원 중 한 명이 학교 팀플에서 관련한 내용을 발표한 것을 보게 되었고, 이를 원타임에 적용해 보면 좋겠다는 생각이 들었다고 한다.

아무래도 원타임의 주요 타겟이 대학생이기도 하고, 당시에는 1월이었기에 3월 개강 시즌에 맞춰 오픈하면 좋은 홍보 수단이 될 것 같다는 생각이 들어 나 또한 좋다고 말을 했다.

백엔드를 맡고 있는 내가 핵심 로직(추출 및 변환)을 구현해야만 했는데, 당시에는 시간이 꽤나 많이 남아있었기에 막연하게 되지 않을까? 싶었던 것 같다. (이 생각은 구현을 하며 처참히 부서졌다 🫠)

또한 어려워보이기는 했지만 개인적으로 도전해서 성공해 보고 싶은 마음이 들었다. 왜냐하면 현재 발생한 문제(내 스케줄 기능 접근성 부족)를 기술을 활용해서 해결하고, 이로 인해 비즈니스 가치를 더욱 늘릴 수 있는 좋은 기회였기 때문이다.

이는 내가 평소에 중요하게 여기는 가치와 일맥상통했기에, 꼭 구현해내겠다는 마음가짐으로 개발에 돌입했다.


🕵🏻‍♂️ 구현 방식에 대한 고민

OCR에 도전해보다

초기 구현 목표는, 시간표 이미지를 입력 받은 후 시간대를 추출하여 스케줄로 등록하는 것이었다.
때문에 OCR 기능의 도입이 필요했고 여러 방식들을 알아 보았다.

가. Python 라이브러리

처음에는 파이썬의 Tesseract OCR 라이브러리를 활용하여 구현에 도전하였다.
어찌저찌 한글로 변환에 성공하고 추출이 되기는 하였으나...
이미지에 있는 글자가 너무 작아서인지 정확히 추출이 되지 않았다.

또한 사람들마다 똑같은 형태의 이미지를 업로드한다는 보장이 없었기 때문에,
어디부터 어디까지를 몇 시로 정해야하는지도 결정하기가 어려웠다.

며칠을 붙잡으며 계속해서 정확도를 올리려고 노력해보았지만.. 결국 어렵겠다라는 것을 인정하고 OCR 라이브러리를 이용한 직접 구현은 포기하였다. 그 대신 조금 비용을 내더라도 상용 서비스를 활용해볼까 하여 네이버 클로바 OCR로 눈을 돌리게 되었다.

나. 네이버 클로바 OCR

그렇게 대기업의 힘을 빌려 해결해보고자 했지만!
이 또한 이미지의 크기와 형태가 제각각이라는 점 때문에 포기하게 되었다. 네이버 OCR은 틀을 미리 정해두고 이미지에서 정보를 뽑아내는 것에 특화되어 있기 때문이었다.

결론적으로 아래의 문제들로 인해서 OCR로 구현은 포기하게 되었다.

1. 사용자마다 이미지가 너무 제각각이고, 정확히 어떤 이미지를 업로드할 지 모름

  • 시간표 이미지는 사용자별로 구성이 다르고 일정한 패턴이 존재하지 않음.
  • 예를 들어, 사용자가 캡처한 이미지마다 해상도, 회전 여부, 폰트 스타일 등이 달라서 일반적인 OCR 모델로는 인식이 어려움.

2. OCR의 성능이 좋지 않음

  • 라이브러리를 쓰자니 인식 성공률이 너무 낮음
  • NCP를 쓰자니 비용 문제 + 이미지가 고정이 아니라 지정 자체가 불가함

3. NCP(네이버 클라우드 플랫폼)의 OCR API를 활용할 경우 비용 문제가 발생함

🧑🏻‍💻 이 때는 정말 포기해야하나.. 싶었다. 왜냐하면 싸피를 하면서 남는 시간에 개발을 해야하기 때문에, 온전히 시간을 쓸 시간이 많이 부족했기 때문이다.
그렇게 좌절하던 찰나... 누군가 해주었던 말이 떠올랐다.
링크를 넣어서 자동으로 추출까지 해주면 편하겠다.

정확한 말은 기억이 안나지만 이끌리듯이 에브리타임 시간표 링크로 접속해보았다. 그리고 해결방법을 찾을 수 있었다.

웹 크롤링 방식

에브리타임에는 시간표 이미지 저장 외에도, 위와 같은 시간표 URL 공유 기능이 존재한다.

공유 버튼을 누른 후 복사된 시간표 URL로 접속하게 되면, 위와 같은 웹 화면이 나오게 된다.

이를 보자마자 웹 크롤링을 이용해야겠다는 생각이 들었다. 이유는 아래와 같았다.

  1. 웹이기 때문에 HTML 구조를 파악해서 웹 크롤링을 실행할 수 있다.
  2. 시간대가 기본적으로 오전 9시 ~ 오후 11시로 고정적으로 구성되어 있다.
  3. 요일과 시간대의 픽셀 크기가 동일하다. (요일은 변동이 있다. 아래에서 추가 설명이 이어진다.)

🧱 설계

HTML 분석 및 공식 도출

지금부터는 HTML을 분석하며 공식을 도출해낸 설계 과정을 설명해보고자 한다.
노션에 정리한 내용을 기반으로 작성하기에 두서가 조금 없을 수 있다 😅

가. 시간표 세로 px 고정

오전 9시부터 시작하는 항목의 top이 450px 이다. => 즉, 시간표는 450px 부터 시작한다.

또한, 각 칸 (=1시간)은 50px 를 차지한다.
=> 즉, 450 + 50 * 15 (=09 ~ 24시까지)
=> 1200px 가 시간표의 끝이 된다.

나. 시간표 가로 px 특정 가능

각 요일의 폭이 어느정도 일정하다.

월 ~ 금 일 때는 94 px 이다.

월 ~ 토 일 때는 79 px 이다.

월 ~ 일 일 때는 67 px 이다.

이 외의 경우는 없기 때문에, 3가지로 나누어 요일을 특정할 수 있다!

다. 시간표 각 항목 세로 px 고정

1시간 15분은 64px 이다.

1시간 25분은 72px 이다.

1시간 30분은 76px 이다.

💡 에브리타임은 5분 단위로 시간이 설정 가능하고, 30분에 26px, 1시간에 51px, 1시간 30분에 76px 를 가지는 것으로 파악된다. 하지만 5분 단위로 세세하게 나눌 수록 조금의 차이가 있다.

⇒ 원타임에서는 30분 단위로 타임블럭이 존재하기 때문에, 자잘한 픽셀은 버리고 계산이 가능하다!

☝🏻 30분에 26px, 1시간에 51px, 1시간 30분에 76px 이 고정이라는 점을 활용한다.

  • Math.ceil((height - 1) / 25)
  • 위 공식에 1시간 15분인 65을 대입 → 64 / 25 = 2.xx → 올림하면 3 → 3은 1시간 반을 의미한다.
  • 올림한 수를 2로 나눈 후, 나머지가 있다면 30분을 추가한다.

🎉 위 테스트2 항목을 기준으로 본다면 top이 600px 이므로, 시작 시간이 12:00 인 것을 알 수 있다.
여기서 위 계산을 통해 1시간 반이 나왔으므로, 종료 시간은 13:30 으로 계산 가능하다!

🚫 하지만 만약에 시작 시간이 xx:00으로 고정이 아니라면?

  • 유저가 시작 시작을 13:15, 16:05 와 같이 설정할 가능성이 존재한다.

  • 시작 시간이 13:15인 경우, 612px

  • 시작 시간이 13:30인 경우, 625px

💡 이런 경우에는 top 픽셀을 25단위로 구분한다!

  1. 우선 450px(09:00)를 기준으로 해서 그 차이를 50px로 나눈다.
  2. 612px(13:15)라면 (612-450) % 50 = 12가 나오게 된다.
  3. 여기서 만약 25보다 그 값이 작다면, 내림(버림)을 해서 600px(13:00)로 맞춰준다.
    1) 원타임 타임블럭은 30분 단위이기에 이를 맞출 수가 없다.
    2) 안 되는 시간 을 위한 기능이기에, 오히려 앞 뒤를 넉넉하게 잡는 편이 낫다.
  4. 하지만 만약 625px(13:30)라면, 25px 이상이 남기 때문에 13:30으로 설정한다.

전체적인 동작 및 크롤링 순서

위와 같이 구조를 파악하고 공식을 수립한 후에, 유저가 기능을 사용한다고 가정하고 설계를 진행했다.
이는 설계 단계에서 예측하며 작성한 것이기에, 실제 코드의 로직 및 자료구조와 다를 수 있다!

가. 유저 에브리타임 URL 링크 입력

  • 링크는 https://everytime.kr/@de9YHaTAnl47JtxH0muz 와 같은 형태이다.
    • everytime.kr/@ 로 시작하는 것이 고정이기에, 그렇지 않은 링크에 대해서는 예외 처리를 하며 최대한 오류를 줄인다.

나. 링크에 접속하여 파싱 진행

1) 요일 정보 구하기

  • 월 ~ 금 일 때는 94 px
  • 월 ~ 토 일 때는 79 px
  • 월 ~ 일 일 때는 67 px

=> 해당 정보를 통해서 요일이 어떻게 구성되어 있는지를 우선 파악한다.

  • width를 통해서 파악이 가능하다.

2) 요일 고정 후, 각 항목 시간대 파악

  • 이제 월 화 와 같은 요일을 고정시킨 후에, 아래로 탐색을 시작한다.

  • td 태그 하나하나가 각 시간표의 항목이다.
  • 이 안에 있는 heighttop을 통해서 시작 시간종료 시간을 계산할 수 있다.

3) 요일 별 계산 후 저장

  • 월요일에 대해서 파악이 끝났다면, Map 자료구조에 저장한다. (Python 사용하므로 Dictionary)
    • 키는 요일 (월, 수..), 값은 시작 시간과 종료 시간이다.
    • {“월”, {[10:00, 11:30], …}} 과 같은 구조로 예상된다.

4) 데이터 정제

{
  "code": "200",
  "message": "유저 고정 스케줄 조회에 성공했습니다.",
  "payload": {
    "schedules": [
      {
        "time_point": "월",
        "times": [
          "08:00",
          "08:30",
          "09:00"
        ]
      },
      {
        "time_point": "화",
        "times": [
          "10:00",
          "10:30",
          "11:00"
        ]
      },
      {
        "time_point": "수",
        "times": [
          "14:00",
          "14:30",
          "15:00"
        ]
      },
      {
        "time_point": "목",
        "times": [
          "16:00",
          "16:30",
          "17:00"
        ]
      },
      {
        "time_point": "금",
        "times": [
          "18:00",
          "18:30",
          "19:00"
        ]
      },
      {
        "time_point": "토",
        "times": [
          "20:00",
          "20:30",
          "21:00"
        ]
      },
      {
        "time_point": "일",
        "times": [
          "22:00",
          "22:30",
          "23:00"
        ]
      }
    ]
  },
  "is_success": true
}
  • 위와 같은 구조로 데이터를 보내주어야 한다. (기존 내 스케줄 등록 요청 데이터 형태)
  • 종료 시간이 없고, 30분을 기준으로 나누어 시작 시간만 보내는 구조이다.
    • 하지만 현재는 시작 시간 & 종료 시간 모두 있는 상태
    • 그러므로 만약 [10:00, 11:30], [14:00, 15:00]가 저장되어 있는 상태라면, 아래와 같이 변환해서 보내주어야 한다.
{
        "time_point": "월",
        "times": [
          "10:00",
          "10:30",
          "11:00",
          "14:00",
          "14:30"
        ]
}

5) 프론트엔드에서 처리

  • 기존 내 스케줄 등록과 동일한 구조로 보내주기 때문에, 이후로는 프론트엔드 단에서 처리 가능하다!

🚀 구현

구현은 방식 고민 & 설계에 비해서는 그리 오래걸리지 않았다.

다행히 설계가 잘 되어서, GPT와 함께 여러 번 코드를 짜고 테스트를 해보니 결과가 잘 나왔다!
결과물은 아래에서 공개하고, 여기서는 사용한 기술 스택과 아키텍처에 대해 이야기해보려 한다.

사용한 기술 스택

✅ Python

  • 크롤링 로직을 쉽게 구현할 수 있으며, 다양한 웹 크롤링 라이브러리들을 지원하기에 선택하였다.

✅ Selenium

  • 브라우저 렌더링이 필요한 페이지에서 동적으로 데이터를 가져올 수 있기에 선택하였다.

✅ Flask

  • 가볍고 빠르게 REST API 서버를 구성할 수 있고, 사용해 본 경험이 있기에 선택하였다.

✅ AWS EC2

  • 기존에도 AWS 플랫폼을 사용중이기에, Flask API 배포를 위해 사용하였다.

✅ Docker

  • 환경 의존성을 줄이고, 배포 과정을 단순화하기 위해 컨테이너화하였다.

✅ Caddy

  • HTTPS 자동 적용 및 Let’s Encrypt SSL 인증서를 손쉽게 관리하기 위해 사용하였다. (도메인 구매 없이 HTTPS 적용 가능)

위와 같은 기술 스택들을 사용하여 구현하였다.
특히 Caddy는 이번에 처음 사용해 보았는데, 도메인을 구입하지 않고 HTTPS를 붙일 수 있어서 편리했다!
참고한 블로그 글

크롤링 서버 깃허브 레포지토리는 아래 링크를 통해 확인해볼 수 있다.
Python과 Flask에 대한 이해도가 높은 상태가 아니기 때문에, 코드 상태는 조악할 수 있다 😅

🌱 GitHub

서비스 아키텍처

웹 크롤링 서버까지 구축한 후의 최종적인 OneTime 서비스 아키텍처는 아래와 같다.

전반적인 아키텍처 설명은 추후 AWS Code Deploy 도입기에서 작성하는 것으로 하고..
크롤링 서버만 보자면 EC2 내부에 플라스크 웹 애플리케이션 서버를 띄워서 클라이언트와 직접 API 통신할 수 있도록 구현하였다.

환경을 동일하게 유지하고 편리하게 배포하며, 경량화를 위해서 도커를 활용하였다.
코드에 대한 변경이 잦지 않을 것 같아서 아직 GitHub Actions 스크립트까지 작성하지는 않았는데, 생각보다 잦아서 조만간 해야할 것 같다..!


🌈 성과

결과물

좌측은 에브리타임 시간표, 우측은 URL 입력 후 자동으로 추출된 내 스케줄이다.
동일하게 추출되어 들어간 모습을 볼 수 있다.

아래의 영상을 보면 기능의 동작 과정에 대해 더 잘 이해할 수 있을 것이다!

거의 일주일 이상을 붙잡았던 기능 구현이었는데, 결과가 잘 나와서 매우 만족스러운 과정이었다!
테스트를 해보았을 때는 추출에 오차가 없이 설계한 대로 잘 동작했다 ✌🏻

해당 기능을 사용해서 몇 명의 유저라도 편리함을 느끼기를 바라며 배포를 진행했다.

배포 이후는?

그래서 배포 이후에 OneTime은 어떻게 되었을까?

누적 사용자 6,100+명 / MAU 2,100+명

25년 3월 17일 기준으로 누적 사용자 6,100+명 / MAU 2,100+명을 보유한 서비스가 되었다.

마케터 영입으로 인한 홍보 진행 + 신기능 업데이트 + 에브리타임 연동 기능 이벤트 + 개강 시즌 등.. 여러 요인이 있었다고 생각한다. 하지만 이 또한 서비스가 쓸만했기에 가능한 결과였다고 생각한다.

지난 24년 8월부터 모든 팀원들이 함께 기획부터 참여하고 사용성에 대해 끊임없이 고민한 결과가 이렇게 긍정적으로 나오는 것 같아 뿌듯한 마음이 든다. 백엔드 파트로서 앞으로도 계속해서 안정적인 서비스를 제공하고 고객의 불편함을 기술로 개선하기 위해 최선을 다할 것이다 🧑🏻‍💻🔥

내 스케줄 사용자 50명 이상 증가

또한 에브리타임 시간표 추출 기능을 활용하여 내 스케줄 을 등록한 유저가 50명 이상 늘어났다.
기존에는 거의 5명에만 그치던 것에 비하면 큰 수확이라고 볼 수 있다.

하지만 아직 더욱 많은 사람들이 사용해주었으면 하는 마음이 크다..!
1초만에 등록하고 나면, 이후로 스케줄을 등록할 때마다 최소 몇 십 초씩 절약이 가능하기 때문에 절대 손해가 아니라고 생각한다 😄 (효율에 미친 내가 보장한다.)

(다들 꼭꼭 이벤트도 참여하기를 바란다!)

🎁 이벤트 인스타그램 게시물
📸 에브리타임 연동 기능 인스타그램 게시물


🏁 마무리

이번 기능을 구현하며 오랜만에 새로운 것에 도전하는 즐거움을 느낀 것 같다.
물론 대부분이 고통스러운 시간이었지만..! 결과적으로 서비스가 성장하는 데 도움을 주었고, 한 명의 유저라도 OneTime을 더욱 편리하게 사용하는 데 이바지했다는 것에 힘든 것은 다 잊을 수 있었다.

고통스러운 시간에서 얻은 트러블 슈팅 내역도 다 기록해두었기에..^^
이어지는 다음 글에서는 에브리타임 연동 기능을 구현하며 마주친 에러들과 이를 해결한 방법, 그리고 부하테스트 및 성능 개선기에 대해 적어보려고 한다!

ps. 작성 완료하였다! 👉🏻 부하테스트 및 성능 개선기

👋🏻 모두들 OneTime 많이 사용해주세요!

profile
안녕하세요. 비즈니스를 이해하는 백엔드 개발자, 한상호입니다.

12개의 댓글

comment-user-thumbnail
2025년 3월 18일

아이디어 발상부터 구현까지 쌈@뽕하네요

1개의 답글
comment-user-thumbnail
2025년 3월 19일

최고에용

1개의 답글
comment-user-thumbnail
2025년 3월 20일

프로젝트 한거에서 멈추지않고 지속적 유지하는게 경쟁력인거같아요 ... (야구가 갔으면 나만의네컷 급으로 대박칠수있었는데 아쉬워요 ㅠㅠ)
원타임도 화이팅이에요

1개의 답글
comment-user-thumbnail
2025년 3월 20일

멋져요

1개의 답글
comment-user-thumbnail
2025년 3월 22일

멋진 청년

1개의 답글
comment-user-thumbnail
5일 전

진짜 멋있어요

1개의 답글

관련 채용 정보