코어 자바스크립트 - 01.데이터 할당

Featured image

현업에서 개발하며 사실 자바스크립트에 대해 체계적으로 공부해 본 적이 없다. 개발을 하면서 그때 그때 구글과 동료들과 익힌 지식으로 돌아가는 코드만 짜왔고 문제가 생기면 근본적인 원인을 찾기 보다는 이것 저것 바꿔보고 구글링하며 해결했다. 실질적인 원인보다는 단순히 “이렇게 하면 문제가 생기는구나”하고 배경지식 없는 경험만 쌓였다.

그러다 보니 익숙하고 체계적으로 배운 백엔드와는 다르게 프론트엔드 개발을 할때에는 항상 뭔가 찝찝한 기분이 들었다.

그래서 한번 차근차근 공부해보자 라는 생각으로 책 하나를 선정하여 읽기 시작했고 공부한 내용도 정리할 겸 블로그에 올릴 생각이다.

코어 자바스크립트 (정재남 저) 도서를 참고하였습니다.

01. 데이터 할당

1-1. 변수 선언

예제 1-1

var a;

위와 같이 변수 선언하는 방법은 기본적으로 알고 있겠지만, 방법이 아닌 동작 원리를 알아보고자 한다.

예제 1-1을 말로 풀어쓰면 “변할 수 있는 데이터를 만든다. 이 데이터의 식별자는 a로 한다” 가 된다.

결국 변수란 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇 이라고 생각할 수 있다.

1-2. 데이터 할당

예제 1-2

var a;        //변수 a 선언
a = 'abc';    //변수 a에 데이터 할당

var a = 'abc' //변수 선언과 할당을 한 문장으로 표현


표 1-1
표1-1

예제 1-2를 표현하면 아래와 같다.

  1. 변수 영역에서 빈 공간(@1003) 확보
  2. 확보한 공간의 식별자를 a로 지정
  3. 데이터 영역의 빈 공간(@5004)에 문자열 ‘abc’를 저장
  4. 변수 영역에서는 a라는 식별자를 검색(@1003)
  5. 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입



예제 1-3

var a = 10;
var b = 10;

예제 1-3의 경우에는 또 다른 변수 b를 더 선언했다. 그렇다면 b의 데이터 할당은 어떻게 될까? 여기서 중요한 것은 10이라는 값이 새로운 주소에 할당되고 b에 해당 주소가 대입되는 것이 아니라는 것이다. b는 a의 10과 같은 주소를 참조하고 새로운 데이터 영역을 더 만들지 않는다.

결국 자바스크립트 엔진은 예제 1-3을 실행할때 다음과 같은 절차를 거친다.

  1. 변수 영역에 빈 공간을 확보하여 식별자 a로 지정하고,
  2. 데이터 영역에서 10이 있는지 검색한 후 10을 찾지 못했기 때문에 새로운 주소에 10을 저장하고,
  3. a 식별자 주소에 10 데이터 주소를 대입한다.
  4. 변수 영역에 빈 공간을 확보하여 식별자 b로 지정하고,
  5. 데이터 영역에서 10이 있는지 검색한 후 10이 있는 것을 확인한 후,
  6. 해당 데이터 주소를 b 식별자 주소에 대입한다.

그렇다면 object의 경우는 어떻게 데이터 할당이 이루어질까?

예제 1-4

var obj1 = {
    a: 1,
    b: 'bbb'
};

표 1-2 표1-2

  1. 변수 영역의 빈 공간(@1003)을 확보하고, 그 주소의 이름을 obj1으로 지정
  2. 임의의 데이터 저장공간(@5002)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이루어진 데이터 그룹이다. 내부 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고, 그 영역의 주소(@7103~?)를 @5002에 저장
  3. @7103과 @7104에 각각 a와 b라는 프로퍼티 이름 지정
  4. 데이터 영역에서 숫자 1을 검색하고 결과가 없으므로 @5003에 저장하고 이 주소를 @7103에 저장
  5. 문자열 ‘bbb’ 역시 임의로 5004에 저장하고, 이 주소를 @7104에 저장

여기까지 문제없이 왔으면 전반적인 데이터 할당에 대한 내용은 이해한 것이다. 그러면 이제 자주 맞닥뜨리는 변수 복사 문제를 이해하고 해결할 수 있다.

1-3. 변수 복사 비교

예제 1-5

var a = 10;
var b = a;

var obj1 = {c: 10, d: 'ddd'}
var obj2 = obj1;

b = 15;
obj2.c = 20;

console.log(a == b);
console.log(obj1 == obj2);

예제 1-5의 결과를 예측하면 어떨까? 이제는 속지 않는다. 결과부터 얘기하면 false, true이다.

이러한 결과가 왜 나오는지 아래 표를 보면서 분석해보자.

표 1-3 표1-3

표 1-3과 같이 obj1과 obj2를 복사한 후 obj2.c의 값을 변경했을때, c의 주소만 @5001 => @5005로 변경된 것을 볼 수 있다.
obj1은 여전히 @5002를 참조하고 @5002는 c에 해당하는 @7103을 참조하고 있기 때문에 obj2.c = 20으로 인해 변경된 @7103을 똑같이 참조하여 변경된 c값을 들고 있는 것이다.

이러한 변수 복사 원리 때문에 개발자는 이 점을 숙지하고 필요에 따라 얕은 복사(shallow copy)와 깊은 복사(deep copy)를 자유로이 활용할 수 있어야한다.
아래 깊은 복사 예제 몇가지를 소개한다.

예제 1-6

// 재귀를 이용하여 모든 프로퍼티를 순회하여 복사하는 방법
var copyObjectDeep = function (target) {
    var result = {};
    if (typeof target === 'object' && target !== null) {
        for (var prop in target) {
            result[prop] = copyObjectDeep(target[prop]);
        }
    } else {
        result = target;
    }
    return result;
}

var obj = {
    a: 1,
    b: {
        c: null,
        d: [1, 2]
    }
};
var obj2 = copyObjectDeep(obj);

예제 1-7

// json을 이용한 방법
var copyObjectViaJson = (target) => {
    return JSON.parse(JSON.stringify(target));
};

var obj = {
    a: 1,
    b: {
        c: null,
        d: [1,2],
        func1: () => console.log(3),
        func2: () => console.log(4)
    }
};

var obj2 = copyObjectViaJson(obj);

obj2.a = 3;
obj2.b.c = 4;
obj.b.d[1] = 3;

console.log(obj);
console.log(obj2);

이 외에 외부 라이브러리를 이용한 방법 등이 있음.

1-4. undefined와 null

예제 1-8은 undefined를 반환하는 경우를 나열한 코드이다. undefined를 반환하는 경우는 아래와 같다.

  1. 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
  2. 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
  3. return문이 없거나 호출되지 않는 함수의 실행 결과

예제 1-8

var a;
console.log(a); // (1) undefined. 값을 대입하지 않은 변수에 접근

var obj = {a: 1};
console.log(obj.b); // (2) 존재하지 않는 프로퍼티에 접근

function func() {};
var c = func();
console.log(c); // (3) return 값이 없으면 undefined를 반환한 것으로 간주

그런데 위의 (1)처럼 값을 대입하지 않는 경우에 대해 배열의 경우 조금 특이한 동작을 확인할 수 있다.

예제 1-9

var arr1 = [undefined, 1];
var arr2 = [];
arr2[1] = 1;
arr1.forEach((v, i) => console.log(v, i));
arr2.forEach((v, i) => console.log(v, i));
console.log(arr1.map((v, i) => v+i));
console.log(arr2.map((v, i) => v+i));
console.log(arr1.filter((v, i)=>!v));
console.log(arr2.filter((v, i)=>!v));

예제 1-9의 결과들을 보면 재밌는 결과를 볼 수 있다. 직접 undefined를 할당한 arr1에 대해서는 일반적으로 알고 있는 대로 배열의 모든 요소를 순회해서 결과를 출력하지만 arr2에 대한 결과를 보면, 각 메서드들이 비어 있는 요소에 대해서는 어떠한 처리도 하지 않고 건너뛰었음을 알 수 있다.

특별한 현상 같지만, ‘배열도 객체’임을 생각해보면 지극히 자연스러운 현상이다.

결과적으로, 사용자가 명시적으로 부여한 경우와 비어있는 요소에 접근하려 할 때 반환되는 두 경우의 ‘undefined’의 의미를 구분할 수 있다. 전자의 undefined는 그 자체로 값이다. 후자의 경우는 자바스크립트 엔진이 하는 수 없이 반환해주는 값이며, 해당 프로퍼티 혹은 배열의 키값 자체가 존재하지 않음을 의미한다.

이 혼란을 피할 방법은 null을 사용하는 것이다. 같은 의미를 가진 null이라는 값이 별도로 있는데 굳이 undefined를 써야할 이유가 없다. ‘비어있음’을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 쓰면 된다.

여기까지 데이터 할당의 내용이었다. 다음 포스팅은 실행 컨텍스트에 대한 내용으로 찾아오겠다.