Vanilla JS에서 변수 할당 방법이 다양해서 한 번 정리해 보고자 합니다.
정리 요약
상황별 할당 방식 선택
| 상황 | 권장 방식 | 예시 |
|---|---|---|
| 단순 변수 선언 | const, let |
const name = "Alice" |
| 객체에서 특정 속성 추출 | 구조 분해 | const { email } = user |
| 함수 옵션 처리 | 구조 분해 + 기본값 | function({ retry = true } = {}) |
| 기존 값 보존 + 기본값 설정 | Null 병합 할당 | obj.timeout ??= 5000 |
| Falsy 제외하고 기본값 | OR 할당 | `config.theme |
| 조건에 따른 업데이트 | AND 할당 | user.isActive &&= updateSession() |
| 안전한 속성 접근 | 선택적 체이닝 | user?.profile?.name |
| 조건부 단일 값 | 삼항 연산자 | const status = age >= 19 ? "Adult" : "Minor" |
기본 할당 (Basic Assignment)
가장 기초적인 선언 및 할당 방식입니다. 할당 연산자(=)는 우측 피연산자의 값을 좌측 피연산자(변수 또는 속성)에 할당합니다.
// 선언과 동시에 할당
const name = "Junie";
let count = 0;
// 나중에 할당
let score;
score = 100;
// 할당식 자체도 값을 반환함
let x = 2;
console.log((x = 3 + 1)); // 4 출력 (할당값이 표현식의 결과)
// 여러 변수에 같은 값 할당
let a, b, c;
a = b = c = 5; // 우측에서 좌측으로 평가됨
console.log(a, b, c); // 5 5 5
주의: 선언 키워드 없이 여러 변수를 체이닝하면 의도와 다를 수 있습니다.
// 문제가 있는 코드
const x = y = z = 10;
// x만 const로 선언됨
// y와 z는 비선언 변수 (strict mode에서는 ReferenceError)
구조 분해 할당 (Destructuring Assignment)
객체나 배열의 속성을 해체하여 개별 변수에 즉시 할당하는 강력한 방식입니다. 특히 함수 매개변수나 복잡한 객체에서 필요한 값만 추출할 때 유용합니다.
객체 구조 분해 (Object Destructuring)
const config = {
url: "https://api.example.com",
method: "POST",
timeout: 5000,
headers: { "Content-Type": "application/json" }
};
// 1. 필요한 속성만 추출
const { url, method } = config;
console.log(url, method); // "https://api.example.com" "POST"
// 2. 다른 이름으로 할당 (변수명 별칭)
const { timeout: requestTimeout } = config;
console.log(requestTimeout); // 5000
// 3. 기본값 설정 (undefined일 때만 적용)
const { retryOnUnauthorized = true, auth = "Bearer token" } = config;
console.log(retryOnUnauthorized); // true (config에 없으므로 기본값)
// 4. 중첩 객체 구조 분해
const { headers: { "Content-Type": contentType } } = config;
console.log(contentType); // "application/json"
// 5. 나머지 속성 수집 (REST 패턴)
const { url, ...rest } = config;
console.log(rest); // { method: "POST", timeout: 5000, headers: {...} }
// 6. 선언 없이 할당 (기존 변수에 값 할당)
let a, b;
({ a, b } = { a: 1, b: 2 }); // 괄호 필수 ({ } 블록과 구분)
console.log(a, b); // 1 2
배열 구조 분해 (Array Destructuring)
const colors = ["#FF0000", "#00FF00", "#0000FF"];
// 1. 순서대로 할당
const [red, green, blue] = colors;
console.log(red, green, blue); // "#FF0000" "#00FF00" "#0000FF"
// 2. 일부 생략
const [primary, , tertiary] = colors;
console.log(primary, tertiary); // "#FF0000" "#0000FF"
// 3. 나머지 요소 할당
const [first, ...rest] = colors;
console.log(first, rest); // "#FF0000" ["#00FF00", "#0000FF"]
// 4. 기본값 설정
const [a = "default"] = [];
console.log(a); // "default"
// 5. 값 교체 (스왑)
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1 (임시 변수 불필요)
// 6. 정규식 exec() 결과 분해
const url = "https://example.com:8080";
const [, protocol, host] = /^(\w+):\/\/([^:]+)/.exec(url);
console.log(protocol, host); // "https" "example.com"
중첩 구조 분해
const data = {
user: {
name: "Alice",
profile: {
age: 28,
location: "Seoul"
}
},
posts: [
{ id: 1, title: "First Post" },
{ id: 2, title: "Second Post" }
]
};
// 깊은 중첩 추출
const {
user: {
profile: { location }
},
posts: [{ title: firstTitle }]
} = data;
console.log(location, firstTitle); // "Seoul" "First Post"
복합 대입 연산자 (Compound Assignment Operators)
연산 후 결과를 다시 변수에 할당하는 축약 형태입니다. x += y는 x = x + y와 동일합니다.
let count = 5;
// 산술 연산자
count += 3; // count = count + 3 → 8
count -= 2; // count = count - 2 → 6
count *= 2; // count = count * 2 → 12
count /= 3; // count = count / 3 → 4
count %= 3; // count = count % 3 → 1
count **= 2; // count = count ** 2 → 1
// 문자열 연결 (+=만 해당)
let message = "Hello";
message += " World"; // "Hello World"
// 비트 연산자
let flags = 5; // 0101
flags &= 3; // 0101 & 0011 = 0001 → 1
flags |= 4; // 0001 | 0100 = 0101 → 5
flags ^= 2; // 0101 ^ 0010 = 0111 → 7
flags <<= 1; // 0111 << 1 = 1110 → 14
flags >>= 2; // 1110 >> 2 = 0011 → 3
// BigInt도 지원
let big = 10n;
big += 5n; // 15n
// big += 5; // TypeError: BigInt와 일반 숫자 혼용 불가
주의: +=의 동작은 피연산자의 타입에 따라 결정됩니다.
let result = "5";
result += 3; // "53" (문자열 연결, 숫자 연산 아님)
result = 5;
result += 3; // 8 (숫자 연산)
함수 매개변수에서의 할당
함수가 호출될 때 인자를 받아들이는 과정 자체가 할당입니다.
기본 매개변수 (Default Parameters)
// ES5 방식 (조건문)
function fetchApiES5(url, method) {
method = method || "GET";
console.log(`${method} request to ${url}`);
}
// ES6+ 방식 (더 권장)
function fetchApi(url, method = "GET") {
console.log(`${method} request to ${url}`);
}
fetchApi("/users"); // "GET request to /users"
fetchApi("/users", "POST"); // "POST request to /users"
fetchApi("/users", undefined); // "GET request to /users" (undefined는 기본값 트리거)
매개변수 구조 분해 + 기본값
실무에서 가장 많이 쓰는 패턴입니다. 함수 호출 시 옵션 객체를 받으면서 동시에 개별 변수로 분해합니다.
// 구조 분해 + 기본값 + 나머지 수집
function request({
url,
method = "GET",
timeout = 5000,
retryOnUnauthorized = true,
...otherOptions
}) {
console.log(`${method} ${url}`);
console.log(`Retry enabled: ${retryOnUnauthorized}`);
console.log(`Other options:`, otherOptions);
}
// 호출
request({
url: "/users",
data: { name: "Alice" }
});
// 출력:
// GET /users
// Retry enabled: true
// Other options: { data: { name: "Alice" } }
인자가 전달되지 않을 때의 기본 객체
// 문제: 인자 없이 호출하면 TypeError
function config1({ theme }) {
console.log(theme);
}
// config1(); // TypeError: Cannot destructure property 'theme' of undefined
// 해결: 기본 매개변수로 빈 객체 제공
function config2({ theme } = {}) {
console.log(theme); // undefined
}
config2(); // 에러 없음
function config3({ theme = "dark" } = {}) {
console.log(theme); // "dark"
}
config3(); // "dark"
논리 할당 연산자 (Logical Assignment)
조건문과 할당을 한 번에 처리하여 코드를 매우 간결하게 만듭니다. ES2021+에서 도입되었습니다.
1. 논리 OR 할당 (||=)
좌측이 falsy 값일 때만 우측을 평가하고 할당합니다. 조건부로 기본값을 설정할 때 사용합니다.
let config = { theme: "dark" };
// config.fontSize가 undefined, null, 0, false, ""이면 16 할당
config.fontSize ||= 16;
console.log(config.fontSize); // 16
// 이미 값이 있으면 할당하지 않음 (우측 표현식도 평가 안 함)
config.theme ||= "light";
console.log(config.theme); // "dark" (변경 안 됨)
// 부작용 있는 함수도 short-circuit됨
function getDefault() {
console.log("Called");
return "light";
}
config.theme ||= getDefault(); // "Called" 출력 안 함 (theme이 truthy이므로)
2. 논리 AND 할당 (&&=)
좌측이 truthy 값일 때만 우측을 평가하고 할당합니다. 조건이 참일 때만 값을 업데이트할 때 사용합니다.
let user = {
name: "Bob",
isLoggedIn: true
};
// isLoggedIn이 true면 새로운 세션 정보 할당
user.sessionId &&= generateSessionId(); // 할당됨
user = {
name: "Alice",
isLoggedIn: false
};
user.sessionId &&= generateSessionId(); // 할당 안 됨 (isLoggedIn이 falsy)
// DOM 요소 업데이트 예제
let element = document.querySelector(".user-card");
// element가 존재하면만 스타일 업데이트 (조건문 대체)
element &&= (element.style.display = "block");
3. Null 병합 할당 (??=)
좌측이 null 또는 undefined일 때만 우측을 평가하고 할당합니다. 0, "", false 같은 falsy 값은 유효한 값으로 취급합니다.
let settings = {};
// 1. null 병합 할당 사용
settings.volume ??= 50;
settings.volume ??= 100; // volume이 이미 50이므로 할당 안 됨
// 2. OR 할당과의 차이
let item = { count: 0 };
item.count ||= 10; // 0은 falsy → 10 할당 (문제!)
console.log(item.count); // 10
let item2 = { count: 0 };
item2.count ??= 10; // 0은 null/undefined가 아님 → 할당 안 함 (올바름)
console.log(item2.count); // 0
// 3. 실무 예제
let userCount;
userCount ??= 100; // undefined → 100 할당
let cachedData = null;
cachedData ??= fetchDataFromServer(); // null → 서버에서 데이터 가져옴
let timeout = "";
timeout ??= 5000; // ""는 유효한 값 → 할당 안 함 (의도대로)
timeout ||= 5000; // ""는 falsy → 5000 할당 (다를 수 있음)
Short-Circuit 동작
모든 논리 할당 연산자는 short-circuit 처리됩니다. 할당이 일어나지 않으면 우측 표현식을 평가하지 않습니다.
// 성능: 비용이 큰 함수 호출 방지
function expensiveOperation() {
console.log("Expensive operation running");
return 1000;
}
let cache = { value: 50 };
cache.value ||= expensiveOperation(); // "Expensive operation running" 출력 안 함
let empty = {};
empty.value ||= expensiveOperation(); // "Expensive operation running" 출력 됨 (평가됨)
조건부 및 비동기 할당
프로그램의 흐름에 따라 결과값을 변수에 담습니다.
삼항 연산자를 통한 할당
const age = 25;
const status = (age >= 19) ? "Adult" : "Minor";
console.log(status); // "Adult"
// 중첩된 삼항 연산자 (가독성 주의)
const category =
score >= 90 ? "A" :
score >= 80 ? "B" :
score >= 70 ? "C" : "F";
선택적 체이닝 (Optional Chaining)
객체가 존재하지 않을 때 에러를 내지 않고 undefined를 할당합니다.
const user = {
profile: {
address: "Seoul"
}
};
// 전통적인 방식 (방어적 코드)
const street = user && user.profile && user.profile.address;
// 선택적 체이닝 (더 간결)
const street = user?.profile?.address;
console.log(street); // "Seoul"
// 존재하지 않는 경로
const country = user?.profile?.country;
console.log(country); // undefined (에러 아님)
// 메소드 호출
user?.profile?.printAddress?.(); // address 함수가 있으면 호출
// 배열 접근
const firstItem = user?.items?.[0];
// Null 병합과 조합
const displayName = user?.name ?? "Guest";
비동기 결과 할당
// Promise then 체인
const fetchUser = () => fetch("/api/user").then(res => res.json());
const user = await fetchUser();
console.log(user); // 유저 객체
// 여러 비동기 작업 병렬 처리
const [profile, settings, posts] = await Promise.all([
fetchProfile(),
fetchSettings(),
fetchPosts()
]);
// try-catch와 함께
let data;
try {
data = await fetchData();
} catch (error) {
data = {}; // 에러 시 기본값
}
// 선택적 체이닝 + 비동기
const email = await user?.getEmail?.(); // user가 없거나 메소드 없으면 undefined
객체 리터럴 단축 및 계산된 속성
객체를 생성하면서 값을 채워넣는 방식입니다.
단축 속성명 (Shorthand Property)
const id = "admin";
const name = "Alice";
const role = "manager";
// 전통적인 방식
const user1 = {
id: id,
name: name,
role: role
};
// 단축 속성명 (변수명 = 속성명일 때)
const user2 = { id, name, role };
// 혼합 사용
const user3 = {
id,
name,
role,
createdAt: new Date() // 다른 값 혼합 가능
};
console.log(user2.id); // "admin"
계산된 속성명 (Computed Property)
const prefix = "user";
const suffix = "key";
const storage = {
[prefix + "_" + suffix]: true, // "user_key": true
[`${prefix}_id`]: 12345, // "user_id": 12345
[Symbol.iterator]: function() { // 심볼 사용도 가능
return this;
}
};
console.log(storage["user_key"]); // true
console.log(storage.user_id); // 12345
메소드 단축
const obj = {
// 전통적인 방식
method1: function() {
return "result";
},
// 단축 메소드
method2() {
return "result";
},
// 화살표 함수는 단축 불가
method3: () => "result"
};
obj.method2(); // "result"
최신 기능 브라우저 지원
| 기능 | 도입 연도 | 비고 |
|---|---|---|
| 구조 분해 할당(Destructuring) | ES2015 (ES6) | 모든 주요 브라우저 지원 |
| 기본 매개변수(Default parameters) | ES2015 | 모든 주요 브라우저 지원 |
| 논리 할당 연산자 (Logical assignment)
( ||=,&&=,??=) |
ES2021 | IE 미지원 |
| 선택적 체이닝(Optional chaining)
( ?.) |
ES2020 | IE 미지원 |
| Null 병합(Nullish coalescing)
( ??) |
ES2020 | IE 미지원 |
| 계산된 속성명(Computed property names) | ES2015 | 모든 주요 브라우저 지원 |
성능 고려사항
- Short-circuit 활용: 논리 할당 연산자는 조건에 따라 우측 평가를 건너뜀으로 성능 최적화
- 구조 분해 오버헤드: 매우 큰 객체에서는 필요한 속성만 추출하기
- 재할당 주의:
const는 재할당 불가,let은 가능하지만 예측 가능한 범위에서만 사용
일반적인 실수 및 해결법
// 1. 구조 분해 시 기본값 누락
// 잘못된 예
const { timeout } = options; // options.timeout이 undefined면 timeout도 undefined
// 올바른 예
const { timeout = 5000 } = options;
// 2. 객체 구조 분해 선언 없이 시작
// 잘못된 예 (블록 문으로 해석됨)
{ url, method } = config;
// 올바른 예
({ url, method } = config);
// 3. 0이나 빈 문자열 기본값 처리
// 잘못된 예 (||= 사용)
settings.width ||= 100; // width: 0은 falsy라 100으로 덮어씀
// 올바른 예 (??= 사용)
settings.width ??= 100; // width: 0은 유지, undefined만 100으로 설정
// 4. BigInt와 일반 숫자 혼용
// 잘못된 예
let big = 10n;
big += 5; // TypeError
// 올바른 예
big += 5n;