본문 바로가기
코드잇 스프린트 6기/위클리 페이퍼

[4주차] 얕은 복사 - 깊은 복사 / 원시 타입 - 객체 타입 / var-let-const / 호이스팅 / 스코프

by 학습하는 청년 2024. 3. 27.

4주차 위클리 페이퍼 과제

1. 자바스크립트에서 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)에 대해 설명해 주세요.

 

우선! '복사'라는 표현 때문에 헷갈릴 수 있다. 깊은 복사라고 하면 직관적으로 떠오르는 건, 깊게 복사한다는 의미처럼 다가오니까 모든 것을 복사할 것 같은 느낌으로 다가온다는 점이다. 하지만 아니다. 결론을 내리면 다음과 같다.

 

깊은 복사 : 주소값을 갖는 원리까지 복사하여 또 다른 주소값을 만드는 것

얕은 복사 : 주소값 자체를 복사하여 같은 주소값에서 내용을 변경하는 것

 

<스토리>

내가 음료에 빨대를 꽂아 마시고 있는 상황이다. 지나가던 친구가 그것을 보고 "나도 음료수 마시고 싶다"라고 하면서 자신도 같은 음료를 사와서 마신다면, 그것은 깊은 복사 - 같은 음료까지 복사(구입)했으므로 별개로 존재

 

그러나 당혹스럽게 빨대만 가져와서 내 음료에 꽂아 마신다면, 그것은 얕은 복사!!

 

깊은 복사는 '같은 음료'이지만 서로 다르기 때문에 한 명이 많이 마셔도 다른 사람의 음료의 양에는 변화가 없다. 구분되어 있기 때문이다. 그러나 얕은 복사는 같은 음료에 변수라는 두 빨대가 꽂혀 있으므로, 한 명이 많이 마시면 다른 한 명에게 남은 음료의 양(값)이 같아진다.


타입에 따라 얕은 복사와 깊은 복사가 결정된다.

데이터 타입은 크게 원시 타입(Primitive Type)객체 타입(Object Type)으로 나눌 수 있다.

  • 원시 타입 : Number, String, Boolean, Undefined, Null, Symbol
  • 객체 타입 : Object, Function, Array

 

원시타입

예를 들어, let message = "Hello!"; 와 같이 값과 변수가 선언되면 각각 특정 메모리 주소값이 부여된다. 값 "Hello!"에 대한 주소값을 A라고 하면, message라는 변수명은 A라는 주소값에 이름을 붙여준 것이다.

이런 상황에서 let phrase = message; 라고 선언하면, 다음과 같이 선언된 것이다.

let message = 40; // 주소값 A
let phrase = 40; // 주소값 A`

즉, A가 복사되어 A`를 만든 것이다. 이때 A`의 주소값은 A의 주소값과 다르다. 파일을 복사하면 안의 내용은 같지만 다른 파일이 생성되는 것과 같은 이치라고 생각하면 된다. 결국, 변수명 phrase는 또 다른 "Hello!"을 갖고 있는 주소값 A`에 대한 이름인 것이다.

 

이와 같이, 같은 값을 갖지만 다른 주소값이 부여되는 경우를 '값에 의한 전달(pass by value)'라고 한다. 이처럼 값을 복사함에 따라, 별도의 주소값이 생기는 원리까지 반영하는 것을 '깊은 복사(deep copy)'라고 한다.

 

이런 현상이 나타나는 값을 '원시 타입(primitive type)의 값', 즉 원시 값이라고 한다. 깊은 복사가 나타나는 이유는 원시 값의 특성에서 찾을 수 있다. 원시 값은 불변성(Immutability)를 갖는다. 값을 변경할 수 없다는 의미이며, 이는 데이터에 대한 신뢰성을 확보한다. 결국, 원시 값은 재할당만 할 수 있을 뿐이다.

let message = "Hello!"; // 주소값 A

message = "Hello!"; // 주소값 B

messagen= "Hello!"; // 주소값 C

이런 상황일 때, message라는 변수명은 A라는 주소값에서 B라는 주소값으로, 최종적으로는 C라는 주소값을 가리킨다. 즉, 값이 바뀌는 것처럼 보이지만, 사실은 값에 대한 주소값이 생기고 가리키는 주소값이 바뀌는 것이다.

 

그렇다면 더 이상 가리키지 않는 주소값 A와 B는 어떻게 될까?

이는 주소값을 가리키는 변수가 할당되지 않게 된 상황이므로, 가비지 컬렉터(GC, Garbage Collector)에 의해 자동으로 삭제된다. 가비지 컬렉터는 관리되지 않는 메모리를 정리해주는 역할을 수행한다.


객체 타입

let language1 = {
  name: 'JS'
}; // 주소값 A

let language2 = {
  name: 'JS'
}; // 주소값 B

console.log(Boolean(language1 == language2)); // false;
console.log(Boolean(language1 === language2)); // false;

이처럼 객체는 같은 내용을 갖고 있더라도, 별개의 주소값을 갖는다. 여기까지는 원시 타입과 다를 바가 없다.

 

let user = {
  name : 'JS'
}; // 주소값 A

let admin = user; // 주소값 A

==> 둘 다 같은 주소값을 나타낸다.

admin.name = 'TS';
console.log(user.name); // TS
console.log(admin.name); // TS

그러나 객체는 변경가능하도록 설계되었다는 점에서 다르다. 즉, 재할당을 통해 다른 값을 갖는 것처럼 보이는 것이 아니라 값 자체를 변경할 수 있다는 의미이다. 선엄함으로써 할당된 특정 주소값, 그 자체에서 내용을 변경하는 것이다. 특정 주소값을 참조하여 값의 변경사항을 전달하고 반영하는 것을, '참조에 의한 전달(pass by reference)'이라고 한다.

 

객체라는 변수에 할당된 특정 주소값, 그 자체만을 복사하기에 '얕은 복사(shallow copy)'라고 한다.

 

Q. 얕은 복사와 깊은 복사가 구분된 이유는 무엇일까?

깊은 복사는 비효율적이기 때문에 얕은 복사가 필요하다. 같은 복사를 한다고 했을 때, 깊은 복사를 하면 그만큼의 주소값이 생성된다. 그러나 얕은 복사를 하면 하나의 주소값을 활용하면 되므로 훨씬 효율적이다.

 

그러나 객체는 깊은 복사도 가능하다.

스프레드 문법을 통해 객체의 요소를 정렬하면 된다.

const javascript = {
  type: "language",
  tool: {
    library: "React",
    framework: "Next.JS"
  }
};

// 얕은 복사
const typescript = { ...javascript };
console.log(typescript === javascript); // false;
console.log(typescript.name === javascript.name); // true

// 얕은 복사를 했으므로
typescript.type = "type JS";
typescript.tool.library = "Vue";

console.log(javascript.type); // 'language'
console.log(typescript.type); // 'type JS' -- 얕은 복사를 했기 때문에 값이 바뀐 것
console.log(javascript.tool.library); // 'Vue'
console.log(typescript.tool.library); // 'Vue' -- 얕은 복사를 했어도, tool 자체가 객체이므로 깊은 복사로 인식

즉, 중첩된 객체일 경우에는 한 단계만 깊은 복사처럼 되고, 안에 있는 객체는 깊은 복사로 작동한다.


객체를 깊은 복사하는 방법

1. JSON.parse(JSON.stringify(obj))

JSON.stringify()는 객체를 json 문자열로 변환시키는 과정에서 원본 객체와의 통로(빨대)가 모두 끊어진다. 그런 상태에서, JSON.parse()를 이용하여 json 문자열을 객체로 변환한다.

 

그러나 중첩 객체인 경우에는 불가능하다. 뿐만 아니라, 복잡한 객체의 경우에는 불가능하다.

 

2. 커스텀 재귀 함수를 활용

원본 객체를 변경하지 않으면서 복사할 수 있다. 그러나 재귀함수를 만드는 것은 복합하다. 주의할 점은 언제 종료하는지에 대한 조건을 항상 작성해주어야 한다.

 

3. Lodash 같은 외부 라이브러리 사용

웹 개발을 하다 보면, Lodash는 흔히 사용하게 된다. 이미 사용하고 있었다면, _.cloneDoop(Object); 통해 깊은 복사를 손쉽게 할 수 있다.


2. var, let, const를 중복 선언 허용, 스코프, 호이스팅 관점에서 서로 비교해 주세요.

  var let const
중복 선언 O X X
재할당 O O X
스코프 함수 레벨 지역 스코프 지역 스코프
선언과 초기화 따로 가능 따로 가능(TDZ 존재) 반드시 동시
호이스팅 O O(블록 안에서) O(블록 안에서)

 

var

ES6를 사용한다면 var 키워드는 사용하지 않는다. 기존 var를 사용함으로써 발생했던 문제를 해결하기 위해  let과 const가 등장했기 때문이다.

 

1) 무엇보다 함수 레벨 스코프의 특징으로 인해, 함수 외부에서 선언한 변수들은 전역 변수가 된다. 또한, 함수 내부에 있는 블록 레벨 스코프에도 영향을 미친다. 이로 인해, 중복 선언되는 경우가 발생할 위험이 크다. 더 큰 문제는 중복 선언에 대해서 문법 오류로 인식하지 않는다는 점이다. 예상과 다른 결과를 마주할 수 있는 것이다.

var를 사용했을 때, 함수 레벨 스코프로 인해 중복선언 될 수밖에 없는 이유

 

2) 또한, 호이스팅(Hoisting)이 발생해 변수가 선언되기 전에 작성된 곳들에 적용되는 현상이 나타난다. 이는 var 키워드로 선언한 변수에서는 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 "선언"과 "초기화"가 한번에 진행되기 때문이다.

A = 9209;

console.log(A); // 9209

var A;

 

 

let

재할당이 필요한 경우에 let 키워드를 사용한다. 핵심 포인트는 '재할당'이라는 데 있다. 우선, 'var'와 달리 중복선언을 할 경우 '문법 에러'가 발생한다. 이때 변수의 스코프는 최대한 좁게 만드는 것이 좋다.

 

1) 변수 선언과 초기화 과정을 따로 할 경우, 자바스크립트 엔진은 var와 달리 변수 선언과 초기화 과정이 따로 작동한다. 그 사이에는 변수를 참조할 수 없다. 이를 일시적 사각지대(TDZ, Temporal Dead Zone)라고 한다.

 

2) let 으로 선언한 변수도 호이스팅 문제가 발생한다. 그러나 블록레벨 스코프이기 때문에 블록 스코프 안에서만 발생한다.

let A = 4; // 전역 변수
{
  console.log(A); // ReferenceError: Cannot access 'A' before initialization
  let A = 2; // 지역 변수
}

위와 같은 상황일 때, let A가 2로 재할당이 되기 직전이므로 console.log(A) 값은 4가 출력된다고 예상할 수 있다. 그러나 '참조 에러'가 발생한다. 즉, 중복 선언됐다고 인식하는 것이다. 즉, 결국 let으로 선언한 변수는 블록 스코프 안에서 호이스팅이 일어난다고 말할 수 있다. let A 대신 A = 2라고 선언하면, console.log(A)는 재할당 직전의 값( = 4)를 출력한다.

 

const

상수를 선언하기 위해 사용하는 키워드. 하지만 반드시 상수만을 위해 사용하지는 않는다.

 

1) const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 한다. 그렇지 않으면 문법 에러가 발생한다.

2) 재할당이 금지되어 있다. 재할당 하려고 할 경우, 문법 에러가 발생한다.

3) 일반적으로 상수는 대문자로 선언하여 명확히 나타낸다. 여러 단어로 이러어진 경우 언더스코어( _ )로 구분하여 스네이크 케이스로 표현한다.

 

4) const로 선언한 원시값을 변경할 수 없지만, 객체를 할당한 경우에는 값을 변경할 수 있다. 객체는 재할당 없이도 직접 변경이 가능하기 때문이다. 즉, const 키워드는 재할당을 금지할 뿐 "불변"을 의미하지는 않는다.

const language = {
  name: 'JavaScript'
};

language.name = 'TypeScript';
console.log(language); // { name: 'TypeScript' }

참고 자료

모던 자바스크립트 Deep Dive

- 얕은 복사, 깊은 복사 (p.137-153)

- var, let, const (p.208-218)

 

얕은 복사 깊은 복사

1. 라매 개발자
https://youtu.be/JtrOxaTvOEM?si=njTjWFx6mQvBQ_v7

 

2. 얕은 복사 깊은 복사에 대한 정보

https://ko.javascript.info/object-copy

 

3. 깊은 복사 - 얕은 복사에 대한 최고의 설명(3분 46초까지!)

https://youtu.be/a7f38bKmQog?si=oOxsXTDO-NckVD9q

 

4. 객체의 깊은 복사 방법

https://developer.mozilla.org/ko/docs/Glossary/Deep_copy

https://chaewonkong.github.io/posts/js-deep-copy.html

댓글