3차 프로젝트가 시작되었다. 2차 프로젝트만큼 재밌는 아이디어가 선정되어 재밌게 작업하고 있다. 이번글에서는 사용자가 특정 단어를 입력하면 이스터에그 컴포넌트를 렌더링하는 기능을 구현하며 겪은 리팩토링과 확장성 고민을 공유하려 한다. 사실 공유하고자 한다기 보다 글을 쓰면서 내 생각을 정리하고 싶었다. # 기능 설명
기능 설명을 하자면 쓰레드(채팅방)에서 특정 단어가 입력하면 해당 쓰레드에 접속해 있는 모든 사용자 화면에 이스터에그 컴포넌트를 마운트하고 일정 시간이 지나면 자동으로 언마운트되도록 하는 기능이다.
우리는 백엔드 단을 Supabase로 구현했기 때문에 수파베이스에서 제공하는 real time 기능을 이용하기로 했다.
trigger_events
이벤트를 발생시킬 쓰레드 아이디와 단어를 저장하는 테이블을 만들었고 real time 기능을 활성화 시켰다. 이제 쓰레드에 입장하면 이 테이블을 구독 하고 row insert가 일어나면 클라이언트에서 이를 감지할 수 있게 된다.
# 초기 설계
이제 사용자가 특정 단어가 포함된 메시지를 입력하면 그 단어를 추출해서 테이블에 insert만 해주면 된다. 그래서 아래와 같이 초반엔 단어와 컴포넌트 1:1 매칭으로 기능을 구현했다.
// 예시
const handleTriggerEasterEgg = (message:string): ComponentType<any> | null => {
// 메시지에서 트리거 단어 추출
const word = extractTriggerWord(message);
// 스위치 문으로 1:1 컴포넌트 매칭
switch(word) {
case '범쌤' :
return Beom;
case '강아지' :
return Dog;
default :
return null;
}
}
# 문제점
메시지에서 단어를 추출하고 switch문으로 매칭해서 컴포넌트를 호출하면 간단하게 구현이 가능하다. 하지만 <Beom /> 컴포넌트를 범쌤이 아닌 더 다양한 단어로 호출하고 싶다면 어떻게 해야할까?
switch(word) {
case '범쌤' :
return <Beom/>;
case '범썜' : // 오타도 호출되게
return <Beom/>;
case '강사님' :
return <Beom/>;
.
.
.
}
더 다양한 이스터에그 효과를 추가하려고 한다면 switch문은 끝임 없이 길어져 가독성이 떨어질 것이다. 그래서 더 확장성 있게 만들고 싶어졌다.
# 리팩토링
확장성을 고려해 리팩토링을 한다면 어떤 방식이 있을까? 우선 어떤식으로 trigger-words를 관리할지 고민 해보았다. trigger-words는 계속 추가 될 수 있고 비슷한 단어들을 묶어 하나의 컴포넌트를 호출하게 만들 수도 있다. 이런 고민을 하던 중 mapping table + registry parttern 조합을 찾아보게 되었다.
mapping table
map 을 사용하면 하나의 key로 여러가지 단어를 관리할 수 있다. key를 카테고리로 설정하고 해당 카테고리에 포함할 단어들을 배열로 관리한다.
// [category : string] : string[]
const TRIGGER_CATEGORIES : Record<Categories, string[]> = {
teacher : ['범쌤', '범썜' ,'강사님', '선생님', '근육맨'],
dog: ['강아지', '갱얼쥐', '가나디', '개', 'dog'],
cat: ['고양이', '야옹이','때껄룩', '킹냥이'],
nimo: [.....],
}
이제 trigger-word를 추가하고 싶다면 카테고리에 맞는 배열에 단어를 추가하면 된다. 카테고리를 더 추가하고 싶다면 TRIGGER_CATEGORIES에 카테고리를 추가하기만 하면 된다.
Registry
다음은 매핑 테이블을 만들었다면 레지스터를 설정해줘야한다. 사용자가 작성한 메시지에서 RIGGER_CATEGORIES 를 이용해서 카테고리를 추출하면 TRIGGER_REGISTRY 에서 매칭되는 컴포넌트를 반환해주면 된다.
// 레지스터
const TRIGGER_REGISTRY : Record<Categories, ComponentType<any>> = {
teacher: Beom,
dog: Dog,
cat: Cat,
}
Registry의 의미
키(key) : 객체(Object / 함수 / 컴포넌트)를 매핑해서 저장해두고 필요할 때 키로 객체를 찾아오는 저장소 역할을 하는 구조이다.
•중앙 집중 관리 : 여러 객체 (컴포넌트, strategy, 핸들러)를 한곳에 모아둠
•동적 조회 : 실행 시점에 키로 원하는 객체를 가져올 수 있음
•확장성 : 새로운 객체를 추가할 때는 레지스트리에 등록하면 기존 분기 (switch / if else)의 수정이 필요 없음
# 궁금증
mapping table과 registry 둘다 맵 객체를 사용한 패턴인데 왜 이름이 다를까? 키 → 값 매핑 구조라는 점에서 똑같아 보이는데 말이다.
구조는 똑같지만 정의한 의도가 다르기 때문인데, 매핑 테이블은 데이터 매핑 즉 단순히 값을 찾기 위해 정의한다면 레지스트리는 객체/로직/컴포넌트 관리와 실행 시점에서 조회하기 위해 정의 하기 때문에 구분을 한다.
매핑 테이블은 규칙/상수 관리에, 레지스트리는 실행 시점 조회와 확장성에 중점을 둔다고 생각하면 된다.
# 마치며
이번 프로젝트는 이전보다 확장성과 최적화를 더 고려하며 작업하다 보니 자연스럽게 많은 것들을 배우고 있는 것 같다. 지금도 바쁘게 MVP를 만들어 내고 있지만 단순히 기능을 구현하는데 그치지 않고 더 나은 구조와 패턴을 고민하면서 작업하다 보니 확실히 성장하고 있다는 걸 느낄 수 있는 것 같다.