bh2980.dev

JS의 데이터 타입과 메모리 관리

  • #Frontend
  • #JavaScript

null과 undefined

nullundefined 모두 '값이 없다'라는 의미를 담고 있지만, 그 쓰임새는 조금씩 차이가 존재한다.

undefined는 자바스크립트 엔진이 변수 초기화 시 자동으로 할당하는 값이다. 반면 null은 개발자가 의도적으로 '값이 없음'을 표현하기 위해 할당하는 값이다.

이 둘은 느슨한 비교(==)에서는 같게 취급되지만, 엄격한 비교(===)에서는 다르게 취급된다.

Truthy와 Falsy 값

자바스크립트 엔진은 조건문이나 논리 연산자와 같이 불리언 값을 평가되어야 할 문맥에서 불리언 타입이 아닌 값을 암묵적 타입 변환을 통해 불리언 값으로 다룬다. 이때 참으로 평가되는 값을 Truthy, 거짓으로 평가되는 값을 Falsy라고 부른다.

대표적인 Falsy 값은 아래 7개가 있다. Truthy 값은 Falsy 값을 제외한 모든 값이다.

  1. false
  2. undefined
  3. null
  4. 0, -0
  5. NaN
  6. ""(빈 문자열)

null 병합 연산자 ??

논리 합 연산자(||)을 사용할 경우 좌항의 피연산자가 Falsy 값이기만 하면 우항의 값을 반환한다. 반면 null 병합 연산자인 ??를 사용하면 좌항의 피연산자가 null, undefined일 경우에만 우항의 값을 반환한다.

0 || '기본값'  // '기본값' (0을 Falsy로 취급하여 우항 반환)
0 ?? '기본값'  // 0 (0을 유효한 값으로 취급하여 좌항 반환)

부동 소수점의 연산 오차

자바스크립트에서 0.1 + 0.2 === 0.3의 결과는 false이다. 그 이유는 부동소수점 형식의 한계에 있다.

자바스크립트에서 모든 숫자는 IEEE 754 표준을 따르는 64비트 부동소수점 형식으로 저장된다. 그러나 이 방식은 일부 소수점 연산에서 정확한 값을 저장하지 못하는 한계를 갖는다.

예를 들어 0.1과 0.2는 이진수로 변환하면 무한 반복되는 소수로 표현되므로, 컴퓨터는 이를 근사값으로 저장한다. 이로 인해 결과적으로 0.1 + 0.20.30000000000000004가 되는데, 이는 0.3과 일치하지 않기 때문이 === 연산자 사용 시 false가 된다.

이를 해결하기 위해서는 아래와 같이 두 가지 해결 방법이 있다.

1. toFixedtoPrecision함수를 이용해 특정 자릿수까지 반올림하는 방법. 단, 숫자가 문자열로 변환된다는 단점이 있다.

2. Number.EPSILON을 활용해 비교하는 방법. Number.EPSILON은 1과 1보다 큰 수 중 가장 작은 수 사이의 차이, 즉 부동소수점 연산의 허용 오차 기준값이다.

function isEqual(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}

console.log(isEqual(0.1 + 0.2, 0.3)) // true;

매개변수 전달 방식

Call by *함수를 호출할 때 매개변수를 어떤 방식으로 전달하느냐를 의미하는 용어이다.

Call by Value

함수를 호출할 때 값 자체를 복사해서 전달하는 방식. JS에서는 원시 타입이 Call by Value로 전달된다.

Call by Reference

함수를 호출할 때 값이 저장된 메모리 주소(참조값)를 직접 전달하는 방식. 따라서 함수 내에서 매개변수를 재할당하면 원본도 함께 변경된다. JS는 이 방식을 지원하지 않는다.

Call by Sharing

함수를 호출할 때 값이 저장된 메모리 주소(참조값)를 복사해서 전달하는 방식. JS에서 객체와 같은 참조 타입이 이와 같은 방식으로 전달된다.

function updateTeam(count, members, leader) {
    // 1. 원시 타입(숫자) 재할당 시 이는 복사된 값이기 때문에 영향을 미치지 않음
    count = 20; 

    // 2. 참조 타입(배열)의 내부 요소 수정 시 원본 주소값은 유지한 채 내부 요소를 수정하는 것이기 때문에 원본 값에 영향을 미침.
    members.push('Park'); 

    // 3. 참조 타입(객체) 자체를 새로운 객체로 재할당하는 것은 매개변수가 가리키는 주소값만 변경할 뿐 원본값이 가리키는 주소값은 변하지 않아 원본값에 영향을 미치지 않음.
    leader = { name: 'Choi', role: 'Senior' }; 
}

let memberCount = 10;
let memberList = ['Lee', 'Kim'];
let teamLeader = { name: 'Jung', role: 'Junior' };

updateTeam(memberCount, memberList, teamLeader);

console.log(memberCount); // 10
console.log(memberList);  // ['Lee', 'Kim', 'Park']
console.log(teamLeader);  // { name: 'Jung', role: 'Junior' }

메모리 관리

메모리 할당

자바스크립트에서 메모리의 할당은 변수나 객체를 생성 시 발생한다.

문자열, 숫자와 같은 원시 값은 고정 크기로 Stack 영역에 저장되기 때문에 컴파일 타임에 그 크기를 알 수 있어 정적 데이터라고 불린다.

반면 객체는 Heap 영역에 저장되고, 실행 시점에 필요한 만큼 메모리가 동적으로 할당되기 때문에 동적 데이터라고 불린다.

메모리 해제

메모리 해제는 할당된 메모리가 더 이상 필요없을 때 발생한다. 자바스크립트는 C언어와 달리 가비지 컬렉션(GC)라는 자동 메모리 해제 방법을 사용한다. 이를 언어 차원에서 메모리 할당을 추적하고, 특정 메모리가 필요하지 않게 되었을 때 메모리를 해제하는 방식을 말한다.

대표적인 가비지 컬렉션 알고리즘에는 Reference-counting과 Mark-and-sweep 알고리즘이 있다.

  • Reference-counting : 객체가 참조되는 횟수를 추적하고 참조 횟수가 0이 되면 해제하는 방식. 순환 참조 발생 시 해제되지 않는 문제가 있다.
  • Mark-and-sweep : 루트 노드부터 타고 내려가며 검색되지 않는 객체를 찾아 청소하는 방식. 순환참조인 경우에도 검색되지 않으므로 메모리를 해제할 수 있다.

메모리 누수

JS에서 메모리 누수가 일어나는 대표적인 경우는 아래와 같다.

  1. 이벤트 리스너를 해제하지 않은 경우
  2. 외부 스코프의 큰 변수를 클로저가 계속 참조하고 있는 경우
  3. 전역 변수를 과도하게 사용하는 경우

불변성

불변성은 데이터가 최초로 생성된 이후 그 상태를 변경할 수 없는 성질을 말한다. JS에서 객체와 배열은 참조 타입으로 기본적으로 가변적이므로 객체의 프로퍼티 값을 직접 변경할 수 있다. 만약 불변성을 유지하고 싶다면, 객체의 프로퍼티를 직접 변경하지 않고 새로운 객체를 생성하는 방식으로 사용해야한다.

불변성을 유지하면 성능 면에서 불리할 것 같아보이지만 이는 일반적으로 무시할만한 수준이다. 불변성을 유지하면 데이터의 변경은 항상 새로운 객체 생성을 통해서만 이루어지기 때문에 데이터의 변경 흐름을 추적하기 쉬워 유지보수성과 안정성을 향상시킨다는 장점이 있다.

정적 타입 언어와 동적 타입 언어

정적 타입 언어는 컴파일 시점에 변수의 타입이 확정되며, 타입 오류를 사전에 검출한다. 타입을 명시하거나 추론을 통해 확정한다. 대표적인 언어로 C, Java, TypeScript가 있다.

동적 타입 언어는 런타임 시점에 타입이 결정되며, 같은 변수에 다른 타입의 값을 자유롭게 할당할 수 있다. 대표적인 언어로 JavaScript, Python이 있다.