S1_U9_CH1. 참조 자료형

Judevv·2023년 5월 1일
0

Chapter 1. 참조 자료형

학습 목표

  • 원시 자료형(primitive data type)과 참조 자료형(reference data type)의 구분이 왜 필요한지 이해할 수 있다.
  • 원시 자료형과 참조 자료형의 차이를 이해하고, 각자 맞는 상황에서 사용할 수 있다.
  • 원시 자료형이 할당될 때는 변수에 값(value) 자체가 담기고, 참조 자료형이 할당될 때는 보관함의 주소(reference)가 담긴다는 개념을 코드로 설명할 수 있다.
  • 참조 자료형은 기존에 고정된 크기의 보관함이 아니라, 동적으로 크기가 변하는 특별한 보관함을 사용한다는 것을 이해할 수 있다.
  • 참조 자료형인 값을 복사하는 방법에 대해서 이해한다.

1-1. 원시 자료형과 참조 자료형

  • JavaScript에서 자료형(type)이란 값(value)의 종류
    • 각각의 자료형은 고유한 속성과 메서드를 가지고 있음
    • 자료형은 크게 두 가지로 구분함
      • 원시 자료형(primitive type)
        • number, string, boolean, undefined, null, symbol
      • 참조 자료형(reference type)
        • 배열, 객체, 함수

원시 자료형과 참조 자료형의 특징

  • 원시 자료형
    • 원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장
    • 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달
    • 원시 자료형은 변경 불가능한 값(immutable value) : 한번 생성된 원시 자료형은 읽기 전용(read only)
  • 참조 자료형
    • 참조 자료형을 변수에 할당하면 메모리 공간에 주소값 저장
    • 참조 값을 갖는 변수를 다른 변수에 할당하면 주소값이 복사되어 전달
    • 참조 자료형은 변경이 가능한 값(mutable value)

값 자체를 저장 vs 주소값을 저장

  • num이라는 변수를 선언하고 숫자 20을 할당했을 때 일어나는 일(원시 자료형)
let num = 20;
  • 변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보

  • 20이라는 원시 값을 그 공간에 저장

  • num이라는 이름의 저장 공간에 원시 값 20이처럼 원시 자료형은 값 자체를 저장

  • arr이라는 변수에 0부터 3까지의 숫자를 요소로 가지고 있는 배열을 할당했을 때 일어나는 일(참조 자료형)

let arr = [0, 1, 2, 3];
  • 배열의 요소 각각이 하나의 값이기 때문에 하나의 공간에 배열 자체를 저장하는 것은 불가능
  • 배열의 요소를 각각 하나의 공간에 저장한 후 같은 변수명을 부여하면?
    • 여러 개의 값이 저장되어 있는 공간에 같은 변수명이 부여되어 있으므로, 원하는 데이터를 조회하기 어려움
    • 배열의 요소나 객체의 프로퍼티는 추가 및 삭제가 수시로 일어나고, 정해진 개수가 없기 때문에 이와 같은 형태도 바람직하지 않음
  • 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장
  • 이때 참조 자료형을 저장하는 특별한 저장 공간을 힙(heap)이라고 부름
  • 변수 arr에 해당하는 저장공간에는 주소값이 저장되어 있고, 그 주소값을 통해 참조 자료형에 접근할 수 있음(이를 참조한다(refer)고 함)

원시 값 자체를 복사 vs 주소값을 복사

  • 원시 값 자체를 복사(원시 자료형)
    • 원시 자료형은 값 자체가 복사
    • 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가짐
let num = 20;
let copiedNum = num;

  • 주소값을 복사(참조 자료형)
    • 두 변수는 같은 주소를 가리킴
let arr = [0, 1, 2, 3];
let copiedArr = arr;

! 만일 원본을 변경한다면? !

  • 원시 자료형

    • 원본(num)에 다른 값을 재할당해도 복사본(copiedNum)에 영향을 미치지 않음
  • 참조 자료형

    • 원본(arr)을 변경하면 복사본(copiedArr)도 영향을 받음

변경 불가능한 값 vs 변경이 가능한 값

<원시 자료형>

  • 한 번 생성된 원시 값은 변경할 수 없음!!!
    • num이라는 변수를 선언하고 숫자 20을 할당
let num = 20;
  • 여기서 변수 num에 할당된 값을 숫자 20 대신, 다른 값으로 변경하려면, 다른 값을 변수에 재할당
num = 30;
  • 변수에 할당된 값이 20에서 30으로 변경되기 때문에 원시 자료형인 숫자 타입의 값이 변경된 것처럼 보임

  • 원시 자료형이 변경 불가능한 값이라는 것

    • num이라는 변수가 참조하던 공간에 들어 있던 20이 30으로 변경될 것 같지만, 메모리 내부에서는 이처럼 동작하지 않음!!!
    • 메모리 내부에서는 30이라는 원시 값을 저장하기 위한 새로운 공간을 확보한 뒤, 그 공간에 num이라는 이름을 붙이고 30을 저장
  • 변수에 다른 값을 재할당해도 원시 값 자체가 변경된 것이 아니라 새로운 원시 값을 생성하고, 변수가 다른 메모리 공간을 참조

  • 원시 자료형은 어떤 상황에서도 불변하는 읽기 전용 데이터!!! (원시 자료형이 높은 신뢰성을 가질 수 있는 요인)

  • 남아 있는 값 20

    • JavaScript 엔진은 이처럼 사용하지 않는 값을 자동으로 메모리에서 삭제 : 가비지 콜렉터(garbage collector)

<참조 자료형>

  • 참조 자료형은 변경 가능한 값!!!

    • 변수는 참조 자료형이 있는 저장공간(heap)의 주소값을 저장
  • 크기가 일정하지 않은 참조 자료형의 경우 매번 값을 복사한다면 그만큼 효율성은 떨어짐

    • 이런 이유로 참조 자료형은 변경이 가능하도록 설계
arr[3] = '3';
arr.push(4);
arr.shift();

console.log(arr); // [1, 2, '3', 4]
  • 위 코드가 실행되면, 변수가 참조하고 있는 주소에 저장되어 있는 값을 변경

  • 문자열은 원시 자료형이지만 배열처럼 인덱스로 문자열의 각 문자에 접근이 가능
console.log(str[0]) // 's'
console.log(str[2]) // 'a'
  • 하지만 배열과는 달리 인덱스에 직접 다른 문자를 할당하여 값을 변경할 수 없음!!!
    • 문자열도 원시 자료형이기 때문에 값을 변경할 수 없기 때문

1-2. 얕은 복사와 깊은 복사

  • 원시 자료형이 할당된 변수를 다른 변수에 할당하면 값 자체의 복사가 일어남
    • 원본과 복사본 중 하나를 변경해도 다른 하나에 영향을 미치지 않음
  • 참조 자료형이 할당된 변수를 다른 변수에 할당하면 주소가 복사되어 원본과 복사본이 같은 주소를 참조
    • 참조 자료형의 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미침
  • 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 수 없음

배열 복사하기

  • 배열을 복사하는 방법은 크게 두 가지
    • slice(), ES6 spread 문법

slice()

  • 배열의 내장 메서드
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
  • 새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다름
  • 주소가 다르기 때문에 복사한 배열에 요소를 추가해도 원본 배열에는 추가되지 않음
copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]

spread syntax

  • ES6에서 새롭게 추가된 문법
    • 배열을 펼칠 수 있음
      • 펼치는 방법은 배열이 할당된 변수명 앞에 ...을 붙임
      • 배열을 펼치면 배열의 각 요소를 확인 가능
let arr = [0, 1, 2, 3];

console.log(...arr); // 0 1 2 3
  • 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당한다면 참조 자료형이기 때문에 각각 다른 주소를 참조
let num = [1, 2, 3];
let int = [1, 2, 3];

console.log(num === int) // false
  • 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조하게 됨
  • 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작
let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false

copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]

객체 복사하기

Object.assign()

  • 객체를 복사하기 위해서 사용
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);

console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
![](https://velog.velcdn.com/images/judevv604/post/327bd10c-6f42-4b92-9c8b-0a2a509b130d/image.png)

spread syntax

  • 배열뿐만 아니라 객체를 복사할 때도 사용할 수 있음
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};

console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
  • 예외의 상황
    • 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없음!!!
    • 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사
  • 유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 users를 slice() 메서드를 사용하여 복사
let users = [
	{
		name: "kimcoding",
		age: 26,
		job: "student"
	},
	{
		name: "parkhacker",
		age: 29,
		job: "web designer"
	},
];

let copiedUsers = users.slice();
  • users와 copiedUsers를 동치연산자(===)로 확인해 보면 false가 반환
    • 각각 다른 주소를 참조하고 있기 때문
console.log(users === copiedUsers); // false

  • users와 copiedUsers의 0번째 요소를 각각 비교하면 true가 반환
    • users[0]과 copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문
console.log(users[0] === copiedUsers[0]); // true

  • slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사 : 얕은 복사(shallow copy)

깊은 복사

  • 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것을 깊은 복사(deep copy)라고 함

  • JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없음!

    • 하지만 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어낼 수 있음

JSON.stringify(), JSON.parse()

  • JSON.stringify()는 참조 자료형을 문자열 형태로 변환하여 반환

  • JSON.parse()는 문자열의 형태를 객체로 변환하여 반환

  • 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고, 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환

const arr = [1, 2, [3, 4]];
const copiedArr = JSON.parse(JSON.stringify(arr));

console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
  • 하지만 중첩된 참조 자료형 중에 함수가 포함되어 있으면 함수가 null로 바뀌기 때문에 예외는 존재!!!
const arr = [1, 2, [3, function(){ console.log('hello world')}]];
const copiedArr = JSON.parse(JSON.stringify(arr));

console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
console.log(copiedArr); // [1, 2, [3, null]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false

외부 라이브러리 사용

  • 완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치
  • lodash와 ramda는 각각 방법으로 깊은 복사를 구현
const lodash = require('lodash');

const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);

console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
profile
감성있는 개발자를 꿈꿔요

0개의 댓글