연산자는 자바스크립트에서 제일 쉬운 파트라고 다들 그런다. 더하기 빼기 비교하기, 기호만 보면 초등학생도 안다. 근데 입력 폼 하나 만들면서 숫자가 자꾸 문자열처럼 이어붙는 버그를 밟아본 사람은 얘기가 다르다. 기호는 똑같이 생겼는데 자바스크립트는 상황마다 다르게 해석한다. 그 틈을 모르고 넘어가면 꼭 나중에 발목을 잡힌다.
더하기 연산자는 상황 따라 다른 일을 한다. 숫자끼리면 더하고, 문자열이 하나라도 끼면 그냥 이어붙인다. 할인가 계산 폼에서 이 차이 때문에 가격이 이상하게 찍히는 걸 본 적이 있다.
const price = document.querySelector("#price").value; // 인풋에서 나온 값은 항상 문자열
console.log(price + 1000); // "500001000", 더한 게 아니라 이어붙였다
HTML 인풋 값은 숫자만 입력해도 자바스크립트로 넘어오면 무조건 문자열이다. 여기에 숫자를 더하면 자바스크립트는 둘 중 하나가 문자열이니 나머지도 문자열로 바꿔서 붙여버린다. 처음엔 서버 응답이 이상한 줄 알고 API 로그부터 뒤졌는데, 범인은 인풋 값을 그대로 더한 클라이언트 코드였다.
const price = Number(document.querySelector("#price").value); // 명시적으로 숫자 변환
console.log(price + 1000); // 501000
반대로 빼기, 곱하기, 나누기는 문자열이 껴 있어도 무조건 숫자로 바꿔서 계산한다. "5" - 1은 4가 나오는데 "5" + 1은 "51"이 나온다. 같은 숫자처럼 생긴 값인데 연산자 하나로 결과가 완전히 갈린다. 그 뒤로 폼 값을 받으면 습관적으로 맨 먼저 Number부터 씌우고 시작한다.
느슨한 비교는 생각보다 훨씬 느슨하다. 상품 아이디 비교하는 코드에서 == 하나 믿고 있다가 나중에 완전히 다른 값끼리 통과되는 걸 보고 식겁한 적이 있다.
const savedId = 100; // DB에서 가져온 숫자
const urlId = "100"; // 주소창 쿼리는 항상 문자열
console.log(savedId == urlId); // true, 타입 무시하고 값만 맞춰본다
console.log(savedId === urlId); // false, 타입까지 같아야 통과
==는 타입이 다르면 자바스크립트가 알아서 한쪽을 변환해서 맞춰본다. 위 예제야 어쩌다 맞아떨어졌지만, 0 == ""도 true고 null == undefined도 true다. 이 변환 규칙을 다 외우고 쓰느니 그냥 ===만 쓰는 게 훨씬 마음 편하다. 타입까지 같아야 통과하니까 예상 밖의 결과가 나올 일 자체가 없다.
참인지 거짓인지도 결국 변환의 결과다. if에 아무 값이나 넣어도 자바스크립트가 알아서 참 아니면 거짓으로 바꿔서 판단하는데, 이걸 몰라서 빈 장바구니를 채워진 것처럼 표시한 적이 있다.
const cart = []; // 빈 배열, 속은 비었지만 객체는 객체다
if (cart) { console.log("장바구니 있음"); } // 빈 배열인데도 이 줄이 실행된다
if (cart.length) { console.log("장바구니 있음"); } // 이제 진짜 개수로 판단한다
{}이나 []는 속이 텅 비어 있어도 객체라서 자바스크립트는 무조건 참으로 취급한다. 배열이 비었는지 확인하려면 배열 자체가 아니라 length를 봐야 한다. 확실하게 참 거짓 값만 뽑아내고 싶을 땐 Boolean(cart.length)처럼 대놓고 변환해서 쓰면, 나중에 다시 봐도 뭘 검사하는 코드인지 한눈에 들어온다. 반대로 거짓으로 취급되는 값들, 그러니까 0, "", null, undefined, NaN은 전부 거짓 판정이라는 것만 외워두면 아래 ?? 얘기가 훨씬 쉬워진다.
기본값 채워주다가 진짜 0을 지워버린 적이 있다. 재고 수량이 0인 상품을 새로 등록했는데 화면에는 계속 기본 수량이 떠 있는 버그였다.
const stock = 0; // 진짜로 재고가 0개
const displayStock = stock || 10; // 값이 없으면 기본값 10
console.log(displayStock); // 10, 재고 0을 무시해버렸다
||는 왼쪽 값이 false, 0, "", null, undefined처럼 조건문에서 거짓 취급되는 값이면 전부 오른쪽으로 넘어간다. 재고 0도 여기 걸려서 진짜 값인데 지워지고 기본값으로 덮였다. 필요한 건 값이 아예 없을 때만 기본값을 쓰는 동작이었다.
const displayStock = stock ?? 10; // null 또는 undefined일 때만 기본값
console.log(displayStock); // 0, 이제 진짜 값이 살아남는다
??는 왼쪽이 null이거나 undefined일 때만 오른쪽 값을 쓴다. 0이나 빈 문자열은 엄연히 유효한 값으로 봐준다. 수량이나 점수처럼 0이 진짜 의미 있는 데이터를 다룰 땐 || 말고 ??를 쓰는 걸 원칙으로 삼았다.
const settings = { theme: null };
settings.theme = settings.theme ?? "dark"; // 풀어 쓴 버전
settings.theme ??= "dark"; // 같은 뜻을 한 줄로 줄인 버전
console.log(settings.theme); // "dark"
??=는 왼쪽 변수가 null이거나 undefined일 때만 오른쪽 값을 대입한다. 이미 값이 있으면 건드리지 않고 그냥 지나간다. 설정값이나 캐시처럼 한 번만 채워 넣으면 되는 값을 초기화할 때 코드 한 줄이 줄어드니 자주 손이 간다.
중첩된 객체를 파고들다 화면이 통째로 하얗게 뜬 적도 있다. 사용자 목록에서 주소 정보가 없는 사람도 섞여 있었는데 그걸 놓쳤다.
const user = { name: "김철수" }; // address 자체가 없다
console.log(user.address.city); // TypeError, 여기서 렌더링이 통째로 멈춘다
address가 아예 없는 객체에서 .city를 또 꺼내려니 자바스크립트가 곧바로 에러를 던지고 멈춰버렸다. 데이터가 비어 있는 사용자는 딱 한 명이었는데 그 사람 프로필 페이지 전체가 하얀 화면으로 죽어버렸다.
console.log(user.address?.city); // undefined, 에러 없이 조용히 넘어간다
const cityName = user.address?.city ?? "주소 없음"; // 기본 문구까지 붙이면 완성
console.log(cityName); // "주소 없음"
?.은 중간 값이 null이거나 undefined면 그 자리에서 에러 대신 undefined를 돌려주고 멈춘다. 뒤에 ??로 기본 문구까지 붙이면 데이터가 비어 있는 사용자도 에러 없이 화면이 뜬다. 함수도 user.sayHi?.()처럼 쓰면 함수가 아예 없을 때만 조용히 건너뛴다.
돌아보면 이 네 가지 전부 연산자 기호 자체보다 자바스크립트가 몰래 하는 타입 변환 때문에 터진 버그였다. 인풋값엔 숫자 씌우기, 비교는 무조건 ===, 기본값은 ??로, 중첩 객체는 ?.로 뚫기. 이 네 개만 습관 들여도 타입 변환 때문에 자다가 벌떡 일어날 일은 확 줄어든다. 기호는 쉬워 보여도 뒤에서 무슨 변환이 일어나는지 모르면 코드는 늘 같은 자리에서 새고 있다.