vinyl.coroke.net 바이닐고로케 개발기

vinyl.coroke.net 바이닐고로케는 아티스트 별 LP 바이닐 음반을 둘러보고, 이를 판매하는 매장을 찾아주는 서비스입니다. 구상과 개발, 완성에는 느긋하게 10일 남짓이 걸렸습니다.

개발 과정

2024년 6월 28일 밤, 잠들려다가 느닷없이 ‘전국의 모든 LP 바이닐을 검색할 수 있는 서비스’를 만들어야겠다는 생각이 떠올랐습니다. 정말 느닷없이 떠오른 생각이었습니다.

도로 일어나 데스크탑을 켜고 주요 레코드샵의 온라인 스토어를 확인해보았습니다. 데이터를 수집하는건 어렵지 않아 보였고, 이 데이터를 모두 모아 클렌징하는 것이 가능한 지가 관건이었습니다. 주말동안 검색 봇을 돌렸고, 클렌징도 어렵지 않게 (물론 어렵습니다) 구현 가능하다고 판단했습니다.

서비스 기획은 어렵지 않았습니다. 흔한 가격비교 사이트처럼 상품 목록을 보여주면 된다고 판단했습니다. 물론 LP 바이닐 음악을 미리 들어보지 못하고 상품을 사야하는 것에 불평하는 이야기도 종종 있었기 때문에, 온라인에서만큼은 미리 대표곡을 들어볼 수 있다면 더 좋을거라 판단했습니다. 미리듣기 서비스는 Spotify가 Embed Code를 제공하기 때문에 이를 붙여보기로 했습니다.

LP 바이닐을 구입하는 사람의 페르소나를 정하는 것은 어렵지 않았습니다. 제가 LP 바이닐을 왕창 사고 있기 때문입니다. LP 바이닐을 뒤적거리며 우연히 설레이는 음반을 만나는 경우도 있지만, 저는 보통 좋아하는 아티스트의 음반이 있는지부터 먼저 뒤적거리는 편입니다. 그래서, 상품 목록은 음원 사이트가 그렇듯이 아티스트 별로 우선 추려야겠다는 생각을 했습니다. 대부분의 레코드샵 온라인 스토어는 아티스트 별 분류기능이 없습니다.

  • 7월 1일, 주요 레코드샵의 온라인 스토어에서 상품정보를 수집하는 검색 봇을 만듭니다.
  • 7월 2일, 각 레코드샵의 상품 등록 규칙에 대응하는 데이터 정제 로직을 만듭니다. 상품명에서 아티스트 이름과 앨범 제목을 분리하는 클렌징 작업입니다.
  • 7월 3일, 데이터를 둘러보며 DB 스키마를 짜고, API 서버를 만듭니다.
  • 7월 4일, 웹 사이트를 만듭니다. 디자인은 CSS로 그냥 짭니다. 이후 API 서버와 결합해 그럭저럭 돌아가는 서비스를 완성합니다.
  • 7월 5일, 완성된 서비스를 실제로 써보며 데이터를 검증합니다. 데이터 정제 과정을 보완해 다시 적재합니다.
  • 7월 6일, 잊고 있던 robots.txt 규칙을 뒤늦게 적용하고, 서비스 가능 스토어만 추려냅니다. 개발 후기를 쓰는게 좋겠냐는 글을 트위터에 올립니다. 여럿 분들이 글을 찾으셔서 글을 쓰기로 합니다.
  • 7월 7일, 만든 코드와 초기 데이터를 서버에 배포합니다.
  • 7월 8일, 개발 후기를 발행합니다.

기술 스택 구성

DB는 MySQL RDB를 쓰기로 했습니다. Sqlite3 만으로도 감당할 수 있는 데이터 규모이지만, 추후 확장성과 검색 기능 구현 등을 고려하여, 늘 쓰고 있던 MySQL을 골랐습니다.

DB에 데이터를 적재하는 코드는 Python으로 구현했습니다. 데이터를 다루기엔 역시 Python입니다. API를 제공하는 서버는 django 프레임워크로 구현했습니다. SaaS를 써볼까 했지만 언제나 비용은 제 코드로 제 통제 하에 있는 것을 선호하는 편입니다. 스프링을 쓸까 nestjs를 쓸까도 고민해보았지만, 취미 비슷하게 가동할 서비스라면 가장 익숙한 언어를 쓰는게 좋다고 판단했습니다.

웹 서버는 TypeScript에 SvelteKit 프레임워크로 구현했습니다. 역시 next.js를 쓸까 고민했지만, 단순히 데이터를 랜더링하는 서비스를 만드는데에는 어쩐지 Svelte가 괜찮았다는 것이 저의 경험적 인식입니다.

API는 django-ninja 를 활용해 설계했습니다. 전 회사에서는 Django DB Model을 TypeScript Interface로 변환하는 API endpoint를 만들어 API서버와 웹 서버간의 타입을 맞추고 REST API를 구현했었습니다. django-ninjapydantic과 결합해 API schema 설계도 쉽게 할 수 있고, FastAPI 처럼 openapi 문서도 자동으로 만들어줍니다. Typescript와 결합시킬 별도의 프로토콜을 만들지 않아도 괜찮습니다.

각 서버는 Amazon EC2 t2.small 인스턴스 하나에 도커로 말아 띄웁니다. 배포는 shell script로 합니다. 가장 신속하게 서비스를 만드는 데에는 이 스택이 저에게 가장 익숙하기도 합니다.

아키텍쳐 구성

vinyl.coroke.net 은 ‘수집 봇’ 과 ‘데이터 정제’, ‘데이터 가공’ 을 거쳐 음반, 앨범, 아티스트 정보를 적재합니다. 그리고 ‘API 서버’와 ‘웹 서버’가 있습니다.

상품을 모을 매장은 직접 사람의 손으로 등록했습니다. 레코드를 파는 매장을 찾는 것만큼은 사람의 몫이기 때문입니다. 등록과정은 django 가 제공하는 기본 admin 만으로도 충분히 할 수 있습니다.

매장의 웹사이트에서 상품 정보를 수집하는 ‘검색 봇’을 만들었습니다. 봇은 상품명과 상품 가격, 상품 이미지를 수집합니다.

수집한 상품 정보에서 앨범 제목과 아티스트 이름을 분리하는 ‘데이터 정제’ 로직을 만들었습니다. 흔히 ‘데이터 클렌징’이라고 부르는 로직입니다. 이 로직이 언제나 까다로운 과정이고, 단순 기술 뿐만 아니라 업종의 동향이나 관행까지도 충분히 이해해야 잘 만든 로직이라 할 수 있습니다. 대부분 정규표현식(Regular Expression)으로 분리해냅니다.

정제된 데이터를 기반으로 아티스트 사진과 유명세 등 부연 정보를 결합하는 ‘데이터 가공’ 로직을 만들었습니다. 데이터 가공에는 Spotify API가 활용되었습니다. Spotify는 기술 기반 회사 답게 API 문서가 정말 잘 만들어져 있고 훌륭한 데이터를 제공합니다. 심지어 미리듣기 Embed Code 까지 제공합니다.

웹 사이트는 media query 를 적용하여 반응형으로 구성했습니다. PC 데스크탑 웹을 먼저 만들면서 IA 를 구현하고 유저스토리 동선을 검증했습니다. 이후 브라우저를 짜부시켜 모바일 화면을 만들었습니다. 디자인은 음악 서비스의 트렌드에 맞게 다크모드를 기본으로 설계했습니다. 색상은 다크모드를 지원하는 여러 서비스를 참조해 가져온 뒤 서비스 톤앤매너에 맞게 조정했습니다.

웹 서버와 API 서버는 nginx proxy로 결합시켰습니다. 보통 api. 로 시작하는 분리된 호스트네임을 연결하는게 편하긴 하지만, 제가 쓰는 Cloudflare 무료플랜에선 api.vinyl.coroke.net 같은 3차 도메인까지 A 또는 CNAME을 설정할 수는 없어 불가피하게 proxy를 걸었습니다. SvelteKit 은 브라우저는 물론 서버 사이드 랜더링(SSR) 단계에서 API를 서버끼리 호출하기도 하기 때문에, nginx를 거치면서 API 보안을 설정할 수 있는 용이함도 있습니다.

개발 환경에서 nginx 없이 proxy를 구현해야할 수도 있습니다. 이건 SvelteKit이 채택하고 있는 vite가 기본 기능으로 제공하는 proxy로 해결할 수 있습니다. 제가 사용한 vite.config.ts 는 아래와 같습니다.

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()],
	server: {
		proxy: {
			'/api': {
				target: 'http://localhost:8000',
				changeOrigin: false,
				secure: false,
			},
		}
	}
});

개발의 주요 이슈들

음악 도메인

이른바 ‘데이터 정제’ 로직은 매장마다 하나씩 마련하였습니다. 각 매장 관리자의 상품명 입력 방식이 저마다 다르기 때문입니다. 구 인디스트릿을 만들었을 때도 비슷한 문제가 있었습니다. 각 공연장마다 공연 일정을 알리면서 트위터에 적는 문장의 구성이 천차만별이었습니다. ‘날짜 - 아티스트 이름’ 으로 쓰는 곳도 있고, ‘아티스트 이름 - 날짜’로 쓰는 곳도 있고, 캔버스 여기저기에 아티스트 이름을 넣은 이미지만 올리는 공연장도 있었습니다. 이 문제는 음반이라는 상품을 판매하는 커머스에서도 비슷하게 나타납니다.

음반은 음악을 저장한 (법률용어로는 ‘고정한’) 매체를 의미합니다. 음반 상품은 각 곡의 묶음으로 구성되는 앨범 제목 뿐만 아니라 앨범에 포함된 각 곡을 만들거나 노래를 한 이를 상품 이름에 함께 표시하기 마련입니다. 이때, 각 매장마다 상품 제목을 어떻게 구성할지의 문제가 발생합니다. “아티스트 이름 - 앨범 제목”으로 표시할 수도 있고, “앨범 제목 (아티스트 이름)” 으로 표시하기도 합니다. LP 바이닐의 경우 1960년대부터 국내에 해외 아티스트의 음반도 꽤 유통되었기 때문에, “한글이름 영문이름 - 앨범 제목” 을 한꺼번에 표시하는 경우가 대부분입니다. 여기서, 한글 이름과 영문 이름 사이에 구분자를 넣는 매장도 있고, 구분자 없이 늘여쓰는 매장도 있습니다. 구분자를 하이픈(-)으로 쓰는 매장도 있고, 슬래쉬(/)로 쓰는 매장도 있습니다. 같은 하이픈(-) 도 반각(-) 하이픈과 유니코드 전각(–) 하이픈이 따로 있고 둘 다 섞어쓰는 곳도 있습니다. 이 모든 경우에 하나하나 대응하고 향후 유지보수를 용이하게 하는 것까지 고려하여, 공통 로직과 스토어 별 로직을 구분한 2단계의 ‘데이터 정제’ 로직를 만들었습니다.

가장 구현하기 어려운 경우는 구분자 없이 한글이름과 영어이름 앨범제목을 섞어 쓰는 경우입니다. 아예 순서 규칙이 없기도 합니다. 이 경우에는 문자열을 스페이스로 쪼갠 뒤 하나씩 조립해가며 아티스트 이름으로 탐지되는 문자열이 맞는지 음원사이트 API를 통해 확인하고, 그 결과 중에서 가장 가능성 높은 아티스트를 선별한 뒤, 나머지 글자들을 앨범 제목으로 분류하여 해결합니다. LLM 모델을 쓸까 하다가 학습 비용이 클 것 같아 일단 제쳐두었는데, 다른 도메인에선 LLM 모델을 쓰는 것이 더 저렴할 수도 있어보입니다.

아티스트 이름이 없는 음반에 아티스트가 탐지되는 경우도 있습니다. 특히 OST의 경우 류이치 사카모토 등 OST로 유명한 아티스트가 자동 매칭되는 경우가 많습니다. 이 경우는 작곡가와 달리 실제 연주자가 더 다양하기 때문에 누구의 앨범이라고 분류하기가 까다롭습니다. 그래서 OST 음반은 과감히 스펙아웃하기로 했습니다.

음악 도메인에서 가장 골칫거리인 아티스트는 ‘베리어스 아티스트(Various Artists)’입니다. 베리어스 아티스트는 실존하는 아티스트가 아니라 ‘여러 아티스트들’을 의미하는 문구입니다. 한 앨범에 참여한 아티스트가 여러명인 컴필레이션 앨범 등에서 아티스트명을 Various Artists (또는 Various Artist) 로 표기하는 경우가 흔합니다. 베리어스 아티스트를 다른 아티스트와 동일하게 취급할 경우 이 아티스트가 음반 발매량 세계 1위를 차지하는 최고의 아티스트로 분류될 가능성이 높습니다. 전세계에서 정말 많은 ‘베리어스 아티스트’의 음반이 나오고 있으니까요. 그래서, 음원 스트리밍 서비스에서는 DB가 터지거나, 연결관계가 과부하되지 않도록 베리어스 아티스트만 따로 처리하는 경우가 많습니다. vinyl.coroke.net은 베리어스 아티스트(Various Artists)로 음반을 찾는 니즈는 없을거라 가정하고서, 이들 음반 역시 스펙아웃하기로 결정했습니다.

Various Artists

Various Artists 조심

모두 음악 도메인에 대한 이해가 없다면 고려하기 어려웠을 이슈들입니다. 다행히도 전 직장에서 (오픈은 못했지만) 음원 스트리밍 서비스를 만들어보았던 터라, 경험을 되살려 예상되는 이슈들을 어렵지 않게 선제적으로 대응할 수 있습니다.

커머스 도메인

vinyl.coroke.net은 방문자가 음반 제품 구매까지 닿도록 안내하는 서비스이기 때문에, 음악을 넘어 커머스 도메인까지 고려한 설계를 담아야 했습니다.

vinyl.coroke.net은 관심있는 음반을 구매하기 위해 온라인 쇼핑몰을 이용할 수도 있지만, 직접 매장에 찾아가보는 것도 권유하는 것을 목적으로 만들어졌습니다. LP 바이닐은 물론 턴테이블과 카페를 갖춘 매장들도 많이 있고, 매장 주변의 소소한 소품점이 가득한 거리를 둘러보는 즐거움도 있기 때문입니다. 이를 위해 각 매장의 주소는 물론 위도, 경도도 함께 DB에 저장하였고, 개별 상품을 클릭/터치했을 때는 온라인 쇼핑몰 링크를 제공하기 앞서 매장 위치가 먼저 나오도록 설계했습니다. 홍대에서는 이 골목으로, 해방촌에서는 저 골목으로 들어가면 되겠거니 짐작할 수 있습니다.

매장의 위치를 표시할 때, 단순히 주소만 표기해서는 실제 위치가 잘 인식되지 않는 경향이 있습니다. 예를 들어 서울 서교동에 위치한 매장은 서울 마포구로 표기하기보다는 서울 홍대로 표기하는 것이 문화콘텐츠를 향유하는 층에게 더 익숙할 것입니다. 이를 위해, 주소를 담는 address 컬럼 외에 별도로 area 컬럼을 추가로 정의했습니다. 해방촌과 경리단길이 위치한 용산구는 용산과 해방촌, 경리단길을 분리하여 표시합니다. 광흥창역 아래는 과거 불리우던대로 마포로 표기하지만, 홍대 근처는 홍대로 표기합니다. 이렇게 분류하면 정식 주소보다도 어느 상권인지 더 효과적으로 인식할 수 있습니다.

품절상품은 표시목록에서 제외시켰습니다. 각 온라인 스토어 웹사이트에서 상품을 둘러본다면 목록화면에서 품절 여부를 바로 인식할 수 있지만, 여러 매장의 상품을 모아 목록으로 표시하는 vinyl.coroke.net 에서는 품절된 상품까지 섞어 표시했다간 방문자들에게 불필요한 기대와 뒤이은 실망을 안길 것입니다. 각 매장별로 품절 상품을 온라인 스토어에서 바로 내리는 경우도 있지만, 목록에는 남겨두되 품절이라고 써두는 경우도 많습니다. 그래서, 각 웹사이트에서 어떤 형태로든 상품의 품절여부가 확인되면 vinyl.coroke.net의 목록에서 바로 제거토록 했습니다.

언젠가는 외국 직구가 가능한 매장을 추가할거라 생각했습니다. 여행에서 돌아오는 길에 현지의 LP 바이닐을 사오는 분들은 1980년대에도 가끔 있었지만 요즘은 훨씬 많은 추세입니다. 저 역시 런던이나 뉴욕, 도쿄에서 LP 바이닐을 여럿 사오곤 했습니다. 해외 직구 매장이 추가되는 것도 염두하여, 거래 화폐를 의미하는 currency 컬럼을 따로 넣어두었습니다. 한국 매장들의 currency는 KRW입니다. 언젠가 일본 도쿄의 레코드점을 소개한다면 JPY가 추가될 것입니다. 판매가격 역시 원화 외의 화폐를 지원하기 위해 integer 대신 decimal 로 정의하였습니다. 한국은 30,000원 50,000원 처럼 정수로 떨어지는 가격을 사용하지만, 달러나 유로는 센트처럼 가격에 소수점을 포함되기 때문입니다. $49.99 등의 가격을 표기하려면 가격에 소수점을 포함해야 합니다.

플랫폼에서 상품을 소개한 뒤 타 스토어로 리다이렉트 시키는 메타 서비스들은 상품과 판매/구매 과정에 관한 분쟁에 있어 책임 문제가 복잡해지기 마련입니다. 오픈마켓부터 배달대행까지 대다수 플랫폼들이 언제나 마주하는 문제입니다. 메타 서비스들은 상품 정보를 모아 표시하고 접근 경로를 제공할 뿐 (모서리찍힘부터 배송 실패까지) 개별 상품의 문제까지 처리할 수는 없습니다. 각 스토어를 보증하지도 않습니다. 하지만 메타 서비스에다가 ‘대신 문제를 해결해달라’고 부탁하는 CS는 언제나 들어오기 마련입니다. 그래서 네이버 쇼핑이나 다나와 같은 메타 서비스들은 리다이렉트 직전 상품의 품질이나 배송과 관련된 책임이 각 스토어에 있고, 관련 문의 역시 각 스토어에다 문의해야함을 항상 고지합니다. 이런 배경을 고려하여 vinyl.coroke.net 도 리다이렉트 전 책임 소재에 대한 고지 페이지를 제공토록 했습니다.

데이터를 확인하던 도중, 오프라인 매장이 없는 온라인 스토어가 유난히 상품이 많다는 사실을 알게 되었습니다. 그런데 상품 설명을 자세히 보니 어딘가에서 복사 붙여넣기를 한 느낌이 들었습니다. 역시나 Amazon 또는 Discorg 등의 상품 소개를 고스란히 가져온 것들이었습니다. 구매대행 사이트가 꽤 많은 셈입니다. 구매대행 사이트 상품을 오프라인 매장 상품과 섞었다간 양적으로 구매대행 사이트가 목록을 잠식할 게 뻔했습니다. 밑도끝도 없이 어마어마한 데이터가 밀려와 DB에도 부담이 될 것이 분명했습니다. 그래서, 구매 대행이 분명해보이는 온라인 스토어는 수집 대상에서 제외하기로 했습니다.

정식 쇼핑몰 서비스의 경우 더 많은 일들이 벌어집니다. ‘배송’, ‘반품’, ‘환불’, ‘교환’ 도 구현해야하고 ‘결제’도 구현해야합니다. 사이드 프로젝트에서 이런 문제까지 다룰 정도라면 회사를 차리는 것이 더 나을 것입니다. 다행히(?) 상품과 매장을 소개만 하는 서비스이기때문에 여기까지 이르지는 않았습니다.

기술적 난제

robots.txt

서비스를 거의 다 만든 뒤, 주말에 밖에 나갔다 돌아오는 길에 문득 robots.txt 가 떠올랐습니다. 크롤링이 수반된 검색 봇을 만들었다면 robots.txt의 정책을 따라야 할텐데, 데이터를 검증하는데에만 몰입해 이를 까먹었던 것입니다. 뒤늦게 검색 봇이 robots.txt를 참조토록 하고, 각 매장 웹사이트의 robots.txt 를 확인하였습니다. 역시나 ‘네이버 스마트스토어’의 경우 외부의 모든 검색 봇 접근과 데이터 수집을 거부하고 있었습니다. 아쉽지만, 네이버 스마트스토어를 쓰는 매장은 제공 목록에서 제외하였습니다. 그래도 다른 매장의 물량이 많아 서비스 오픈에는 무리가 없었습니다.

이미지 lazy loading

이미지 트래픽을 걱정해야 하는 시대는 지났지만 그래도 이미지가 다량으로 로딩되는 것은 데이터요금을 쓰는 분들에겐 부담되는 일입니다. 이미지 Lazy Loading을 적용하면 스크롤 하지 않는 사용자의 트래픽도 일정 부분 줄어들 수 있고 document의 랜더링도 좀 더 빨라집니다. 불과 10년 전만 해도 onScroll 이벤트마다 scrollTop 을 체크해서 offsetTop 이 스크롤 영역 안에 들어오는 이미지의 데이터값을 불러 src에 넣는 식으로 이미지 Lazy Loading을 구현했습니다. 요즘에는 세상이 좋아져서(…) img 태그에 loading=“lazy”만 넣어도 브라우저가 알아서 lazyloading을 해줍니다. 최신 브라우저는 다 지원해주기 때문에 스크롤 영역 밖으로 넘어갈 법한 이미지는 모두 loading=“lazy” 를 적용했습니다.

한글 자동완성 제대로 만들기

검색 기능을 만들 지에 대해 두어시간 정도 고민했습니다. 검색은 실은 꽤나 귀찮고 까다로운 일이기 때문입니다. 화면에는 검색창 하나만 구현하면 되고, 언뜻 보기엔 같은 제목의 상품만 찾아다 주면 될거라 생각하지만 실은 그렇지 않습니다. 늘 겪는 문제지만, 자동완성(autocomplete)이 지원되는 ‘한글 검색’을 만드는 것은 특히 까다롭습니다. 영어 알파벳은 단순히 글자가 포함되어있는지로 비교할 수 있지만, 한글은 한글 타이핑 과정에서 언제 글자 입력이 완료되었는지를 판단하기 어려워 단순 비교가 아닌 ‘초성-중성-종성’ 을 분리한 범위 검색을 구현해야 합니다. (전문 용어로 🔎 한글 도깨비불 현상 이라고 부릅니다) 를 쳐도 뉴진스가 나와야하지만, 을 입력하기 위해 를 누른 순간 이라는 글자가 생성되어도, 또는 에 모음이 결합되어 분리되면서 뉴지라는 글자가 생성되어도, 종성(받침)까지 포함하여 뉴진스가 검색될 수 있어야 합니다. 이나 뉴지로만 비교검색하도록 구현하면 검색결과가 텅 비어있을 것입니다.

이를 구현하는 것은 간단합니다. django에는 오래전부터 ORM filter 구문으로 QuerySet을 받는 것 외에, SQL 구문 등의 Raw Query를 직접 요청하여 데이터를 받을 수 있는 기능이 마련되어있습니다. vinyl.coroke.net 는 MySQL RDB를 사용하기로 했기 때문에 SQL Query로 한글 범위검색을 구현할 수 있습니다. 테일러 스위프트를 검색하기 위해 테이까지 입력한 경우 결과를 잘 받을 수 있는 SQL Query는 다음과 같습니다.

SELECT artists.`name` FROM artists WHERE artists.`name` >= "테이" AND artists.`name` <= "테잏"

검색창에 테이까지 입력했을때는 테이를 검색하는 건 물론이고, 테익부터 테일, 테잌까지 입력될거라 가정해야합니다. 따라서 한글 범위를 위와 같이 구성하여, 받침이 없는 경우부터 받침이 있는 경우까지 모두 찾는 것입니다.

검색창에 테일ㄹ 를 입력했다면 모음이 범위에 포함됩니다. 부터 까지 찾아야 할 것입니다.

SELECT `artists`.`name` FROM artists WHERE artists.`name` >= "테일라" AND artists.`name` <= "테일맇" 

다행히도 유니코드는 과거 ‘완성형(KSC5601)’ 코드와 달리 한글의 범위 검색이 가능하도록 순차적으로 글자를 배치해두었습니다. 그래서 위와 같이 한글 조합 원리대로 범위검색을 하여 ‘자연스러운’ 자동완성이 가능토록 구현할 수 있습니다.

한글 키보드 이벤트 대응하기

자동완성 UI는 편의상 키보드 입력 UI를 제공하곤 합니다. 이때, 한글 IME 문제가 발생됩니다. 일부 OS (MacOS) 와 일부 한글 IME(구름 등)을 같이 쓰는 경우 onkeydown, onkeyup이 영문 입력때와 달리 두 번 발생되는 경우가 있습니다. 한글 글자가 조합되려다가 풀리는 과정에서 이벤트를 두번 발생시키기 때문에 그렇습니다. 일반적으로 KeyboardEvent.iscomposing 이 true 일 때 한글 조합이 생성되는 도중인 것으로 간주해서 회피할 수도 있지만, React처럼 compositionstart, compositionend 이벤트를 따로 주는 경우도 있고, 아예 iscomposing이 엉터리로 날아오는 환경도 있습니다. 2000년대부터 쓰여온 회피기법은, 200ms 이내에 같은 이벤트가 반복되면 하나는 무시하도록 setTimeout 을 거는 것입니다. writersclub.io 글쓰기 에디터에도 적용된 해법입니다.

향후 계획

대부분의 레코드샵은 1주일에 한번씩 상품을 재등록합니다. 그래서 데이터 수집과 정제 로직은 이틀에 한번 천천히 돌도록 했습니다.

당장은 writersclub.io01410.coroke.net 처럼 심심할 때마다 가꾸는 서비스 정도로 둘 예정입니다. 작은 서비스 하나 만드는데도 이렇게나 소소하게 고민해야할 이슈가 많다니 다음에는 좀 더 작은 서비스를 만들어야겠다고 다짐해봅니다. (과연)

이 개발자는 구직중입니다

이전 회사를 나온 뒤 반년가량 여러 도시를 여행하고, 이제 새로 몰입할 회사를 찾던 중입니다. 다양한 언어와 프레임워크를 써보긴 했지만, 회사에서는 주로 Python과 node.js, React를 다루었습니다. 시니어 개발자가 필요한 회사가 있다면 언제든 메일 또는 메신저로 연락해주세요. 팀 매니징이나 CTO도 마다하지 않습니다. 간단한 이력 사항은 LinkedIn 에서 확인할 수 있습니다.