
이전 회사에서 switch(true) 패턴을 자주 사용했다.
최근 코드를 짜면서 관성적으로 해당 패턴을 사용한 뒤, 셀프 리뷰 도중 문득 그런 생각이 들었다. 이게 정말 괜찮은 코드인가?
회사에서 짠 코드를 보기 쉽게 각색하여 switch(true) 패턴과 if-else 패턴으로 적어보았다.
switch (true) {
case isOnlyOneItem:
// Do Nothing
return;
case isFirst:
const originalFirstItem = _items[1];
newRank = originalFirstItem.prevRank();
break;
case isLast:
newRank = _items.at(-1).nextRank();
break;
case isBetween:
newRank = beforeRank.between(afterRank);
break;
default:
// case for last index item and has more items to load
const newItem = await fetchNextItem();
newRank = _items.at(-1).between(newItem.rank)
break;
}
if (isOnlyOneItem) {
// Do Nothing
return;
} else if (isFirst) {
const originalFirstItem = _items[1];
newRank = originalFirstItem.prevRank();
} else if (isLast) {
newRank = _items.at(-1).nextRank();
} else if (isBetween) {
newRank = beforeRank.between(afterRank);
} else {
// case for last index item and has more items to load
const newItem = await fetchNextItem();
newRank = _items.at(-1).between(newItem.rank)
}
코드는 드래그 앤 드랍 이후 정렬을 위해 새로운 rank를 결정하는 코드이다.
정렬 간 rank 개념이 궁금하다면 Jira에서 만들었으며, 라인에서도 사용하는 LexoRank 개념을 찾아보삼!
얼핏 보기에는 switch(true) 구문이 더 깔끔해보인다.
코드를 읽는게 아니라 그냥 그림 보듯이 슥 보면 말이다.
그런데 지금와서 생각해보면, Readability를 향상시키고자 적었던 이 패턴이 실제로는 오히려 해친다는 생각이 들었다.
나와 같이 switch(true)를 사용하는 개발자가 있다면 이 구문을 보고 슥 넘어갈지도 모르지만, 아마 처음 보는 사람이라면 switch(true)? 이게 뭐지? 바본가? 라는 생각부터 들 법하다.
이게 첫번째 문제라고 생각한다.

MDN에도 친절히 나와있듯, switch 직후 인자인 expression에는 case 구문과 매칭될 것으로 기대되는 값을 넣어주어야 한다.
true를 넣는 것은 편법이며, 표현식을 평가해서 그 값과 일치하는 case 절을 찾아 실행한다 라는 switch의 설계 의도에 부합하지 않다. (제일 큰 문제다)
if else에서는 앞에서 평가한 expression의 타입이 다음 expression에 반영된다.
좀 어거지로 만들긴 했지만 아래 예시를 보면, 첫번째 if문에서 value가 undefined가 아닌 것을 알 수 있으므로, 두번째 else if에서 '+' 연산자를 써도 문제가 없다.
value의 타입이 number로 좁혀졌기 때문이다.
interface Value {
value?: number;
}
if(value === undefined) {
return value;
} else if(value + 1 === 2) {
return value;
} else {
return 0;
}

그러나 switch 문에서는 case 블록의 조건을 Type Narrowing의 근거로 사용하지 않는다. 요게 은근 불편할 때가 많았다.
switch(true) {
case value === undefined:
return value;
case value + 1 === 2:
return value;
default:
return 0;

Type Narrowing에 관한 내용은 타입스크립트 문서에서 확인 가능하다.
자주하는 고민 중 하나는 if-else 문에서 우선순위가 중요한 경우 어떻게 적는 것이 좋겠냐는 것이다.
순서가 중요한 경우는 조건1이 참이고 조건2도 참일 때 조건1에 해당하는 동작을 먼저 수행해야 할 때이다.
예를 들어, 로그인을 안한 경우 접근 불가능한 페이지가 있다. 그러나 맥북 M4 PRO 오너라면 로그인 없이 접근할 수 있는 사이트가 있다고 가정하자.
그렇다면 코드는 아래와 같이 짤 수 있다.
if (isMacbookM4ProOwner) { // 로그인 안해도 입장 가능
return '환영합니다'
} else if (!isLogin) {
return '로그인 해주세요'
}
그런데 만약 평가식의 순서를 바꾸게 된다면
if (!isLogin) {
return '로그인 해주세요'
} else if (isMacbookM4ProOwner) {
return '환영합니다'
}
아뿔싸, 맥북 M4 프로 오너임에도 로그인하라는 메시지를 받게된다.
이와 같이 평가식에서 참인 케이스가 여럿인 경우는 개발하다보면 은근 있다.
지금은 평가식이 두 개여서 간단하지만 더 많은 케이스가 추가된다면?
if (isLoading) {
return '로딩중...'
} else if (isMacbookM4ProOwner) { // 로그인 안해도 입장 가능
return '환영합니다'
} else if (isWindows) {
return '이 참에 바꿔보시는건 어떨까요?'
} else if (!isLogin) {
return '로그인 해주세요'
}
그럴때는 그냥 if문을 분리하자.
if (isLoading) {
return '로딩중...'
}
if (isMacbookM4ProOwner) {
return '환영합니다'
} else if (isWindows) {
return '이 참에 바꿔보시는건 어떨까요?'
}
if (!isLogin) {
return '로그인 해주세요'
}
영역을 잘 분리하는 것이 가독성에 도움이 된다고 생각한다.
물론 위의 예시는 return을 해주기에 유의미하다. 안하면 환영도 하고 로그인도 하고 난리난다.
여러 우선순위가 있고 조건이 복잡하다면 조건을 변수로 분리하는 등 최대한 노력을 기울여보자. 거지같은 비즈니스 로직에서 비롯된 if else문 떡칠 속에서도 깨끗한 코드 한 줄을 적어내려고 노력하자.
switch(true)는 이제 잊자. switch문은 평가 가능한 enum 혹은 value에 대해 사용하도록 하고, if-else는 예쁘게 잘 쓰면 되겠다.
최근에 알게된 건데, useMount useEffectOnce (react-use 라이브러리에서 제공하는 라이프사이클 관련 훅)은 리액트 공식 문서에서 쓰지 말라고 언급되어 있다. 그것도 모르고 그동안 신나게 썼다.
관성적으로 적는 코드를 다시 한 번 잘 들여다보자. 나도 모르게 나쁜 습관이 배어 있을 수 있다.