if랑 for는 코딩 배우고 제일 먼저 만지는 문법이라 다들 우습게 본다. 나도 그랬다. 근데 조건 순서 하나, break 하나 빠뜨린 걸로 서비스가 이상하게 도는 걸 몇 번 겪고 나니까 생각이 바뀌었다. 이 편은 조건문과 반복문에서 실제로 사고 쳤던 지점들 얘기다.
if else는 위에서부터 순서대로 걸리고, 먼저 걸린 곳에서 끝난다. 회원 등급별 할인율을 매기면서 조건을 낮은 금액부터 순서대로 썼다가 사고가 났다.
function getGradeDiscount(totalSpent) {
if (totalSpent >= 0) {
return 0;
} else if (totalSpent >= 500000) {
return 0.05;
} else if (totalSpent >= 2000000) {
return 0.1;
}
}
console.log(getGradeDiscount(3000000)); // 0, 3백만원 쓴 VIP인데 할인이 0퍼센트다
총 구매액이 얼마든 0원보다는 크니까 제일 위 if (totalSpent >= 0)에서 바로 걸려버린다. 자바스크립트는 위에서부터 조건을 검사하다가 처음 참이 되는 데서 멈추고 나머지 else if는 쳐다보지도 않는다. 아래 조건은 영원히 실행될 일 없는 죽은 코드였다. 큰 값부터 검사하도록 순서만 뒤집으면 해결된다.
function getGradeDiscountFixed(totalSpent) {
if (totalSpent >= 2000000) {
return 0.1;
} else if (totalSpent >= 500000) {
return 0.05;
} else {
return 0;
}
}
console.log(getGradeDiscountFixed(3000000)); // 0.1, 이제야 VIP 할인이 제대로 걸린다
좁은 조건이 넓은 조건한테 먹히지 않게, 까다로운 조건부터 위에 올려야 한다는 걸 몸으로 배웠다.
switch는 break 하나 빠뜨리면 다음 case까지 줄줄이 실행된다. 주문 상태 알림 로직에서 배송 시작 알림을 보낸 고객한테 배송 완료 알림까지 같이 나가는 사고가 있었다.
function notifyOrderStatus(status) {
switch (status) {
case "paid":
console.log("결제 완료 알림 발송");
break;
case "shipping":
console.log("배송 시작 알림 발송");
case "delivered":
console.log("배송 완료 알림 발송");
break;
default:
console.log("알 수 없는 상태");
}
}
notifyOrderStatus("shipping");
// 배송 시작 알림 발송
// 배송 완료 알림 발송, 둘 다 나가버렸다
case "shipping" 끝에 break가 없으니 조건을 다시 검사하지 않고 바로 아래 case "delivered"까지 흘러 내려간다. break를 만날 때까지 아래로 쭉 실행되는 걸 fall-through(폴스루)라 부른다. break 한 줄 추가하니 바로 잡혔다.
배열은 for...in으로 돌리면 안 된다. 상품 목록 배열에 개수를 세어두려고 속성 하나를 얹어놨는데, for...in으로 돌리다가 화면에 이상한 줄이 하나 더 찍혔다.
const products = ["노트북", "마우스", "키보드"];
products.total = products.length; // 배열에 속성 하나를 얹어놨다
for (const key in products) {
console.log(key, products[key]);
}
// 0 노트북
// 1 마우스
// 2 키보드
// total 3, 이 한 줄 때문에 화면에 undefined 행이 하나 더 찍혔다
for...in은 인덱스만 도는 게 아니라 그 객체에 달린 열거 가능한 속성을 전부 훑는다. 배열도 결국 객체라 나중에 얹은 total까지 순회에 끼어버렸다. key도 전부 문자열이라 산술을 하면 이어붙이기가 되는 함정까지 따라온다. 배열은 for...of로 값만 돌아야 한다.
for (const p of products) {
console.log(p);
}
// 노트북
// 마우스
// 키보드, total은 아예 안 나온다
while 루프는 종료 조건 갱신을 깜빡하면 탭이 그대로 죽는다. 재고만큼 상품을 자동 배분하는 로직에서, 반복문 안에 재고를 깎는 줄을 통째로 빼먹은 적이 있다.
let stock = 10;
let assigned = 0;
while (stock > 0) {
console.log("상품 배분", assigned);
assigned++;
// stock--; 이 줄을 깜빡했다
}
// 콘솔에 로그가 미친 듯이 쌓이다가 탭이 그대로 멈췄다
while은 조건이 참인 동안 무한히 돈다. stock을 안에서 실제로 줄여야 언젠가 stock > 0이 거짓이 되는데, 갱신 줄을 빼먹으니 조건이 영원히 참으로 남아 브라우저가 먹통이 됐다. stock--; 한 줄 되살리니 정상으로 돌아왔다. do...while은 본문을 먼저 한 번 실행하고 나서야 조건을 본다는 점만 다르니, 헷갈리면 그냥 while을 쓰는 게 안전하다.
break와 continue는 하는 일이 다르고, 중첩 반복문에서 특히 헷갈린다. 신규 아이디가 기존 아이디와 겹치는지 이중 반복문으로 검사하는 코드에서, 중복을 찾고도 반복문이 계속 도는 걸 나중에야 알아챘다.
const newIds = ["u01", "u02", "u03"];
const existingIds = ["a99", "u02", "b77"];
let hasDuplicate = false;
for (const newId of newIds) {
for (const oldId of existingIds) {
if (newId === oldId) {
hasDuplicate = true;
break; // 안쪽 반복문만 빠져나간다
}
}
}
console.log(hasDuplicate); // true는 맞는데, 중복을 찾은 뒤에도 바깥 반복문이 끝까지 다 돈다
break는 자기가 속한 반복문 하나만 끝낸다. 안쪽 for가 끝나도 바깥 for는 다음 newId로 계속 넘어간다. 데이터가 몇만 건으로 늘어나서야 응답이 눈에 띄게 느려져서 원인을 찾았다. 바깥까지 한 번에 끝내려면 라벨을 붙여야 한다.
outer: for (const newId of newIds) {
for (const oldId of existingIds) {
if (newId === oldId) {
hasDuplicate = true;
break outer; // 라벨로 바깥까지 한 번에 탈출
}
}
}
continue는 성격이 다르다. 반복문을 끝내는 게 아니라 지금 회차만 건너뛰고 다음으로 넘어간다. 품절 상품만 건너뛰고 싶을 때 if로 걸러 continue 한 줄이면 else로 감쌀 필요도 없이 깔끔해진다.
돌아보면 여기 나온 사고들 전부 문법을 몰라서 난 게 아니었다. 순서, break 한 줄, 갱신 한 줄을 놓쳐서 난 거였다. 요즘 조건문이나 반복문을 볼 때 이 네 가지부터 확인한다. 앞 조건이 뒤 조건을 미리 먹어버리진 않는지, switch에 break가 case마다 다 박혀 있는지, 배열을 for...in으로 돌고 있진 않은지, 반복문 안에서 종료 조건이 실제로 갱신되는지. 이 네 개만 봐도 조건과 반복에서 나던 사고는 태반이 미리 걸러진다.