기본형(Number, String, Boolean, Null, Undefined, Symbol): 불변값
참조형(Array, Function, Date, 정규형, Map, Set): 가변값
기본형을 제외한 것이 참조형이다.
저장 구조: 기본형은 변수 이름과 값이 메모리에 저장되며, 그 값 자리에는 실제 데이터(예: 1)가 담긴 주소가 바로 들어간다. 참조형은 한 단계를 더 거쳐서 값 자리에 들어가는 주소가 "메모리 영역"의 주소이고, 그 영역 범위 내에 프로퍼티별로 각각 또 주소값이 들어가며 그 주소에 실제 값이 저장된다.
재할당 시 동작: 기본형은 값이 바뀌면 새로운 값을 메모리 어딘가에 저장하고 변수 이름이 가리키던 주소값만 교체한다. 참조형은 가리키는 영역 주소 자체는 안 바뀌고, 그 내부의 주소값들만 바뀐다.
공유 여부: 참조형은 다른 변수가 같은 영역을 참조하거나 복사하면 한쪽을 수정해도 두 변수의 값이 전부 바뀐다. 새 객체를 할당한 경우에는 한쪽을 수정해도 서로 영향이 없다.
가변/불변: 기본형은 불변값, 참조형은 가변값이다.
Symbol은 유일한 값을 만들어내는 역할을 한다. Symbol()을 호출할 때마다 절대 겹치지 않는 새로운 값이 생성된다.
const a = Symbol();
const b = Symbol();
console.log(a === b); // false
생성자 함수가 아니라 원시값을 반환하는 함수이기 때문에 new를 붙이면 TypeError가 난다.
대괄호 표기법 사용: Symbol을 객체의 프로퍼티 키로 쓸 때는 반드시 대괄호 표기법을 써야 한다. 점 표기법(obj.key)은 오른쪽을 문자 그대로의 이름으로 해석하지만 대괄호 표기법(obj[key])은 안의 표현식(변수)을 평가해서 그 값을 키로 사용하기 때문이다.
const key = Symbol('age');
const obj = { [key]: 20 };
obj[key]; // 가능: 심벌 값 자체를 키로 찾음
obj.key; // "key"라는 문자열 프로퍼티를 찾음 → 접근 불가
열거되지 않는 특성: Symbol 키는 for...in 같은 일반적인 나열 방법에는 나타나지 않는다. 완전히 접근 불가능한 건 아니고 심벌을 알고 있으면 직접 값을 꺼낼 수 있다. 이 특성 덕분에 외부에 쉽게 노출되면 안 되는 내부용 프로퍼티를 만드는 용도로 쓸 수 있다.
Symbol.for(): 여러 파일·장소에서 같은 상수를 공유해야 할 때 사용한다. 전역 심벌 레지스트리에 문자열 키로 등록해두면 같은 문자열로 다시 호출했을 때 동일한 심벌을 다시 얻을 수 있다.
Symbol.for('ADD_TODO') === Symbol.for('ADD_TODO'); // true (같은 문자열 → 같은 심벌)
Symbol('ADD_TODO') === Symbol('ADD_TODO'); // false (일반 Symbol은 매번 새로 생성)
심벌을 언제 쓰는가:
// 1. 심벌을 만든다.
const nameKey = Symbol('name')
const student = {
// 2. 이름 값을 저장하는데, 키를 문자열 "nameKey"가 아니라
// 위에서 만든 심벌 값 자체로 사용한다 (대괄호 표기법 필수).
// → 외부에서는 이 키의 존재 자체를 알 수 없다 (Object.keys, for...in에도 안 나타남).
[nameKey]: '벨로그',
// 3. 이름을 "읽을 때" 쓰는 공식 통로 (getter).
// student.name 이라고 접근하면 이 함수가 대신 실행된다.
get name() {
// this[nameKey] → 심벌 키에 저장된 진짜 값을 꺼내온다.
return this[nameKey]
},
// 4. 이름을 "바꿀 때" 쓰는 공식 통로 (일반 메서드).
// student.setName(값) 형태로 호출한다.
setName(name) {
// this[nameKey] = name → 심벌 키에 새 값을 저장한다.
this[nameKey] = name
}
}
// 5. 이 객체를 다른 파일에서 가져다 쓸 수 있게 내보낸다.
// 주의: nameKey 자체는 export하지 않았으므로
// 다른 파일은 이 심벌이 존재하는지조차 알 수 없다.
export default student
import "./styles.css";
import Student from './student.mjs'
// 6. setName을 통해서만 값을 바꿀 수 있다.
// 내부적으로 student[nameKey] = 3 이 실행되지만,
// 이 파일은 nameKey가 뭔지 전혀 모른 채 그냥 메서드만 호출한다.
Student.setName(3)
document.getElementById("app").innerHTML = `
<h1>Hello JavaScript!</h1>
학생이름 : ${Student.name}
`;
// 7. Student.name → get name()이 실행되며 심벌 키의 값을 꺼내와 화면에 출력한다.
// (여기서는 3이 출력됨)
여러 곳에서 공유해야 하는 공용 키 → Symbol.for()
외부에서 접근 못 하게 숨기고 싶은 고유 키 → 일반 Symbol().
// 스프레드 연산자
const copy = { ...obj };
// Object.assign
const copy = Object.assign({}, obj);
// 배열은 slice()
const copyArr = arr.slice();
// structuredClone (요즘 가장 권장되는 방법, 내장 함수)
const copy = structuredClone(obj);
// JSON.parse(JSON.stringify()): 예전부터 쓰던 방법
const copy = JSON.parse(JSON.stringify(obj));
// 단, 함수/undefined/Symbol은 사라지고, Date는 문자열이 되고, Map/Set은 빈 객체가 되는 등 한계가 있음
// lodash의 cloneDeep: 위 한계를 신경 안 써도 되게 처리해주는 라이브러리
import cloneDeep from 'lodash/cloneDeep';
const copy = cloneDeep(obj);
중첩 객체가 없는 데이터라면 얕은 복사만으로 충분하고, 중첩 구조가 있어서 안쪽 값까지 완전히 별개로 만들어야 할 때만 깊은 복사가 필요하다.
var obj1 = {a: 1, b: 2}를 선언하면, 기본형과 동일하게 변수 이름 obj1과 값이 메모리 어딘가에 저장된다. 다만 이 값의 자리에 들어가는 주소값은 "메모리 영역"의 주소다.
메모리의 영역 범위 내에 a, b가 각각 저장되고 이 a, b의 값 자리에 다시 1, 2가 들어간 주소값이 들어가서 그 주소에 실제 값(1, 2)이 저장된다. 참조형의 프로퍼티에 또 참조형을 할당하는 경우에도 같은 방식이 반복된다.
기본형은 불변값이라서 값을 바꾸려면 기존 값을 직접 변경하는 게 아니라 새로운 값을 메모리 어딘가에 저장하고 변수가 가리키는 주소값만 바꾼다(재할당). 반면 참조형은 가변값이라서 식별자를 가리키는 메모리 주소는 그대로 둔 채 내부 프로퍼티의 주소들만 직접 바꿀 수 있다.
🔎 불변성을 어떻게 이용할 수 있을까?
원래 가변값인 참조형 데이터를 불변값처럼 다루고 싶을 때는 깊은 복사를 이용해 내부 프로퍼티까지 전부 복사하면 완전히 별개의 데이터가 된다. 이렇게 하면 같은 영역을 참조/공유해서 한쪽 수정이 다른 쪽에도 의도치 않게 반영되는 문제를 막을 수 있다.
한줄 요약: 강한 참조와 약한 참조
| 구분 | Map | WeakMap |
|---|---|---|
| 키 타입 | 기본형과 참조형 모두 가능 | 참조형 데이터만 가능 |
| 객체에 대한 참조 | 키 객체를 계속 참조함(참조 카운트 유지) | 키 객체의 참조 카운트를 증가시키지 않음(약한 참조) |
| 객체가 사라질 때 | 키로 남아 있으면 객체가 계속 메모리에 유지될 수 있음 | 다른 참조가 모두 사라지면 관련 정보도 가비지 컬렉팅될 수 있음 |
| 순회 | for...of, keys(), values(), entries() 가능 | 순회 불가 |
| 크기 확인 | size로 확인 가능 | size 없음 |
| 주된 용도 | 데이터를 보관하고 조회·순회하는 자료구조 | 객체에 종속된 부가 정보나 private 데이터 보관 |
🔎 WeakMap이 참조형만 키로 받는 이유
WeakMap은 키 객체가 더 이상 다른 곳에서 참조되지 않으면 그 객체와 연결된 값을 함께 정리한다. 문자열·숫자 같은 기본형은 객체처럼 생명주기를 기준으로 관리할 수 있는 대상이 아니기 때문에 키로 허용되지 않는다.
const wm = new WeakMap();
let obj = {};
wm.set(obj, '부가 정보');
obj = null; // 외부 참조가 사라지면 wm의 관련 정보도 GC 대상이 될 수 있음
🔎 약한 참조가 필요한 이유
일반 Map은 키로 넣은 객체를 계속 붙잡고 있어서 외부 변수가 사라져도 Map이 참조하고 있는 한 객체가 메모리에 계속 남을 수 있다. 객체가 많이 생성·소멸하는 상황에서 이런 참조가 쌓이면 메모리 누수로 이어질 수 있다. WeakMap은 참조 카운트를 올리지 않아서 객체를 가리키는 다른 참조가 모두 사라지면 관련 정보도 함께 자동 정리된다.
🔎 Map을 WeakMap으로 대체하면 안 되는 이유
둘은 대체 관계가 아니라 용도가 다르다. 전체 데이터를 계속 보관하며 순회·크기 확인이 필요하면 Map, 특정 객체가 살아있는 동안만 부가 정보를 연결하고 객체가 사라지면 정보도 같이 정리되길 원하면 WeakMap이 적합하다.
🔎 WeakMap을 순회할 수 없는 이유
키 객체가 언제 가비지 컬렉팅될지 외부에서 예측할 수 없기 때문이다. 만약 순회가 가능하다면 순회 도중 키 객체가 가비지 컬렉팅될 수 있고, 호출할 때마다 요소 개수가 달라지며, size나 전체 키 목록을 정확히 보장할 수 없게 되고, 외부에서 객체가 아직 남아있는지를 추적할 수 있게 되어 약한 참조라는 의미 자체가 약해진다. 그래서 WeakMap에는 for...of, keys(), values(), entries(), size가 아예 제공되지 않는다.
64비트를 넘는 숫자는 BigInt를 사용한다.
BigInt와 일반 Number는 타입이 달라서 서로 직접 연산이 안 된다.
값은 표현식이 평가되어 생성된 결과다. (예: 30)
리터럴은 사람이 이해할 수 있는 문자나 약속된 기호를 사용해 값을 생성하는 표기법이다. 30, 16진수 0x1E, 배열 [1, 2]가 모두 리터럴이며, 표기는 달라도 30과 0x1E는 같은 값을 만든다.
표현식은 값으로 평가될 수 있는 코드 조각이다. 리터럴도 표현식에 해당한다. 표현식은 변수에 할당이 가능하다.
문은 프로그램을 구성하는 기본 단위다. 문 중에는 표현식인 문과 표현식이 아닌 문이 있는데, 변수에 할당해보면 구분할 수 있다. 표현식이 아닌 문은 값으로 평가할 수 없으므로 에러가 난다.
변수는 하나의 값을 저장하기 위해 확보한 메모리 공간이나 그것을 식별하기 위한 이름이다. 변수 선언은 런타임 이전에 실행되고 할당과 평가는 런타임에 실행된다.
자바스크립트는 원시 값을 제외한 대부분이 객체로 이루어져 있고 선언 시 타입을 정하지 않아도 어떤 타입의 값이든 할당할 수 있는 동적 타입 언어다.
new 연산자 없이 String(), Number() 같은 생성자 함수를 쓰거나 toString(), parseInt() 같은 타입 변환 메서드를 쓰는 방식이다. 문자열 연결(1 + '')이나 단항 산술 연산자(+'10')로 의도적으로 타입을 바꾸는 방법도 있는데 이건 엄밀히는 암묵적 변환을 이용하는 방식이다.
좌항이 null이나 undefined일 때만 우항의 값을 반환하므로 기본값을 지정할 때 사용한다.