Digital Garden by Rycont | 뉴스레터 구독하기

"비 내리는 호남선" 만든 이야기 (어이없음을 위한 엔지니어링)

비 내리는 호남선 서비스 스크린샷

서비스 바로가기: http://binary-honam.postica.app/

망막을 스치는 중성자처럼, 이상한 아이디어가 번뜩 떠오르곤 합니다. 개발과 전혀 관련 없는 일을 할 때도 예외는 없습니다. 제가 만들어서 대뜸 자랑하는 서비스들은 대부분 이렇게 시작합니다. 엄랭도, 연고로직도, 포타영역도 그랬어요. 이번에 만든 서비스인 "비 내리는 호남선" 또한 그랬습니다.

항해를 해서 목포에 갔는데, 언덕을 지나는 중 문득 머릿속에 노래 가사 한 소절이 떠올랐어요.

비 내리는 호남선~ 남행열차에~🎵
흔들리는 차창 너머로~🎵

이 다음에 "빗물이 흐르고~"가 나왔어야 했는데.. 무심코 이상한 생각을 해버렸습니다.

"...만들 수 있을 것 같은데?"

남행열차는 대한민국 국민이라면 누구나 알 정도로 대중적인 국민가요인데요, 소프트웨어 개발자에게는 이 노래가 더 특별합니다.

Binary는 호남선~ Num Row Column Char에~라는 글이 에브리타임 게시판에 업로드 되어있는 스크린샷

남행열차를 알고 있는 개발자라면, 위 밈 또한 알고있으리라고 확신합니다. 저 또한 "비 내리는 호남선"보다 "Binary는 호남선"이라는 표현이 훨씬 익숙합니다.

이전에도 스스로를 "개발 개그맨"이라고 칭하며 재밌는 결과물을 종종 만들어내곤 했지만, 재밌는 디지털 서비스를 제작하는건 처음이였습니다. 대중에게 재미를 선사함과 동시에, 정확한 정보를 읽기 좋게 제공해야 했습니다. 그저 재미로 시작해 추진력을 얻었지만, 그 기저에서는 시인성, 접근성, 반응성 등 좋은 사용자 경험을 제공하기 위해 노력했어요. 유머와 사용성이 균형을 넘어 시너지를 내는 것이 이 프로젝트의 가장 큰 과제였습니다.

누가 비 내리는 호남선을 웹사이트로 만들 생각을 하겠어요, 그리고 그 누가 비 내리는 호남선 운행 정보를 알고 싶어 하겠어요, 공급의 여지도 수요의 가망도 전혀 없었지만 제가 감히 그 틈을 비집고 뭔가를 만들어야겠다는 생각을 했습니다.

오직 **"✅사이트를 봤을 때 어이 없다는 생각이 들게 하기"**가 단 하나의 목표였어요. 겉으로 봤을 때는 그저 어이 없다는 생각 밖에 들지 않지만, 단 1초의 "어이없음"을 위해 수없이 다듬고 깎았습니다.

UI 만들기

가장 먼저 서비스의 외형을 상상해보았습니다. 재밌는 서비스의 UI 디자인을 만드는건 처음이라서 조심스러웠습니다. 앞서 말했듯, 사용성과 재미을 모두 잡아야 했는데, 직접 UI를 그려보니 쉽지 않은 일이였습니다.

비 내리는 호남선 서비스의 UI 디자인 목업 스크린샷

Figma를 켠지 약 두 시간 만에 UI 디자인을 끝내버렸습니다. 이전에 인하우스 프로덕트 빌드용으로 만든 UI 라이브러리인 Shade UI를 재활용했습니다. 난해함을 충분히 담아서 디자인을 하고 싶었지만, 아직 그만한 실력이 되지 않아서 정갈하면서도 어이 없는 디자인으로 마무리 했습니다. 배경에 있는 노래 가사 띠지엔 애니메이션이 적용되어 있어서 텍스트가 흐르게 만들었습니다!

UI 디자인을 하다가 알아 낸 사실인데, 무궁화호에는 공식 로고가 없습니다. 호남선에 운행하는 다른 열차들(ITX, KTX, SRT)는 모두 로고가 있는데, 무궁화호만 없습니다. 기차 운행 일정 카드에 기차 로고 이미지를 보여줘야 하는데, 어떻게 해야 하나 고민하다가 결국 무궁화호에게 로고를 하나 만들어줬습니다. 기차역에 가면 있는 "일반열차 탑승구" 플랫폼 안내판에 있던 아이콘이 생각나서, 해당 아이콘과 "무궁화호" 텍스트를 나란히 둬서 만들기로 했습니다.

코레일 일반철도 아이콘이 있는 플랫폼 안내판

제 생각엔 일반열차 로고 정도면 무궁화호를 잘 나타낼 수 있을거라고 생각했습니다. 그래서 코레일 일반열차 로고를 찾아 열심히 인터넷을 뒤졌지만, 그 어디에서도 벡터 이미지는 커녕 디지털 이미지 조차 구할 수 없었습니다. 그래서 직접 그리기로 했습니다. 인천 공항철도 로고가 이와 유사했던게 생각나서, 공항철도 SVG를 베이스로 해서 변형했습니다.

인천 공항철도 아이콘, 저화질 일반철도 이미지, 직접 복원한 벡터 이미지가 나열되어있는 스크린샷

열심히 복원하였습니다.

호남선에서 운행하는 기차 브랜드, 왼쪽 위부터 아래로 KTX, KTX 산천, KTX 청룡, ITX 마음, ITX 새마을, 무궁화호, SRT

내가 만든 무궁화호 벡터 로고가 비내리는호남선 홈페이지에 들어가있는 사진

그렇게 수제 무궁화호 로고가 탄생했습니다. 글꼴은 코레일 전용서체를 사용하였습니다.


서비스를 공개한 이후에 연락을 받았는데, 저 일반열차 로고는 공공안내 그림표지에 수록되어 있었다고 합니다.. 이 사실을 알자마자 허망했지만, 만드는 동안 재밌었으니 그럼 됐습니다!

Rail+ 철도동호회 | 철도 관련 픽토그램 (공공안내 그림표지) 국가표준 - Daum 카페

공공안내 그림표지에 수록된 기차 관련 픽토그램

열차 운행 계획 가져오기

비 내리는 호남선의 운행 일정을 계산하려면 먼저 호남선의 열차 운행 일정을 알아내야 합니다. 다행히도 대한민국의 여객철도는 민영화되지 않아서, 코레일과 SRT만이 운행하고 있습니다. 그렇기에 두 회사의 여객운행 계획만 가져오면 열차 데이터는 준비가 끝납니다.

코레일 열차 운행 계획 가져오기

구글에 검색해보니, 코레일은 다음과 같이 열차 운행정보 API를 제공하고 있습니다.

한국철도공사_열차운행정보 | 공공데이터포털

[GET] /travelerTrainRunPlan: 여객열차 운행계획

여객열차의 운행계획을 알려주는 정보로 운행일자, 열차번호, 출발역코드, 출발역명, 도착역코드, 도착역명, 열차계획출발일시, 열차계획도착일시 등을 제공한다.

이 API를 사용해 향후 며칠간 열차 운행 정보를 받아오고, 출발역 혹은 도착역이 호남선의 역인 운행 계획만을 필터링 하면 되겠습니다.

그러나 이는 공공데이터포털 API 희망편이였습니다.. 코레일은 제게 그렇게 데이터를 쉽게 주지 않았습니다. 이 API의 각 필드는 모두 Optional이라서 날짜만 입력한다면 해당 날짜 구간의 운행계획을 반환하도록 되어 있는데요, 내일의 운행계획을 호출해보니

고장난 코레일 API 스크린샷, 데이터가 0개 반환되었다.

아무것도 반환되지 않았습니다.. 의아하여 주변 철덕에게 물어보니

정한: 철도 운행정보 API는 원래 이런가요
wm.south: 네 원래 그렇습니다

이미 지난 날의 운행 계획 정보는 잘 나왔습니다. 전혀 운행 계획이 아니잖아 이거, 완전히 틀렸잖아, 지난 계획이 어떤 의미가 있는지 저는 잘 모르겠습니다..


API를 사용하는건 아무래도 힘들 것 같으니, 코레일에서 제공하는 운행 일정 엑셀 파일을 참고해서 직접 데이터를 만들기로 했습니다.

열차운임 및 시간표 - letskorail.com

코레일 호남선 일반열차 시간표

먼저 일반열차입니다. 위와 같이 거대한 시간표가 있습니다. 운행 계통별로 각 운행 일정의 시발역/시간, 정차역/시간, 종착역/시간이 기재되어 있습니다. 이중 호남선 열차 운행 일정을 따오겠습니다. 엑셀에서 복사한 셀을 텍스트로 붙혀넣으면 행은 \n(줄바꿈), 열은 \t(탭)으로 치환됩니다. 그래서 시발역 부분을 텍스트로 옮기면 다음과 같습니다:

const departureRawText = `용산	부전	용산	용산
龍 山	釜 田	龍 山	龍 山
Yongsan	Bujeon	Yongsan	Yongsan
07:34	06:16	08:44	09:47
무궁화	무궁화	ITX-새마을	ITX-마음
1401	1951	1071	1161
... `

이러한 텍스트를 줄, 탭으로 자르고 각 열을 하나의 레코드로 변환하는 함수를 작성하였습니다.

const tableHeaders = [
    "departureStation",
    null,
    null,
    "departureTime",
    "trainName",
    "trainNumber",
]

const departures = parseVerticalTableTextWithHeaders(
    departureRawText,
    tableHeaders,
);
result: [{
    departureStation: "용산",
    departureTime: "07:34",
    trainName: "무궁화",
    trainNumber: "1401"
}, {
    departureStation: ...
}]

남행열차이기에 하행만 파싱합니다.

고속열차도 파싱해보겠습니다.

코레일 호남선 고속열차 시간표

일반열차 시간표 포맷과 다른 점이 몇 가지 있습니다:

  1. 한 편성이 열이 아닌 행으로 작성된다
  2. 시발역 종착역이 명시되어있지 않아 첫 정차역 / 마지막 정차역에서 유추해야 한다
  3. 일부 요일만 운행하는 편성이 있다

셀 파싱 함수는 행열을 바꿔서 그대로 사용하되, 후처리를 더 거쳤습니다.


일부 전라선 열차는 호남선을 지납니다. 그렇기에 다음에 해당하는 편성을 데이터에 추가하였습니다:

  1. 모든 전라선 고속열차
  2. 용산발 전라선 일반열차

그렇기에 비내리는호남선 사이트에 순수 호남선 계열 열차만 필터링하는 옵션을 구현 해두었습니다.


SRT 열차 운행 게획 가져오기

SRT는 운행 계획을 구조화된 데이터로 제공하지 않습니다. 그렇기에 SRT 홈페이지에 게시되어 있는 표준 시간표 PDF 파일을 보고 눈으로 옮겨적었습니다.

열차운임 및 시간표 < 승차권이용안내 < 이용안내 < 승차권 예약/발매 - etk.srail.kr

JSON으로 옮겨적은 SRT 시간표의 일부 스크린샷

SRT 열차 운행 시간표를 JSON으로 옮겨적기가 코레일 데이터 전처리기 구현보다 빨리 끝났습니다😂 원래 개발자는 30분 걸릴 일을 15초에 끝내기 위해 두 시간 고민하는 존재가 아닐까요? 저 또한 사회의 보편 가치를 따르는 개발자이기에 두 시간 고민하는 편을 더 좋아합니다.

비 오는 시간 알아내기

기차 운행 정보를 얻어왔으니, 이젠 비 오는 시간을 알아내야 합니다. 국내 기상 예보 제공 서비스는 여러 곳이 있고, 기상청에서 데이터를 얻어오겠습니다.

기상청에서 제공하는 "단기 예보" 서비스는 50시간까지의 기상 예보를 1시간 시격으로 제공합니다. 특이한 점은, 예보 데이터를 요청할 때 위경도가 아닌 기상청 자체 좌표계를 사용합니다. API 설명 페이지에서 행정구역 - 좌표 매칭 엑셀 파일을 참고해서 요청해야 합니다.

기상청의 행정구역 - 좌표 매칭 엑셀 파일

또한 API 응답이 덜 추상화되어 있습니다. 다음과 유사한 형식으로 응답합니다:

[{
    category: "강수 확률",
    value: "60%",
    time: "20240806 1700"
}, {
    category: "강수량",
    value: "0.1%",
    time: '20240806 1700"
},
...
]

예측 항목과 일시별로 레코드가 나누어져 있어서, 후처리를 충분히 거쳐야 합니다. 다음과 같이 추상화하였습니다.

Map(4) {
  '20240727 1300' => { probability: '60', precipitation: '8.0mm' },
  '20240727 1500' => { probability: '60', precipitation: '6.0mm' },
  '20240727 1700' => { probability: '60', precipitation: '3.0mm' },
  '20240727 1800' => { probability: '60', precipitation: '7.0mm' }
}

이렇게 향후 50시간의 강수 확률과 강수량을 받아왔습니다.

export const PRINCIPAL_STATION = {
    용산: {
        x: 60,
        y: 126,
    },
    광주송정: {
        x: 57,
        y: 74,
    },
    서대전: {
        x: 68,
        y: 100,
    },
    목포: {
        x: 50,
        y: 66,
    },
}

위와 같이 호남선의 주요 역을 거점으로 설정하여, 위 역중 한 곳에 비가 오는 시간대에 운행하는 호남선 열차를 "비 내리는 호남선"으로 정의하였습니다.

사실 이 정의가 썩 마음에 들진 않습니다 :( 호남선에 비가 내리고 있긴 하지만, 내가 탄 열차가 비를 맞을지는 알 수 없기 때문입니다. 그렇기에 추후 알고리즘 고도화를 거쳐 정말로 "비 맞는 호남선 열차"를 보여주도록 수정하고 싶습니다..

서비스 구현하기

어떤 형태로 구현할지 많이 고민하였습니다. 비 내리는 호남선은 이러한 특징이 있습니다:

위 사항을 고려한다면 빌드타임에 기상 정보를 받아와서 정적 사이트로 만드는게 좋아보입니다. 다음과 같은 배포 파이프라인을 사용합니다:

  1. Cloudflare Pages로 프론트엔드를 배포한다
  2. Astro로 HTML 페이지를 빌드한다
  3. Cloudflare Workers와 Cron Trigger로 12시간마다 페이지를 새로 빌드한다

Cron Trigger를 사용한 주기적인 빌드는 다음 아티클을 참고했습니다:

위 절차가 아닌 다른 방법도 고민을 했었으나

  1. Astro로 개발하고 Deno Deploy에 배포해서 실시간 SSR하기
    • 기상청 API가 응답에 오래걸리거나 실패할 때가 있어서 실시간 정보 제공이 어려움
  2. Cloudflare Pages + Worker Functions에 SSR로 배포하고 Worker KV에 페이지 캐싱하기
    • Pages와 Cron Trigger를 이용하는것 보다 나은 점이 없음 :(
  3. 간단하게 백엔드를 만들어서 JAM 스택으로 만들기
    • 제가.. 프론트엔드 자바스크립트를 쓰는걸 별로 좋아하지 않습니다. 필요하지 않다면 굳이 쓰지 말자는 주의라서.. 이번 프로젝트에서는 없앴습니다!

그리고 수 많은 여담

긱뉴스에 올리니 많은 사람들이 좋아해주셨습니다, 맞아요 제가 직접 올렸어요. 많은 업보트를 받아서 텔레그램 채널과 트위터에도 올라왔습니다. 제가 참여한 프로젝트를 종종 긱뉴스에 공유하곤 하는데, 채널에 공유가 됐던건 디미페이 다음으로 두번째입니다😌 연고로직은 아쉽게도 공유가 안됐었지말이죠 .. 굉장히 기쁘답니다~~


자바스크립트를 많이 쓰기 싫어서, 편성 상세 정보 필터링(기차 종류, 순수 호남선 계열 만 검색)을 CSS 클래스로 구현했습니다. 이쯤되면 객기일까요?


흐르는 가사 컴포넌트를 다시 구현하다 보니, HTML Marquee 태그를 재구현하고 있다는걸 깨달았습니다..


연결된 페이지 (Inlinks)


댓글 쓰기, GitHub에서 보기