2.4 연산자 심화
논리 연산자와 단락 평가
JavaScript의 논리 연산자는 단순히 true/false를 반환하는 것이 아니라, 실제 피연산자 값을 반환합니다. 이를 **단락 평가(Short-circuit Evaluation)**라고 합니다.
&& (AND) 연산자
왼쪽이 falsy이면 왼쪽 값 반환, 그렇지 않으면 오른쪽 값 반환.
// 기본 동작
console.log(true && true); // true
console.log(true && false); // false
console.log(false && true); // false
// 단락 평가: 실제 값 반환
console.log(1 && 2); // 2 (왼쪽이 truthy → 오른쪽 반환)
console.log(0 && 2); // 0 (왼쪽이 falsy → 왼쪽 반환, 오른쪽 미평가)
console.log("abc" && 42); // 42
console.log("" && 42); // "" (빈 문자열은 falsy)
console.log(null && "value"); // null
// 실용적 패턴: 조건부 실행
const user = { name: "김철수", isAdmin: true };
user.isAdmin && console.log("관리자입니다"); // 실행됨
user.isAdmin && doSomething(); // isAdmin이 true일 때만 실행
// 조건부 렌더링 (React에서 많이 사용)
const isLoggedIn = true;
const element = isLoggedIn && "<div>환영합니다!</div>";
|| (OR) 연산자
왼쪽이 truthy이면 왼쪽 값 반환, 그렇지 않으면 오른쪽 값 반환.
// 기본 동작
console.log(true || false); // true
console.log(false || true); // true
console.log(false || false); // false
// 단락 평가: 실제 값 반환
console.log(1 || 2); // 1 (왼쪽이 truthy → 왼쪽 반환)
console.log(0 || 2); // 2 (왼쪽이 falsy → 오른쪽 반환)
console.log("" || "기본값"); // "기본값"
console.log(null || "대안"); // "대안"
// 기본값 패턴 (전통적 방법)
function greet(name) {
const displayName = name || "익명"; // name이 falsy면 "익명"
return `안녕하세요, ${displayName}!`;
}
console.log(greet("철수")); // "안녕하세요, 철수!"
console.log(greet("")); // "안녕하세요, 익명!" (빈 문자열도 기본값 적용!)
console.log(greet(0)); // "안녕하세요, 익명!" (0도 기본값 적용!)
?? (Nullish 병합 연산자)
||의 문제: 0, "", false 같은 유효한 falsy 값도 기본값으로 대체됨.
??는 null 또는 undefined일 때만 오른쪽 값을 반환합니다.
// ?? vs || 비교
const count = 0;
console.log(count || 10); // 10 (0은 falsy → 기본값 10 사용)
console.log(count ?? 10); // 0 (0은 null/undefined가 아님 → 0 그대로)
const name = "";
console.log(name || "이름없음"); // "이름없음" (빈 문자열은 falsy)
console.log(name ?? "이름없음"); // "" (빈 문자열은 null/undefined 아님)
const value = null;
console.log(value ?? "기본값"); // "기본값" (null이면 기본값)
const value2 = undefined;
console.log(value2 ?? "기본값"); // "기본값" (undefined이면 기본값)
// 실전 사용
function getConfig(userConfig) {
return {
timeout: userConfig.timeout ?? 3000, // 0도 유효한 값으로 처리
retries: userConfig.retries ?? 3,
debug: userConfig.debug ?? false, // false도 유효한 값
};
}
const config = getConfig({ timeout: 0, retries: 0, debug: false });
console.log(config); // { timeout: 0, retries: 0, debug: false }
// || 였다면 모두 기본값으로 대체됐을 것!
?. (옵셔널 체이닝)
null 또는 undefined인 값의 프로퍼티에 접근할 때 에러 대신 undefined를 반환합니다.
const user = {
name: "김철수",
address: {
city: "서울",
zip: "12345",
},
getGreeting() {
return `안녕하세요, ${this.name}!`;
},
};
// 기존 방식 (번거로움)
const city = user && user.address && user.address.city;
// 옵셔널 체이닝 (간결함)
const city2 = user?.address?.city; // "서울"
const country = user?.address?.country; // undefined (에러 없음)
// 없는 사용자
const guest = null;
console.log(guest?.name); // undefined (에러 없음)
console.log(guest?.address?.city); // undefined
// 메서드 호출
console.log(user?.getGreeting()); // "안녕하세요, 김철수!"
console.log(guest?.getGreeting()); // undefined (에러 없음)
// 배열 접근
const arr = [1, 2, 3];
console.log(arr?.[0]); // 1
const empty = null;
console.log(empty?.[0]); // undefined
// ?? 와 결합 (매우 유용!)
const userName = user?.profile?.displayName ?? user?.name ?? "익명";
console.log(userName); // "김철수"
// 함수 존재 여부 확인 후 호출
const callback = null;
callback?.(); // undefined (에러 없음)
const handler = (msg) => console.log(msg);
handler?.("이 메시지가 출력됩니다"); // "이 메시지가 출력됩니다"
논리 할당 연산자 (ES2021)
// ||= (OR 할당): 왼쪽이 falsy일 때만 오른쪽 값 할당
let a = null;
a ||= "기본값";
console.log(a); // "기본값"
let b = "기존값";
b ||= "기본값";
console.log(b); // "기존값" (기존값이 truthy)
// &&= (AND 할당): 왼쪽이 truthy일 때만 오른쪽 값 할당
let c = "원본";
c &&= "업데이트";
console.log(c); // "업데이트"
let d = null;
d &&= "업데이트";
console.log(d); // null (null은 falsy라 할당 안됨)
// ??= (Nullish 할당): 왼쪽이 null/undefined일 때만 할당
let e = null;
e ??= "기본값";
console.log(e); // "기본값"
let f = 0;
f ??= 100;
console.log(f); // 0 (0은 null/undefined가 아님)
// 실전 사용: 캐시 패턴
const cache = {};
function getUser(id) {
cache[id] ??= fetchUserFromDB(id); // 없을 때만 DB 조회
return cache[id];
}
전개 연산자 (Spread Operator)
... 연산자는 이터러블을 개별 요소로 펼칩니다.
// 배열 전개
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// 배열 복사
const copy = [...arr1];
copy.push(99);
console.log(arr1); // [1, 2, 3] (원본 변경 없음)
// 함수 인수에 전개
console.log(Math.max(...arr1)); // 3
// 객체 전개 (ES2018)
const defaults = { theme: "light", lang: "ko", fontSize: 14 };
const userPrefs = { theme: "dark", fontSize: 16 };
const settings = { ...defaults, ...userPrefs }; // 나중 것이 이전 것을 덮어씀
console.log(settings);
// { theme: "dark", lang: "ko", fontSize: 16 }
// 객체 복사 (얕은 복사)
const original = { a: 1, b: { c: 2 } };
const spread = { ...original, d: 3 };
console.log(spread); // { a: 1, b: { c: 2 }, d: 3 }
// 문자열 전개
console.log([..."Hello"]); // ['H', 'e', 'l', 'l', 'o']
// Set → 배열 (중복 제거)
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
console.log(unique); // [1, 2, 3]
지수 연산자와 기타 유용한 연산자
// ** 지수 연산자 (ES2016)
console.log(2 ** 10); // 1024
console.log(3 ** 3); // 27
// Math.pow(2, 10)과 동일
// 비트 연산자 활용
// ~~ 이중 비트 NOT: 소수점 버림 (Math.trunc보다 빠름)
console.log(~~3.7); // 3
console.log(~~-3.7); // -3 (Math.floor와 다름!)
console.log(Math.floor(-3.7)); // -4
// | 0 : 정수 변환
console.log(3.9 | 0); // 3
console.log(-3.9 | 0); // -3
// & 1: 홀짝 판별
console.log(5 & 1); // 1 (홀수)
console.log(4 & 1); // 0 (짝수)
// 쉼표 연산자 (잘 사용 안함, for 루프에서 가끔)
const x = (1, 2, 3); // 마지막 값 3
for (let i = 0, j = 10; i < 5; i++, j--) {
console.log(i, j);
}
// void 연산자: undefined 반환
console.log(void 0); // undefined
console.log(void ""); // undefined
// href="javascript:void(0)"에서 사용됨
고수 팁
옵셔널 체이닝과 Nullish 병합 조합
// API 응답 처리에서 매우 유용
async function fetchUserProfile(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return {
name: data?.user?.profile?.displayName ?? data?.user?.name ?? "Unknown",
avatar: data?.user?.avatar?.url ?? "/default-avatar.png",
bio: data?.user?.bio ?? "",
followerCount: data?.user?.stats?.followers ?? 0,
};
}
// 중첩 객체 안전하게 업데이트
function updateNestedConfig(config, path, value) {
const keys = path.split('.');
let current = config;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] ??= {}; // 없으면 빈 객체 생성
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return config;
}
const config = {};
updateNestedConfig(config, 'database.host', 'localhost');
updateNestedConfig(config, 'database.port', 5432);
console.log(config); // { database: { host: 'localhost', port: 5432 } }