조건문과 반복문 자체는 어느 언어나 비슷하다. if는 참이면 실행, for는 돌리는 것. 여기까진 새로 배울 게 없다. 그런데 JS에서 진짜 사고가 나는 지점은 문법이 아니라 조건이 참인지 거짓인지를 판정하는 방식이다. 앞 편에서 다룬 truthy와 falsy가 여기서 폭발한다. 이번 편은 그 판정이 어떻게 함정이 되는지, 그리고 JS만의 반복 구문 몇 개를 짚는다.


if는 값의 진위를 truthy 규칙으로 본다. if에 넣은 값이 불리언이 아니어도 JS는 알아서 참거짓으로 바꿔서 판단한다. 문제는 이 규칙이 직관과 어긋날 때다. 빈 문자열은 거짓이라 아래 코드에서 else로 빠진다.


const name = '';
if (name) {
console.log('이름 있음');
} else {
console.log('이름 비었음'); // 이쪽이 실행됨
}


빈 문자열 ''은 falsy라서 이름 비었음이 찍힌다. 여기까진 오히려 편하다. 값이 있는지 없는지를 if (name) 한 줄로 걸러주니까. 문제는 0이다. 사용자가 입력한 수량이 0일 때, if (count)라고 쓰면 0도 falsy라 값이 없는 걸로 취급돼 버린다. 진짜 없는 건지 값이 0인 건지 구분하려면 if (count === 0)처럼 명시적으로 비교해야 한다.


여러 갈래는 else if로 잇거나 switch로 편다. 조건이 서너 개로 갈리면 if / else if / else 사슬로 처리한다. 등급 매기기 같은 게 전형적인 예다.


function grade(score) {
if (score >= 90) return 'A';
else if (score >= 80) return 'B';
else return 'C';
}
console.log(grade(95)); // A
console.log(grade(83)); // B
console.log(grade(70)); // C


grade(95)A, grade(83)B, grade(70)C다. 조건을 위에서부터 훑다가 처음 참인 데서 멈춘다는 게 핵심이라, 범위 조건은 큰 값부터 좁혀 내려가야 순서가 안 꼬인다. 한편 하나의 값을 여러 경우와 대조할 땐 switch가 읽기 좋다. 다만 switch에는 악명 높은 함정이 하나 있다.


function noBreak(n) {
let out = [];
switch (n) {
case 1: out.push('하나');
case 2: out.push('둘');
case 3: out.push('셋'); break;
case 4: out.push('넷');
}
return out.join(',');
}
console.log(noBreak(1)); // 하나,둘,셋
console.log(noBreak(3)); // 셋


noBreak(1)하나만 나올 것 같지만 실제로는 하나,둘,셋이다. break를 안 걸면 매칭된 case 아래로 줄줄이 흘러내려서(fall-through, 폭포처럼 다음 case로 떨어짐) break를 만날 때까지 다 실행한다. case 1break가 없으니 2, 3까지 쭉 내려가다 case 3break에서 멈춘 것이다. 이 동작을 일부러 쓰는 경우도 있지만, 대개는 break를 빼먹은 버그다. 각 case 끝에 break 붙이는 걸 손가락에 새겨둬라.


반복문은 셋인데 성격이 조금씩 다르다. for는 횟수가 정해졌을 때, while은 조건이 참인 동안, do...while은 일단 한 번 돌고 나서 조건을 본다. do...while은 조건이 처음부터 거짓이어도 무조건 한 번은 실행된다는 게 차이다.


let sum = 0;
for (let i = 1; i <= 5; i++) { sum += i; }
console.log(sum); // 15

let n = 3, w = '';
while (n > 0) { w += n; n--; }
console.log(w); // 321

let m = 0, d = '';
do { d += m; m++; } while (m < 3);
console.log(d); // 012


for는 1부터 5까지 더해 15, while은 3부터 거꾸로 붙여 321, do...while012다. 셋 다 같은 일을 시킬 수 있으니, 실무에선 그냥 forwhile을 압도적으로 많이 쓰고 do...while은 가뭄에 콩 나듯 본다. 반복 도중 빠져나가려면 break, 이번 회차만 건너뛰려면 continue를 쓴다. break는 첫 짝수를 찾자마자 멈추는 식으로, continue는 짝수를 걸러 홀수만 모으는 식으로 자주 쓴다.


배열을 돌 땐 for...of, 객체 키를 돌 땐 for...in이다. 이 둘은 이름이 비슷해서 자주 헷갈리는데, 헷갈린 채로 쓰면 결과가 정말로 다르게 나온다. for...of는 값을 꺼내주고, for...in은 인덱스나 키를 꺼내준다.


const fruits = ['사과', '배', '감'];
let a = [];
for (const f of fruits) { a.push(f); }
console.log(a.join(',')); // 사과,배,감

let b = [];
for (const i in fruits) { b.push(i); }
console.log(b.join(',')); // 0,1,2
console.log(typeof '0'); // string


for...of는 실제 값 사과,배,감을, for...in은 인덱스 0,1,2를 준다. 게다가 for...in이 주는 인덱스는 숫자가 아니라 문자열 '0'이다. 배열은 값이 필요하니 for...of를 쓰는 게 맞고, for...in은 원래 객체의 키를 돌 때 쓰라고 만든 물건이다. 배열에 for...in을 잘못 붙였다가 값 대신 문자열 인덱스가 나와서 한참 헤맨 기억이 다들 하나씩 있다.


짧은 조건은 삼항과 단축 평가로 줄인다. 조건 ? A : B 형태의 삼항 연산자는 간단한 분기를 한 줄로 만든다. 그리고 &&||는 참거짓만 따지는 게 아니라, 평가를 멈춘 지점의 값을 그대로 돌려준다. 이걸 단축 평가(short-circuit)라고 부른다.


const age = 19;
console.log(age >= 18 ? '성인' : '미성년'); // 성인
console.log(0 || '기본값'); // 기본값
console.log('설정값' || '기본값'); // 설정값
console.log(null ?? '기본값'); // 기본값
console.log(0 ?? '기본값'); // 0


||는 왼쪽이 falsy면 오른쪽을 주니 기본값 설정에 자주 쓰인다. 그런데 0 || '기본값'기본값을 주는 데서 함정이 또 나온다. 0이 유효한 값인데도 falsy라 기본값으로 덮여버리는 것이다. 그래서 등장한 게 ??(널 병합, nullish coalescing)다. ??는 왼쪽이 null이나 undefined일 때만 오른쪽을 준다. 0 ?? '기본값'0을 그대로 지켜주는 걸 보면 차이가 뚜렷하다. 값이 0이나 빈 문자열일 수 있는 상황에선 || 말고 ??를 써야 데이터가 안 날아간다. 조건과 반복은 문법이 아니라 이 진위 판정의 함정을 아느냐에서 실력이 갈린다.