2025. 6. 12. 09:00ㆍJavascript/Javascript

아무리 고급 문법들을 익히고 활용하더라도 프로그래밍에서 가장 많은 비중을 차지하는 문법 중 하나는 아마 조건문일 것이다. 오죽했으면 프로그램은 조건문과 반복문만 있으면 돌아간다는 말이 다 있겠는가. 그만큼 조건 분기는 프로그래밍의 기본이기도 하지만 이를 생각 없이 지나치게 중첩하고 나열하면 가독성과 성능이 떨어지는 문제가 발생한다.
물론 컴퓨터의 입장에서야 로직상 문제는 없겠지만 나중에 누군가 유지보수하기 위해 코드를 복기하는 과정에서는 상당한 시간과 노력 그리고 스트레스가 소모될 것이다. 그래서 이러한 조건문을 최적화하는 데에는 스타일 기법이 존재한다.
이번 글에서는 총 4가지 방법론을 소개할 예정인데 첫 번째와 두 번째의 경우 if문의 위치를 조정함으로써 보다 보기 편하게, 그러니까 유지보수하기 좋게 만들어주는 것에 가깝다면 세 번째와 네 번째는 디자인 패턴에 가깝다.
주니어와 시니어 프로그래머를 가르는 기준은 회사마다 다르기 때문에 항상 논란이 되지만 if문을 어떤 식으로 짜느냐에 따라 둘을 가를 수 있다는 점에는 누구나 동의한다. 그만큼 조건문의 리팩토링은 사소한 것처럼 보이지만 유지보수가 중요한 현업에서는 더더욱 중요히 여겨진다. 그러니 우리 지금부터 주니어 딱지를 조금이라도 덜어내러 가보자.
1. 긴 조건식은 함수로 분리
아래 예제는 날짜 비교를 통해 여름 방학인지 겨울 방학인지 구하는 조건식이다. 한눈에 봐도 조건식에 해당하는 코드가 상당히 난잡한 것을 볼 수 있다.
const now = new Date();
const year = now.getFullYear();
if (now > new Date(year, 6, 1) && now < new Date(year, 7, 31)) {
// 매년 7월 1일 ~ 8월 31일일 경우
console.log('여름 방학입니다');
} else if (now > new Date(year, 11, 1) && now < new Date(year + 1, 1, 28)) {
// 매년 12월 1일 ~ 다음 년도 2월 28일일 경우
console.log('겨울 방학입니다');
}
주석을 통해 어느 정도 뜻하는 바를 알 수는 있지만 분기문 코드 자체가 난잡한 것은 사실이다. 이럴 경우 조건식 자체를 아예 함수로 만들어서 심플하게 구성하는 것이 좋다. 이때 함수명은 이름만 보고도 무엇을 판단하는 로직인지 쉽게 알 수 있도록 지어주는 것이 좋다. 위 조건문을 리팩토링한 결과는 아래와 같다.
function isSummerVacation(now) {
const year = now.getFullYear();
return now > new Date(year, 6, 1) && now < new Date(year, 7, 31)
}
function isWinterVacation(now) {
const year = now.getFullYear();
return now > new Date(year, 11, 1) && now < new Date(year + 1, 1, 28)
}
const now = new Date();
if (isSummerVacation(now)) {
console.log('여름 방학입니다');
} else if (isWinterVacation(now)) {
console.log('겨울 방학입니다');
}
isSummerVacation()과 isWinterVacation() 함수를 만들어 주고 긴 조건문을 함수의 return문에 넣었다. 그리고 if문 블록의 조건식은 함수 호출문으로 변경해 준다. 이렇게 함으로써 코드가 더욱 가독성이 좋아지는 것을 볼 수 있다. 전체적인 로직 자체는 변한 것이 없다. 코드를 모듈화하여 그저 if문의 조건식 코드 길이를 함수 호출문을 통해 줄인 것이다.
2. Early Return
Early Return이란 조건문에서 먼저 return할 수 있는 부분을 분리하여 초반의 if문 내에 작성해 함수를 미리 종료하는 기법을 말한다. if-else문이 너무 많으면 그만큼 읽기가 어렵고 조건에 대해 명시적이지 않을 수 있는데 Early Return을 적용하면 뒤의 else문 코드로 진입하지 않기 때문에 로직은 변함이 없으면서도 가독성이 좋고 명시적인 코드로 리팩토링할 수 있다.
아래와 같이 loginService()라는 함수에 로그인 여부, 토큰 여부, 가입 여부를 확인하는 중첩 분기문이 있다고 가정해 보자.
function loginService(isLogin, user) {
let result = '';
// 1. 로그인 여부 확인
if (isLogin == false) {
// 2. 토큰 존재 확인
if (checkToken() == true) {
// 3. 가입 여부 재확인
if (user.nickName == undefined) {
registerUser(user); // 회원 가입하기
result = '회원가입 성공';
} else {
refreshToken(); // 토큰 발급
result = '로그인 성공';
}
} else {
throw new Error('에러 - 토큰이 없습니다 !');
}
} else {
result = '이미 로그인 중';
}
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
}
그리고 이 코드의 대략적인 흐름도를 표현하자면 아래와 같을 것이다.

의도하고자 하는 바는 알겠지만 계속 중첩해서 들어가며 조건문을 파악해야 하기 때문에 가독성이 그리 좋지는 않아 보인다. 이제 이 코드를 최적화해 보자.
Early Return의 리팩토링 순서
1) if문 다음에 나오는 공통된 절차를 첫 번째 분기점 내부에 각각 넣는다.
기껏 코드 중복을 피하겠다고 분기문 밖으로 꺼내 놨더니 반대로 다시 중복시키는 것처럼 보일 수도 있다. 다만 이는 리팩토링에 있어 필요한 과정이니 일단은 그대로 이행해 보자.
여기서 첫 번째 분기점이란 코드 레벨의 첫 if문을 의미한다. 즉 if문 내부에 있는 if문 같은 경우는 제외된다.

function loginService(isLogin, user) {
let result = '';
if (isLogin == false) {
if (checkToken() == true) {
if (user.nickName == undefined) {
registerUser(user);
result = '회원가입 성공';
} else {
refreshToken();
result = '로그인 성공';
}
} else {
throw new Error('에러 - 토큰이 없습니다 !');
}
result += ` (+ 시도 횟수 ${count++}번)`; // 공통된 절차
return result; // 공통된 절차
} else {
result = '이미 로그인 중';
result += ` (+ 시도 횟수 ${count++}번)`; // 공통된 절차
return result; // 공통된 절차
}
}
2) 분기점에서 짧은 절차부터 실행되도록 if문을 반전시킨다.
지금 예시에서 if문과 else문 블록을 보면 코드 절차가 긴 것은 if문이다. 따라서 절차가 짧은 else 블록의 코드를 if문으로 옮겨 주기 위해 if-else를 반전시킨다. 간단하게 기존의 if(isLogin === false)를 거꾸로 if(isLogin === true)로 구성해 주면 코드를 뒤바꿀 수 있게 되어 짧은 부분이 위쪽으로 오게 되고 긴 부분이 아래쪽으로 오게 된다.
function loginService(isLogin, user) {
let result = '';
if (isLogin == true) { // else와 뒤바꾸기 위하여 boolean 을 반전시킨다
result = '이미 로그인 중';
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
} else {
if (checkToken() == true) {
if (user.nickName == undefined) {
registerUser(user);
result = '회원가입 성공';
} else {
refreshToken();
result = '로그인 성공';
}
} else {
throw new Error('에러 - 토큰이 없습니다 !');
}
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
}
}
만약 아래와 같이 첫 if문이 else문이 없는 순수 if문이라서 이와 같은 작업에 무리가 있다면 간단하게 빈 else문을 만들어버리고 진행하면 된다.

3) 짧은 절차가 끝나면 return(함수 내부의 경우)이나 break(for문 내부의 경우)로 일찍 중단점을 추가한다.
지금의 예제는 함수문에 해당이 되고 이미 return문을 위로 올렸으니 그대로 진행하면 된다.

4) else를 제거한다(이때 중첩 하나가 제거된다).
function loginService(isLogin, user) {
let result = '';
if (isLogin == true) {
result = '이미 로그인 중';
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
} // else { 를 제거한다
if (checkToken() == true) {
if (user.nickName == undefined) {
registerUser(user);
result = '회원가입 성공';
} else {
refreshToken();
result = '로그인 성공';
}
} else {
throw new Error('에러 - 토큰이 없습니다 !');
}
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
}
5) 다시 1번으로 돌아가 작업을 반복한다.



6) 리팩토링의 결과
이렇게 중첩된 조건문들을 하나하나 꺼내면서 리팩토링한 결과 코드는 아래와 같이 중첩되지 않고 하나의 레벨에서 각 조건식을 판별하는 형태가 된다. 이렇게 구성하면 로직이 의미하고자 하는 바를 한눈에 알 수 있고 조건식을 중첩하여 생각하지 않고 분리하여 생각함으로써 코드를 빠르게 복기할 수 있게 된다.
function loginService(isLogin, user) {
let result = '';
// 1. 로그인 여부 확인
if (isLogin == true) {
result = '이미 로그인 중';
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
}
// 2. 토큰 존재 확인
if (checkToken() == false) {
throw new Error('에러 - 토큰이 없습니다 !');
}
// 3. 가입 여부 재확인
if (user.nickName == undefined) {
registerUser(user); // 회원 가입하기
result = '회원가입 성공';
} else {
refreshToken(); // 토큰 발급
result = '로그인 성공';
}
result += ` (+ 시도 횟수 ${count++}번)`;
return result;
}


세 번째 분기점인 가입 여부 재확인은 다시 한 번 리팩토링을 하여 else 영역을 없애도 좋고 안 없애도 좋다.
Early Return 기법은 자바스크립트뿐 아니라 다른 프로그래밍 언어에서도 똑같이 적용되는 부분이니 연습해 두는 것이 좋다.
3. Lookup Table
이번에는 좀 더 자바스크립트스러운 조건문 클린 코드에 대해 소개하려 한다. 이 리팩토링 기법은 자바스크립트만의 일급 객체의 특성을 이용한 방법으로 기존 else if 로직을 key-value쌍의 형태로 각각의 논리를 캡슐화한 것이다. 마치 객체 테이블을 조회하여 분기를 실행하는 것과 같다 하여 Lookup Table 기법이라 불린다.
예를 들어 아래와 같이 각 소셜 서비스들의 전용 로그인 함수가 있다고 가정하고 socialLogin() 함수에서 하나의 파라미터 where에 대해 조건별로 로그인 분기가 일자 형태로 나뉘어 있다고 가정해 보자.
const naverLogin = (id) => { return '네이버'; };
const kakaoLogin = (id) => { return '카카오'; };
const facebookLogin = (id) => { return '페이스북'; };
const googleLogin = (id) => { return '구글'; };
const socialLogin = (where, id) => {
let domain;
if (where === 'naver') {
domain = naverLogin(id);
} else if (where === 'kakao') {
domain = kakaoLogin(id);
} else if (where === 'facebook') {
domain = facebookLogin(id);
} else if (where === 'google') {
domain = googleLogin(id);
}
return `${domain} ${id}`;
};
console.log(socialLogin('naver', 'testId'));
console.log(socialLogin('google', 'testId'));
위 코드는 else if의 연속으로 이를 흐름도로 표현하자면 아래와 같을 것이다.

언뜻 보기엔 가독성 면에서 그렇게 큰 문제가 되지는 않아 보이지만 이러한 구성 또한 보다 효율적으로 작성할 수 있다.
Lookup Table의 리팩토링 순서
1. 조건문만 따로 분리한다
가장 먼저 socialLogin() 내 분기문 코드만 따로 뽑아내 별도의 excuteLogin()이라는 함수로 분리해 구성한다. 그러면 기존의 socialLogin()은 excuteLogin() 함수를 호출해 결과 값을 받는 형식이 된다.
const executeLogin = (where, id) => {
if (where === 'naver') {
domain = naverLogin(id);
} else if (where === 'kakao') {
domain = kakaoLogin(id);
} else if (where === 'facebook') {
domain = facebookLogin(id);
} else if (where === 'google') {
domain = googleLogin(id);
}
};
const socialLogin = (where, id) => {
let domain = executeLogin(where, id);
return `${domain} ${id}`;
};
2. if-else문을 switch-case문으로 변환한다.
중첩 if문이 아닌 형태는 switch문으로 변환이 가능하다. 변환하면 아래와 같이 구성될 것이다.
const executeLogin = (where, id) => {
switch (where) {
case 'naver':
return naverLogin(id);
case 'kakao':
return kakaoLogin(id);
case 'facebook':
return facebookLogin(id);
case 'google':
return googleLogin(id);
}
};
const socialLogin = (where, id) => {
let domain = executeLogin(where, id);
return `${domain} ${id}`;
};
cf) siwtch-case문의 경우 다른 언어에서는 해당 케이스로 점프를 해서 바로 원하는 곳으로 이동하지만 자바스크립트에서는 case를 나열된 순서대로 평가하기 때문에 사용을 지양하라는 설도 있다.
3. switch-case문을 객체로 변환한다.
switch문을 잘 보면 각 문자열 case마다 이에 맞는 함수를 호출하고 있다는 규칙을 찾을 수 있다. 따라서 case 부분을 객체의 key 값으로, return문을 객체의 value 값으로 구성해 준다면 아래와 같이 자바스크립트 객체로 똑같이 구성이 가능해진다.
const executeLoginMap = {
naver: naverLogin,
kakao: kakaoLogin,
facebook: facebookLogin,
google: googleLogin,
};
const socialLogin = (where, id) => {
let domain = executeLoginMap[where](id); // naver일 경우 naverLogin(id) 로 함수 실행
return `${domain} ${id}`;
};
그리고 socialLogin()에서 executeLoginMap를 호출할 때 배열 인자를 통해 호출할 key를 얻게 하고, executeLoginMap[where]을 통해 얻은 객체의 value 값인 함수 표현식을 가져와 그대로 (id) 매개변수를 할당하여 함수를 실행함으로써 결과 값을 얻는 디자인 패턴의 일종이라고 볼 수 있다.

처음 그림과 비교하자면 기존에는 올바른 분기를 검사하기 위해 일일이 각 A, B, C 분기를 모두 순회해야 했지만 Lookup Table 기법을 적용하면 모든 조건을 순회할 필요 없이 파라미터 값에 해당되는 객체의 프로퍼티에 접근하는 식으로 구성되어 있기 때문에 위 그림과 같이 병렬적으로 분기에 접근하는 형태가 되고 이는 곧 성능 향상으로 직결된다고 할 수 있다.
4. Responsibility chain pattern
앞서 본 Lookup Table 기법은 매치된 분기가 있으면 그 분기만 실행하고 함수를 종료한다. 그리고 key 매칭 방식을 사용하기 때문에 하나의 인자 값에 대해서만 비교를 하므로 여러 개의 인자를 비교하는 상황이라면 적합하지 않게 된다.
따라서 이에 대한 대응 기법으로 책임 연쇄 패턴(Responsibility chain pattern)이 존재한다. 책임 연쇄 패턴은 GOF 디자인 패턴의 한 종류로, 분기문의 블록들을 객체화하여 다수의 처리 객체(핸들러)들을 체인 형태로 묶는 패턴이다. 그래서 어떠한 요청이 발생했을 때 그 요구를 처리할 객체 핸들러를 순회하는 식으로 하여 분기문을 객체 지향적으로 표현한 기법이라고 볼 수 있다.

위의 Early Return 예제의 결과를 다시 가져와 보자.
const refreshToken = () => {};
const registerUser = () => {};
function loginService(isLogin, checkToken, user) {
let result = '';
let count = 0;
// 1. 로그인 여부 확인
if (isLogin == true) {
result += '이미 로그인 중';
result += ` (+ 시도 횟수 ${++count}번)`;
return result;
}
// 2. 토큰 존재 확인
if (checkToken == false) {
throw new Error('에러 - 토큰이 없습니다 !');
}
// 3. 가입 여부 재확인
if (user.nickName == undefined) {
registerUser(user); // 회원 가입하기
result += '회원가입 성공';
result += ` (+ 시도 횟수 ${++count}번)`;
return result;
}
refreshToken(); // 토큰 발급
result += '로그인 성공';
result += ` (+ 시도 횟수 ${++count}번)`;
return result;
}
console.log(loginService(false, true, { nickName: 'testId' })); // 로그인 성공 (+ 시도 횟수 1번)
console.log(loginService(true, false, { nickName: 'testId' })); // 이미 로그인 중 (+ 시도 횟수 1번)
console.log(loginService(false, true, {})); // 회원가입 성공 (+ 시도 횟수 1번)
console.log(loginService(false, false, { nickName: 'testId' })); // Error: 에러 - 토큰이 없습니다 !
이 자체만으로도 별다른 문제는 없지만 만약 새로운 분기문이 추가될 경우 함수 코드 자체를 통으로 뜯어고쳐야 할 수도 있다. 이는 하나의 함수 안에 여러 가지 인자에 대한 조건을 판단하는 책임이 중앙집권적으로 모여 있기 때문이다.
클래스를 이용한 책임 연쇄 패턴 설계
따라서 위의 Early Return 예제의 결과를 책임 연쇄 패턴으로 재구성해 보자.
class Handler {
nextHandler; // 연결될 다음 핸들러 객체를 저장하는 필드
result = '';
count = 0;
setNext(handler) {
this.nextHandler = handler; // 메서드를 통해 연결할 핸들러를 등록한다
return handler; // 메서드 체이닝을 위해 인자의 핸들러를 리턴한다
}
}
class LoginHandler extends Handler {
check(isLogin, checkToken, user) {
// 1. 로그인 여부 확인
if (isLogin == true) {
this.result = '이미 로그인 중';
this.result += ` (+ 시도 횟수 ${++this.count}번)`;
return this.result;
} else {
return this.nextHandler.check(isLogin, checkToken, user);
}
}
}
class TokenHandler extends Handler {
check(isLogin, checkToken, user) {
// 2. 토큰 존재 확인
if (checkToken == false) {
throw new Error('에러 - 토큰이 없습니다 !');
} else {
return this.nextHandler.check(isLogin, checkToken, user);
}
}
}
class JoinHandler extends Handler {
check(isLogin, checkToken, user) {
// 3. 가입 여부 재확인
if (user.nickName == undefined) {
registerUser(user); // 회원 가입하기
this.result = '회원가입 성공';
this.result += ` (+ 시도 횟수 ${++this.count}번)`;
return this.result;
} else {
return this.nextHandler.check(isLogin, checkToken, user);
}
}
}
class SuccessHandler extends Handler {
check(isLogin, checkToken, user) {
refreshToken(); // 토큰 발급
this.result = '로그인 성공';
this.result += ` (+ 시도 횟수 ${++this.count}번)`;
return this.result;
}
}
const loginService = (isLogin, checkToken, user) => {
let result = '';
// 1. 핸들러(처리 객체) 생성
const handler1 = new LoginHandler();
const handler2 = new TokenHandler();
const handler3 = new JoinHandler();
const handler4 = new SuccessHandler();
// 2. 핸들러 체이닝 등록
handler1.setNext(handler2).setNext(handler3).setNext(handler4);
// 3. 핸들러 실행
result = handler1.check(isLogin, checkToken, user); // handler1 → handler2 → handler3 → handler4
return result;
};
console.log(loginService(false, true, { nickName: 'testId' })); // 로그인 성공 (+ 시도 횟수 1번)
console.log(loginService(true, false, { nickName: 'testId' })); // 이미 로그인 중 (+ 시도 횟수 1번)
console.log(loginService(false, true, {})); // 회원가입 성공 (+ 시도 횟수 1번)
console.log(loginService(false, false, { nickName: 'testId' })); // Error: 에러 - 토큰이 없습니다 !
각 핸들러들은 자기 고유의 요청에 대한 처리의 책임을 가지고 있으며 실행부에서 setNext() 메서드를 통해 핸들러를 체인시켜 준다. 그리고 실행 함수를 호출하면 마치 if-else처럼 분기를 객체마다 순회하면서 처리하게 되는 것이다. 이런 식으로 복합문 else-if 논리를 분리하는 데 성공했다.
보다 심플한 책임 연새 패턴 설계
자바스크립트는 일급 객체의 특징을 갖는 언어로 굳이 클래스 지향으로 설계할 필요가 없다. 이 부분은 모든 GOF 디자인 패턴의 단점이기도 한데, 당장 위의 코드만 보더라도 엄청나게 길어진 것을 확인할 수 있다. 따라서 일반 객체를 이용해 다음과 같이 좀 더 자바스크립트답게 심플한 리팩토링을 할 수 있다.
먼저 객체를 담은 배열을 생성하고 원소 객체마다 match와 action이라는 함수를 각각 만들어 준다. match 부분은 if문의 조건식을 넣는 부분이고 action은 if문 블록 안의 실행 코드를 넣는 부분이다. match는 flase일 경우 다음 match로 넘어가게 된다. action은 match가 true일 경우 실행되고 평가를 종료한다.
const rules = [
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
},
{
match: function (a, b, c) { /* ... */ },
action: function (a, b, c) { /* ... */ }
}
]


이러한 기법을 이용해 최종으로 구성한 자바스크립트의 책임 연쇄 패턴 코드는 아래와 같다.
const Handler = {
result: '',
count: 0,
rules: [
{
// match는 false일 경우 다음 match로 넘어가게 된다.
match(isLogin, checkToken, user) {
return isLogin ? true : false; // 1. 로그인 여부 확인
},
// action은 match가 true 인 실행 부분을 기재한다
action(isLogin, checkToken, user) {
Handler.result = '이미 로그인 중';
Handler.result += ` (+ 시도 횟수 ${Handler.count++}번)`;
return Handler.result;
},
},
{
match(isLogin, checkToken, user) {
return !checkToken ? true : false; // 2. 토큰 존재 확인
},
action(isLogin, checkToken, user) {
throw new Error('에러 - 토큰이 없습니다 !');
},
},
{
match(isLogin, checkToken, user) {
return user.nickName == undefined ? true : false; // 3. 가입 여부 재확인
},
action(isLogin, checkToken, user) {
registerUser(user); // 회원 가입하기
Handler.result = '회원가입 성공';
Handler.result += ` (+ 시도 횟수 ${++Handler.count}번)`;
return Handler.result;
},
},
{
match(isLogin, checkToken, user) {
return true; // 마지막 핸들러 부분이니까 바로 action()이 실행되도록
},
action(isLogin, checkToken, user) {
refreshToken(); // 토큰 발급
Handler.result = '로그인 성공';
Handler.result += ` (+ 시도 횟수 ${++Handler.count}번)`;
return Handler.result;
},
},
],
};
const socialLogin = (isLogin, checkToken, user) => {
let result = '';
// {march, action} 핸들러 객체가 들어있는 의사 결정 규칙 배열 rules를 순회하면서
for (const rule of Handler.rules) {
// 만일 해당 분기에 해당되면
if (rule.match(isLogin, checkToken, user)) {
result += rule.action(isLogin, checkToken, user); // 그 분기의 실행부를 호출한다
return result;
}
}
};
console.log(loginService(false, true, { nickName: 'testId' })); // 로그인 성공 (+ 시도 횟수 1번)
console.log(loginService(true, false, { nickName: 'testId' })); // 이미 로그인 중 (+ 시도 횟수 1번)
console.log(loginService(false, true, {})); // 회원가입 성공 (+ 시도 횟수 1번)
console.log(loginService(false, false, { nickName: 'testId' })); // Error: 에러 - 토큰이 없습니다 !
'Javascript > Javascript' 카테고리의 다른 글
[Javascript] 이벤트 제거하기 (0) | 2025.06.13 |
---|---|
[Javascript] reduce()에 break 걸기 (0) | 2025.06.13 |
[Javascript] 이벤트 핸들러 등록 여부 확인하기 (1) | 2025.06.11 |
[Javascript] 객체: 메서드와 this (1) | 2025.06.11 |
[Javascript] 전역 객체(Global Object) (0) | 2025.06.10 |