티스토리 뷰

00. TL;DR

  • 자바스크립트의 값은 원시 값(Primitive Value)참조 값(Reference Value) 으로 나뉨
  • 이 둘은 메모리에 저장되는 방식, 비교 방식, 복사 방식에서 차이점이 존재함

01. 원시 값(Primitive Value)

01.01. 불변(Immutable)이란?

불변이란, 값을 한 번 생성하면 변경할 수 없는 성질을 의미합니다.

let a = 10;
a = 20; // 새로운 값으로 재할당한 것이지, 기존 값(10)을 변경한 것이 아님

값을 바꾸는 것이 아니라 새로운 값을 생성해서 변수에 할당하는 것입니다.

01.02. 메모리 구조: 스택과 값 자체 저장

원시 값은 스택(Stack)에 값 자체가 저장됩니다.

Stack:
 a ──▶ 10
 b ──▶ 10 (복사된 값)

여기서 a와 b는 각각 서로 다른 메모리 주소에 10이라는 값을 복사해서 저장하고 있습니다.

01.03. 왜 복사가 일어날까? (값에 의한 전달)

원시 값은 불변성(immutable)을 가지며, 변수에는 값 자체가 저장됩니다.
이러한 특성 때문에 원시 값을 다른 변수에 할당하거나 함수의 인자로 전달할 때는 값이 복사되어 전달됩니다.
이를 값에 의한 전달(Pass by Value)이라고 합니다.

let x = 5;
let y = x; // x의 값 5가 y에 복사됨
y = 10; // y를 변경해도 x는 변하지 않음

console.log(x); // 5 (변경 없음)
console.log(y); // 10

위 예제에서 x의 값이 y에 복사되었기 때문에, y를 변경해도 x에는 아무런 영향을 주지 않습니다.
즉, 원시 값은 항상 복사 기반으로 전달되며 독립적인 값을 유지합니다.

01.03.01. 왜 복사 방식으로 설계되었을까?

단순히 "참조로 전달하면 불변성이 깨지니까 안 돼"가 아니라, 다음과 같은 언어 설계 차원의 이유가 존재합니다.

  1. 작고 고정된 크기이기 때문에 복사가 효율적이다
    대부분의 원시 값은 숫자, 불리언 등 고정된 크기의 데이터이며, 스택에 직접 저장되고 빠르게 복사할 수 있습니다.
  2. 참조 공유는 코드의 예측 가능성을 낮춘다
    만약 원시 값이 참조로 전달된다면, 한 쪽의 변경이 다른 변수에도 영향을 주게 됩니다.
    → 이런 동작은 버그의 원인이 되고, 테스트/디버깅이 매우 어려워집니다.
  3. 참조를 쓸 필요가 없다
    참조는 주로 객체처럼 복잡하고 용량이 큰 구조에서 메모리를 절약하기 위해 사용됩니다.
    원시 값은 대부분 작고 단순하므로, 굳이 참조를 사용할 이유가 없습니다.

📌 따라서 원시 값은 복사를 통해 전달되도록 설계되었으며, 이는 성능, 안정성, 예측 가능성을 모두 고려한 언어 차원의 선택입니다.

01.04. 원시 값의 종류

원시 값은 모두 스택(Stack) 메모리에 직접 저장되며, 각 자료형은 크기나 표현 방식에 따라 고유한 방식으로 메모리에 할당됩니다.

또한, 원시 값은 불변이기 때문에 값이 바뀌면 새로운 메모리 공간이 할당됩니다.

01.04.01. Number

  • 정수와 실수를 모두 표현하며, 64비트 부동소수점(IEEE 754) 방식으로 저장됩니다.
  • 값 자체가 스택 메모리에 8바이트로 저장되며 복사 및 처리 속도가 빠릅니다.
[변수: score] ──▶ [메모리 주소: 0x1000] ──▶ [값: 95]
const score = 95;

⚠️ 정밀도 문제

console.log(0.1 + 0.2); // 0.30000000000000004
  • 일부 실수는 이진수로 정확히 표현할 수 없어 계산 오차가 발생합니다.

해결 방법 예:

(0.1 + 0.2).toFixed(2); // "0.30"
Math.round((0.1 + 0.2) * 100) / 100; // 0.3

⚠️ 안전한 정수 범위

  • 자바스크립트는 안전하게 표현할 수 있는 정수의 범위가 존재합니다.
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MIN_SAFE_INTEGER; // -9007199254740991
console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // ⚠️ 여전히 9007199254740992 (정확하지 않음)
  • 큰 정수를 다룰 땐 BigInt 사용 권장

01.04.02. String

  • 문자열은 유니코드 문자들의 시퀀스이며, 길이에 따라 동적으로 메모리 할당됩니다.
  • 문자열은 불변이며, 수정 시 새로운 메모리 공간이 생성됩니다.
[변수: name] ──▶ [메모리 주소: 0x2000] ──▶ [값: "Odin"]
const name = "Odin";

01.04.03. Boolean

  • true 또는 false 두 가지 상태만 가지며, 1비트 또는 1바이트로 표현됩니다.
[변수: isActive] ──▶ [메모리 주소: 0x3000] ──▶ [값: true]
const isActive = true;

01.04.04. null

  • 내부적으로는 null도 특정 고정된 메모리 패턴으로 처리되며, 참조가 없음을 명시하는 특별한 값입니다.
[변수: value] ──▶ [메모리 주소: 0x4000] ──▶ [값: null]
const value = null;

01.04.05. undefined

  • undefined변수는 선언되었지만 값이 없음을 의미하며, 고정된 식별값으로 저장됩니다.
[변수: notDefined] ──▶ [메모리 주소: 0x5000] ──▶ [값: undefined]
let notDefined;

01.04.06. BigInt

  • BigInt정수 크기에 제한 없이 정수를 표현할 수 있는 자료형입니다.
  • 숫자가 매우 클 경우 Number 대신 사용되며, 동적으로 메모리 할당됩니다.
[변수: big] ──▶ [메모리 주소: 0x6000] ──▶ [값: 123456789012345678901234567890n]
const big = 1234567890123456789012345678901234567890n;

01.04.07. Symbol

  • Symbol은 고유하고 변경 불가능한 식별자를 생성하는 자료형입니다.
  • 주로 객체의 프로퍼티 키로 사용되며, 메모리에는 설명과 고유 식별자가 함께 저장됩니다.
[변수: unique] ──▶ [메모리 주소: 0x7000] ──▶ [값: Symbol("id")]
const unique = Symbol("id");

01.05. 문자열은 왜 불변일까? (메모리 관점에서)

문자열은 문자 하나하나가 조각처럼 연결되어 저장되며, 수정 시 전체 문자열을 새로 생성합니다.

let greeting = "Hello";
greeting[0] = "h";
console.log(greeting); // Hello

01.05.01. 문자열은 어디에 저장될까?

대부분의 자바스크립트 엔진에서는 문자열은 힙(Heap) 에 저장됩니다.
길이가 가변적이고 불변 특성을 갖기 때문에, 고정 크기 스택보다 힙에 저장하는 것이 일반적입니다.

다만, 문자열 리터럴(String Literal)은 내부적으로 문자열 상수 풀이나 캐시 메모리처럼 관리되기도 합니다.

Stack:
 str ──▶ 0x1001

Heap:
 0x1001 ──▶ "Hello"

String Buffer란?

문자열이 불변이기 때문에, 문자열을 자주 수정하거나 연결할 경우 매번 새로운 문자열 객체가 생성됩니다.

이를 최적화하기 위해 자바스크립트 엔진은 내부적으로 문자열 결합 시 임시 버퍼처럼 문자열을 누적하여, 최종 결과만 한 번에 메모리에 할당합니다.

JavaScript 는 명시적인 StringBuffer는 없지만, 엔진이 문자열 결합 시 성능을 위해 유사 구조를 사용하고 있습니다.

// 문자열 결합 예 (자바스크립트)
let result = "";
for (let i = 0; i < 1000; i++) {
  result += i; // 내부적으로 최적화
}

02. 참조 값(Reference Value)

02.01. 가변(Mutable)이란?

참조 값은 내부 상태를 자유롭게 변경할 수 있는 값입니다.

const obj = { name: "Odin" };
obj.name = "Soo";
console.log(obj.name); // Soo

02.02. 메모리 구조: 스택(참조) + 힙(값)

참조 값은 스택에 참조 주소가 저장되고, 힙(Heap)에 실제 값이 저장됩니다.

Stack:
 obj1 ──▶ 0x001
 obj2 ──▶ 0x001

Heap:
 0x001 ──▶ { name: 'Soo' }

두 변수는 같은 힙 메모리 주소를 참조하고 있으므로, 둘 중 하나의 내부 속성을 변경하면 다른 쪽에서도 변경된 값을 확인할 수 있습니다.

02.03. 참조 값의 종류

02.03.01. Object

const user = { name: "Odin", age: 30 };

02.03.02. Array

const items = [1, 2, 3];

02.03.03. Function

function greet() {
  console.log("Hello");
}

02.04. 값 복사의 두 방식

02.04.01. 얕은 복사(Shallow Copy)

한 단계까지만 복사하며, 내부 객체는 여전히 같은 참조를 가집니다.

const original = { name: "Soo", info: { age: 30 } };
const copy = { ...original };
copy.info.age = 40;
console.log(original.info.age); // 40

02.04.02. 깊은 복사(Deep Copy)

모든 하위 객체까지 재귀적으로 복사합니다.

const deepCopy = JSON.parse(JSON.stringify(original));

⚠️ 순환 참조가 있는 객체는 위 방법으로 복사하면 오류 발생할 수 있음.


03. 값의 비교 방법

03.01. 원시 값의 비교 (값 비교)

const a = 10;
const b = 10;
console.log(a === b); // true

03.02. 참조 값의 비교 (참조 주소 비교)

const obj1 = { name: "Soo" };
const obj2 = { name: "Soo" };
console.log(obj1 === obj2); // false

const obj3 = obj1;
console.log(obj1 === obj3); // true

03.03. 깊은 비교를 위한 도구 소개 (lodash.isEqual, structuredClone 등)

import isEqual from "lodash/isEqual";

const a = { x: 1, y: { z: 2 } };
const b = { x: 1, y: { z: 2 } };
console.log(isEqual(a, b)); // true
const cloned = structuredClone(a);
console.log(cloned === a); // false
console.log(isEqual(cloned, a)); // true

structuredClone은 깊은 복사 기능을 제공하지만, 함수, Symbol, DOM 요소 등 일부 복잡한 값은 복사되지 않으며 오류가 발생할 수 있습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함