본문 바로가기

Frontend Study

[FE Bootcamp] 13일차_원시 자료형과 참조 자료형

오늘은 원시 자료형과 참조 자료형에 대해 공부하였다.

그동안 여러개의 언어를 배우며 수천개 이상의 정수, 문자열, 배열 등을 사용해 보았을 텐데,,..,,

이 녀석들이 이렇게 두개의 종류로 나뉘는지는 처음 알았다.

예전에 에브리타임에 '컴퓨터 과학 과목을 듣지 않고 코딩 테스트 준비를 해도 되나요?'라고 질문을 올렸던 적이 있는데

그 당시 익명의 선배님께서 굉장히 난감한듯한 표정(본적 없음)으로 'CS 지식 없이 코테 준비는 자살행위다!'라고 말씀 해 주셨었다.

지금 와서 생각해보니 역시 겪어본 자만이 해줄 수 있던 아주 큰 조언이었던 듯 하다,.,..,

 

여튼, 원시 자료형과 참조 자료형에 대해 알아보고, 이들을 복사하면 어떻게 되는지도 함께 알아보도록 하자.

 

1. 자료형

그전에, '자료형'이란 뭘까??  

자료형(type)이란, 어떤 값(value)의 종류를 말한다.

 

let num = 1
let str = "type"
let arr = [1,2,3]
let obj = { type:'object'}

 

우리는 그동안 변수에 값을 할당하여 사용해왔다. 이때, 변수에 할당되는 '값'들의 종류를 자료형이라 하는 것이다.

num의 자료형은 'number', str의 자료형은 'string', arr의 자료형은 'array', obj의 자료형은 'object'이다.

type 하니 생각나는 내용이 하나 있는데,..,.,,.바로 typeof 연산자.

 

console.log(typeof 1234)
//result = number
console.log(typeof '1234')
//result = string

 

typeof 연산자도 바로 변수나 값들의 자료형을 나타내주는 연산자였던 것이다.

이제 이 연산자들을 다시 두 종류로 나누어 보자.

 

A. 원시 자료형

원시 자료형이란, 값이 변하지 않는 자료형을 말한다. 

이들은 변수에 할당됨과 동시에 메모리에 값이 저장된다.

원시 자료형에는 'num, string, boolean, undefined, null' 5 종류만 존재한다.

 

let num = 1
let str = "hi"

console.log(num)
//result = 1
console.log(str)
//result = 'hi'

 

num str  
1 'hi'  
     
     
     
     

 

원시 자료형은 메모리에 '값 자체'를 담고 있기 때문에, 이들을 복사하면 메모리에 저장된 값을 그대로 복사해 다른 메모리에 저장하게 된다.

let num2 = num
let str2 = str

console.log(num2)
//result = 1
console.log(str2)
//result = 'hi'

 

num str  
1 'hi'  
     
     
num2 str2  
1 'hi'  

num2같은 경우에는 1이라는 값을 담고 있는 num을 복사하여 다른 메모리에 num2라는 이름으로 저장한 것이다.

 

그런데, 이런 경우를 보자

num = 2
str = 'bye'

console.log(num2)
//result = 2
console.log(str)
//result = 'bye'

 

 분명 원시 자료형은 '값이 변하지 않는 자료형'이라고 했는데, num과 str에 다른 값을 할당하니 값이 바뀌었다.

어떻게 된 일일까,.,..,?

 

     
1 'hi'  
  num str
  2 'bye'
num2 str2  
1 'hi'  

 

이는 기존 num과 str의 값이 바뀐 것이 아니라, 변경된 값을 다른 메모리에 할당 한 것이다. 

이 때문에 원시 자료형은 변하지 않는 자료형이라 하는 것이다.

이러한 특징들에 의해 원시 자료형은 읽기 전용이며, 높은 신뢰도를 가진다.

그리고, 원본이 변경되었다고 해도 복사본은 변경되지 않는다

복사된 순간부터, 이미 다른 메모리에 저장된 다른 값으로 취급되기 때문이다.

 

참고로, 기존에 num과 str에 할당되어 있던 1과 'hi'는 더이상 사용하지 않는 값이므로, 메모리가 자동으로 삭제하게 된다.

이를 '가비지 컬렉터'라고 한다.

 

B. 참조 자료형

참조 자료형은 원시 자료형과는 다르게 변경이 가능한 자료형이다. 

원시 자료형이 아닌 모든 자료형은 참조 자료형에 속한다.

대표적인 예로는 배열, 객체, 함수 등이 있다.

 

원시 자료형이 변수에 저장된 값을 메모리에 할당하는 것 처럼, 참조 자료형도 메모리에 값을 할당하는 것일까?

 

let arr = [1,2,3,1]

 

arr arr arr
1 2 3
arr    
1    
     
     

이런 경우를 보자. 만약 참조 자료형이 메모리에 값을 할당하게 된다면, 같은 변수 아래에 여러개의 값이 존재하므로 원하는 데이터를 찾기 매우 어려울 것이다. 

또한 값까지 같은 경우에는 내가 찾고 싶은 데이터와 변수명, 값은 같지만 저장된 메모리가 다른 값이 나오는 경우가 생겨 큰 혼란을 빚을 수도 있다. 

 

이를 해결하기 위해, 참조 자료형은 값이 아니라 '주소값'을 메모리에 저장한다. 

주소값은 또 다른 저장소인 힙(heap)을 참조하며, 참조 자료형의 값은 이 heap에 저장되어 있다.

 

arr    
주소값:10    
     
     
     
     
heap
10 : [1,2,3,1]

 

참조 자료형은 메모리에 '주소값'을 담고 있기 때문에 참조 자료형을 복사하면 주소값이 복사된다.

변수명은 다르더라도, 변수에 할당된 주소값은 같기 때문에, 두 배열은 같은 배열로 취급되고, heap에 저장된 값이 변하면 같은 주소값을 가진 변수들의 값도 모두 변하게 된다.

 

let arr2 = arr
arr2.push(5)

console.log(arr)
//result = [1,2,3,1,5]
console.log(arr2)
//result = [1,2,3,1,5]
console.log(arr1 === arr2)
//result = true

 

arr    
주소값:10    
  arr2  
  주소값:10  
     
     
heap
10 : [1,2,3,1,5]

 

참조 자료형이 어렵다면, RPG 게임을 예시로 들어 쉽게 이해할 수 있다.

게임을 하다 보면 가끔 이런 저런 사유로 업데이트를 해야 할 때가 있다.

예를 들어 이번 업데이트로 '전사' 직업의 '공격력'이 10씩 증가했다고 하자.

 

게임을 하는 수많은 유저들은 각자 ID가있고, 이 ID는 모두 다르다. 그리고 ID들은 서버에 저장되어 있다.

  • ID = 변수
  • 서버 = 메모리

한 게임에는 여러개의 직업이 있고, 이 직업들은 각각 다른 기본 능력치를 가진다.

  • 직업별 능력치를 저장해 둔 곳 = heap

수많은 직업별 능력치 중, '전사' 직업의 '공격력'을 증가시킨다

  • '전사' 직업 = heap에 저장되어 있는 주소값
  • 공격력 = '전사' 주소값에 저장되어 있는 실제 값

 

위의 RPG 게임을 코드와 메모리로 구현해 본다고 하자.

 

let warrior = {str : 8, dex:5, int:2}
let archer = {str : 4, dex:8, int:3}
let magician = {str : 2, dex:2, int:11}
let pirates = {str : 5, dex:5, int:5}
// 원본 객체(참조 자료형)

let kim = warrior
let zizon = pirates
let dudu = magician
let babo = pirates
let legolas = archer
let love = warrior
let bear = magician
//서버에 저장된 아이디와 직업

warrior.int += 2
//업데이트

console.log(warrior.int)
//result = 2
console.log(kim.int)
//result = 2
console.log(love.int)
//result = 2

 

kim zizon dudu
주소값:10 주소값:13 주소값:12
babo legolas love
주소값:13 주소값:11 주소값:10
bear    
주소값:12    
heap
10 = {str : 8, dex:5, int:2}  →  {str : 8, dex:5, int:4}
11 = {str : 4, dex:8, int:3}
12 = {str : 2, dex:2, int:11}
13 = {str : 5, dex:5, int:5}

 

console.log(warrior.int)
//result = 4
console.log(kim.int)
//result = 4
console.log(love.int)
//result = 4

 

이처럼 참조 자료형은 주소값만 같다면 실제 값의 변동도 동시에 적용된다는 것을 알 수 있다.

그런데, 만약 같은 값을 복사 하고 싶지만 주소값은 다르게 하고 싶다면 어떻게 할까?

 

2. 얕은 복사와 깊은 복사

주소값을 다르게 하기 위해서는, 여러가지 방법이 있다. 

배열과 객체의 복사 방법이 다르므로, 따로 알아보도록 하자.

 

A. 배열의 얕은 복사

배열을 복사하는 메서드는 slice, splice 등이 있다. 이중 slice 메서드를 이용해 보자.

let arr = [1,2,3,4,5]
let newArr = arr.slice()

console.log(arr)
//result = [1,2,3,4,5]
console.log(newArr)
//result = [1,2,3,4,5]

slice 메서드를 사용해도 두 배열의 실제 값은 똑같다. 그렇다면 주소값은 어떻게 될까?

 

arr    
주소값:10    
  newArr  
  주소값:13  
     
     
heap
10 : [1,2,3,4,5]
13 : [1,2,3,4,5]

 

놀랍게도, slice 메서드를 사용하면 기존 배열과 복사된 배열의 주소값이 달라진 것을 볼 수 있다.

 

arr.push(6)
console.log(arr)
//result = [1,2,3,4,5,6]
console.log(newArr)
//result = [1,2,3,4,5]
console.log(arr1 === arr2)
//result = false

 

두 배열의 주소값이 다르기 때문에, 두 배열은 다른 배열로 취급되고 한 배열의 값이 바뀌어도 다른 배열에는 영향을 끼치지 않는다.

slice 배열 외에도, 전개 연산자(...)를 사용할 수 있고, 객체의 경우 Object.assign()을 통해 복사가 가능하다.

 

그런데, 참조 자료형이 중첩된 경우에는 가장 바깥쪽에 있는 자료형만 다른 주소값으로 복사가 되고, 안에 들어있는 자료형들은 기존 자료형과 동일한 주소값을 갖게 된다.

  • 다차원 배열
  • value = 객체일 때
let arr = [1,2,[3,4]]
let newArr = [...arr]

 

arr    
주소값:54    
  newArr  
  주소값:58  
     
     
heap
54 : [1,2,주소값:60]
58 : [1,2,주소값:60]

60 : [3,4]

이처럼 가장 바깥쪽의 구조까지만 복사하는 것을 '얕은 복사'라고 한다. 

다만 내부 구조는 동일하므로, arrr와 newArr는 다르지만 arr[2]와 newArr[2]는 같게 된다.

 

B. 깊은 복사

그렇다면 내부의 구조까지 새로운 주소값으로 복사하려면 어떻게 해야 할까?

아쉽게도 자바스크립트 내부적으로는 깊은 복사를 할수 있는 방법이 없다.

하지만 다음의 방법을 사용하여 깊은 복사를 어느정도 해결할 수 있다.

  • JSON.stringify()와 JSON.parse()를 사용(참조 자료형 내부에 함수가 있는 경우 함수가 null로 바뀌는 문제)
  • node.js 환경에서 외부 라이브러리인 lodash나 ramda를 사용
const lodash = require('lodash');

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

console.log(arr); 
//result =  [1, 2, [3, 4]]
console.log(newArr); 
//result = [1, 2, [3, 4]]
console.log(arr === copiedArr) 
//result = false
console.log(arr[2] === copiedArr[2]) 
//result = false

 

arr    
주소값:54    
  newArr  
  주소값:58  
     
     
heap
54 : [1,2,주소값:60]
58 : [1,2,주소값:62]

60 : [3,4]
62 : [3,4]

 

 

3. 정리

  원시 자료형 참조 자료형
값 변경 여부 X O
복사되는 것 Value(값) 주소값
원본 변경시 복사본 변경 여부 X O

 

 

아주아주 긴 시간동안 자료형의 종류와 복사에 대해 알아보았다.

그동안 '원본 배열의 수정 없이'라는 조건을 많이 들었는데, 이게 이제서야 이해가 되는 기분이다.

조금 복잡하지만, 원리를 이해하니 머릿속에 쏙쏙 들어오는 것 같다.

그리고 학교 다닐때,.,.,.수업시간에 들은 가비지 컬렉션과 얕은복사, 깊은복사를 아직도 이해하지 못하고 있었는데

이번 수업을 통해 완벽히 이해한 것 같다,..,,.

교수님 죄송했습니다,..,,.!!!

 

'Frontend Study' 카테고리의 다른 글

[FE Bootcamp] 14일차_클로저와 ES6  (0) 2023.03.05
[FE Bootcamp] 13일차 _스코프  (0) 2023.03.02
[FE Bootcamp] 12일차_객체  (1) 2023.02.28
[FE Bootcamp] 11일차_배열  (0) 2023.02.28
[FE Bootcamp] 10일차_CLI와 Linux 기초  (0) 2023.02.27