Language/JS(Node.js)

[memory] Javascript에서 Swap 하는 방법 (+ 얕은 복사와 깊은 복사)

Joonfluence 2022. 1. 2.

들어가기에 앞서

혹시 두 개념을 들어본 사람이 있을지 모르겠습니다. 아마 처음 듣는 사람이라면 어떤 개념인지 와닿지 않을 수 있습니다. 그러나 C언어와 같이 low level language를 학습해본 사람이라면 대략적으로 어떤 개념인지 유추해볼 수 있을 겁니다. 이는 Call by ValueCall by Reference의 차이와도 비슷하기 때문이죠. 또한 이는 함수형 프로그래밍에서 중요하게 다루는 Mutable/Immutable 개념과도 관련이 있으니, 오늘 다 같이 공부해봅시다.

C언어에서의 메모리 접근 방법

둘을 비교하기 위해, 두 값을 서로 바꿔야만 하는 상황이 있다고 가정해봅시다. 가장 먼저 떠오르는 방법은 swap이란 함수를 새롭게 만들고, 파라미터로 서로 변환하고자 하는 두 값을 파라미터로 받아와 함수 내에서 임시변수에 값을 일시적으로 저장하여 처리하고자 할 것입니다. 아래와 같이 말이죠.

#include <stdio.h>

void swap(int a, int b) {
  int temp = a;
  a = b;
  b = temp;
}

int main(){
    int a, b;
    a = 83, b = 23;

    printf("before swap=> a: %d, b: %d \n", a, b);  
    swap(a, b);
    printf("after swap => a: %d, b: %d \n", a, b);

    return 0;
}


// 출력결과 : 83  23
// 출력결과 : 83  23

그러나 이 방법은 적용되지 않습니다. main 함수의 변수 a와 b가 아닌 swap 함수 내의 지역변수 a와 b를 변환하는 것이기 때문입니다. 위와 같은 방식은 새로운 변수가 생성되고 이에 복사된 값이 전달되는 방식인 깊은 복사 방식이 적용됩니다. 다른 언어에서는 call by value 방식으로 더 잘 알려져있죠.

그러면 이와 같은 경우에는 어떻게 해야 할까요? 간단한 방법이 있습니다. C/C++을 배웠던 분들이라면 포인터라는 개념에 대해 익숙하실 겁니다. 해당 개념을 처음 접하는 분들을 위해 설명하자면, 포인터 변수는 자신이 가리키는 변수 값을 메모리 주소로 보고 해당 메모리 주소 값을 갖습니다. 그리고 * 연산자를 통해 값에 접근할 수 있습니다. 또한 이는 얕은 복사 방식이 적용됩니다. call by reference 방식으로 더 잘 알고 계실 겁니다.

#include <stdio.h>

void swapByRef(int *a, int *b){
    printf("%d, %d \n", *a, *b);
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(){
    int a, b;
    a = 83, b = 23;

    printf("memory address : %d, %d \n", &a, &b); // &변수 => 변수의 메모리 주소를 나타냅니다. 

    printf("before swapByRef=> a: %d, b: %d \n", a, b);
    swapByRef(&a, &b);
    printf("after swapByRef => a: %d, b: %d \n", a, b); 

    return 0;
}

// 출력 결과 : 83 23
// 출력 결과 : 23 83

위의 코드와 그림을 보면, & 연산자를 통해 변수가 저장된 메모리 주소를 알 수 있습니다. 또한 이는 swapByRef 함수에서 *(별표를 포인터라고 합니다)a*b로 받아옵니다. * 연산자를 통해, 해당 값의 메모리 주소이 가리키는 값에 접근합니다. 이처럼 메모리 주소를 참조하여 값에 복사해오는 방식을 얕은 복사라고 합니다. 그리고 메모리 주소에 직접 접근함으로써 값이 정상적으로 변경된 것을 알 수 있습니다.

자바스크립트의 메모리 접근 방법

원시 값과 참조

그렇다면 메모리 주소에 접근할 수 없는 자바스크립트에서는 값을 swap 할 수 없는 걸까요? 자바스크립트는 기본적으로 call by vale, 즉 복사에 의해 값이 전달되는 방식입니다. 그러나 타입에 따라 C언어에서 포인터 연산자를 사용한 것과 같이 메모리 주소를 통해 값에 접근할 수 있습니다. 원시타입(Number, String, Boolean, Null, Undefined, Symbol, Bigint)의 경우, 복사에 의해 값이 전달됩니다. 이는 곧 함수의 파라미터로 값을 전달할 때, 새로운 메모리 주소에 변수를 할당하는 것을 의미합니다. 그러나 참조타입(Object)의 경우, 새롭게 변수를 메모리 상에서 생성하지 않고 기존 메모리 주소를 그대로 참조하여, 값이 전달됩니다.

// 원시값 (Number)

let temp;
let a = 2;
let b = a;

b = 3;

console.log(a); // a : 2
console.log(b); // b : 3

그리고 원시 값깊은 복사를 사용합니다. 이는 값을 복사 할 때 복사된 값을 다른 메모리에 할당하기 때문에, 원래의 값과 복사된 값이 원본 데이터에 영향을 미치지 않습니다. 이러한 특성을 불변성이라고 합니다.

반면 참조값은 변수가 객체의 주소를 가리키기 때문에 복사된 값(주소)이 같은 값을 가리킵니다. 따라서 참조값을 변경하면 그 값을 가리키던 값도 함께 변화됩니다. 아래의 a의 경우처럼 말이죠. 이는 C언어에서 포인터로 값에 접근한 것과 같습니다. 그리고 이는 얕은 복사가 일어나게 됩니다.

const a = {number: 1};
let b = a;

b.number = 2

console.log(a); // {number: 2}
console.log(b); // {number: 2}

또한 메모리 주소를 참조하는 특성을 이용하여, 위의 c언어의 call by reference 방식과 같이 값을 서로 변환됩니다. 아래와 같이 배열을 파라미터로 전달하면 배열의 복사가 이루어지며, 배열의 인덱스 배열의 indexA 값과 indexB 값은 각각 자신의 주소값을 참조하여 값에 접근하게 됩니다.

function swapArr(arr, indexA, indexB) {
  let temp = arr[indexA];
  arr[indexA] = arr[indexB];
  arr[indexB] = temp;
}

(function main() {
 const arr = [83, 23];

 console.log("a, b", arr[0], arr[1]);
  swapArr(arr, 0, 1);
  console.log("a, b", arr[0], arr[1]);
})();

불변성(immutablity)을 지키기 위해선 깊은 복사를 사용해야 한다.

자바스크립트 UI 라이브러리인 리액트에서는 흔히 불변성을 지키는 것이 중요하다고 이야기합니다. 왜 갑자기 제가 불변성 이야기를 할까요? 그 까닭은 불변성을 지키기 위해선, 값을 수정할 때 얉은 복사를 사용해줘야 하기 때문입니다. 이를 위해 배열이나 객체를 업데이트 할 때, 원본 데이터를 보존하기 위해 새로운 값을 생성하고 그곳에 값을 할당해줍니다. 변수에 직접 해당 값을 할당하면 주소 값이 참조되어, 참조되는 원본 데이터까지 변경시키기 때문이죠.

이는 나중에 리액트의 성능을 최적화하는데 중요합니다. 불변성을 지키지 않게 되면, React.memo와 같은 성능 최적화 메서드를 사용할 수 없기 때문입니다. React.memo는 컴포넌트 안에 state나 props가 변경되었는지 각 데이터 간 비교하여, 변경되지 않았으면 리렌더링이 일어나지 않도록 해줍니다.

결론

자바스크립트에서의 얉은 참조와 깊은 참조를 이해하는 것이 불변성을 지키는 데 도움을 줍니다. 그리고 불변성을 지키면, 리액트의 성능 최적화할 때 큰 도움을 받을 수 있습니다. 리덕스 dev tools와 같이, dispatch 내역이 기록되어 개발 과정에서 발생한 오류를 디버깅하는 데에 도움을 받을 수도 있고 말이죠.

반응형

댓글