[JS] structuredClone() vs spread operator

숨송·2023년 5월 22일
0

JavaScript

목록 보기
1/1

테오님의 다시 쓰는 함수형 프로그래밍을 읽으며 생긴 의문을 풀어가는 과정을 정리한 글이며, 제 글은 함수형 프로그래밍과 관련된 내용을 설명하지 않으므로 먼저 읽어 보심을 추천드립니다 :)


불변성: 카피온라이트, 방어적복사

함수형 프로그래밍의 3가지 요소 중 2번째 파트인 불변성에 카피 온 라이트(copy on write)와 방어적 복사에 대한 내용이 나옵니다.
이러한 용어는 글에서 다루고 있는 책인 <쏙쏙 들어오는 함수형 코딩>에서 새롭게 풀어 정의한 용어이고, 더 알려진 개념으로는 "순수함수"를 의미한다고 생각합니다.

카피 온 라이트 (Copy on Write)

카피 온 라이트 (Copy on Write)
계산을 여러 번 실행해도 외부를 변경하지 않아야 한다.

카피 온 라이트는 pass by value로 값을 복사해서 원본을 건드리지 않는 함수를 작성하는 것을 말하는데, 이를 통해 액션 함수를 계산 함수로 만들 수 있습니다.
예시로는 slice()spread 표기법이 사용되었습니다.

그런데 테오님의 글에서는 "이러한 방식을 카피 온 라이트(Copy on Write) 혹은 얕은 복사라고 합니다."라고 되어있습니다.
이 때 "얕은 복사라고? 깊은 복사가 아니고..?"라는 의문이 들었습니다.

방어적 복사

그리고 이후 <방어적 복사> 파트를 계속 읽게 되면, 우리가 수정할 수가 없는 라이브러리 함수를 사용하게 되는 특수한 경우나 mutation 함수를 이용해야 하는 경우에 대해 나옵니다.

방어적 복사 예시코드 [출처: 다시 쓰는 함수형 프로그래밍 @teo]

// 액션을 써야하지만 라이브러리 함수라서 내가 수정할 수가 없다.
import someActionLibray from "lib"

const someCalcuation = (obj, value) => {
  someActionLibray(obj, value) // obj의 값을 변경해서
  return obj // 출력한면 이 함수는 계산일까?
}
// 액션을 써야하지만 라이브러리 함수라서 내가 수정할 수가 없다.
const someCalcuation = (obj, value) => {
  const clone = structuredClone(obj); // 완전한 clone을 만들어 낸다.
  someActionLibray(clone, value) // clone값을 변경해도 원본은 변하지 않는다.
  return clone
}

방어적 복사 개념에서는 structuredClone() 함수를 통한 깊은 복사에 대한 내용이 나옵니다. 처음 본 함수여서 검색을 해보며 깊은 복사를 수행하는 함수라는 것을 알게되었습니다.

여기서 두번째 의문이 듭니다.
"structuredClone() 대신 spread 연산자를 사용하면 되는거 아닐까..?"

그럼 이어서 바로 세번째 의문이 따라옵니다.
"그럼 카피 온 라이트와 방어적 복사의 차이가 무엇인거지..?"


의문 정리

structuredClone()과 spread의 차이

두 방법 모두 검색하면 "깊은 복사(deep copy)"로 나옵니다.
measurethat.net에서 두 방법의 copy를 측정해보았습니다.

속도는 spread operator가 훨씬 훨씬 빠르게 나오는데 그럼 structuredClone()은 왜 나온거지?

spread operator는 1레벨 깊이만 깊은 복사를 수행합니다.

const exObj = {
  name: {
    first: "sumin",
    last: "song"
  },
  country: "seoul"
};

const copyObj = { ...exObj };
copyObj.name.last = "tae";
copyObj.country = "daejeon";

console.log(exObj);
// { name: { first: "sumin", last: "tae" }, country: "seoul" }
console.log(copyObj);
// { name: { first: "sumin", last: "tae" }, country: "daejeon" }

예시 코드처럼 중첩된 객체(다차원 객체)의 경우, spread operator로 깊은 복사를 수행할 경우 객체 내의 객체는 copy by reference로 복사가 이루어져 여전히 원본의 값(name.last)이 변경됩니다.
중첩되지 않는 객체의 프로퍼티 country만 깊은 복사가 이루어짐을 알 수 있으며 country의 단계가 1레벨 깊이를 뜻합니다.

spread operator는 깊은 복사일까?

답은 Yes일 수도, No일 수도 있습니다.
1레벨 깊이 한정이라면 깊은 복사가 맞지만, 그 이외의 경우는 얕은 복사라고 봐야 할 것 같습니다.

카피 온 라이트는 얕은 복사?

"spread의 애매한 특성상 얕은 복사라고 작성하신 걸까?"
"하지만 카피 온 라이트 === spread의 사용이라고 볼 수는 없지 않을까..?"
...
등등의 생각이 이어졌고, 태오님께 직접 질문을 드려 추가 설명을 받아 완전히 이해가 되었습니다.

책을 읽지 않은 상태에서 우리가 가진 지식의 깊은 복사/얕은 복사만 가지고 테오님의 글을 읽으니 헷갈렸던 부분인데, 해당 책에서는 기존의 용어가 왜 이걸 써야 하는지에 대한 설명을 하지 못한다고 생각해서 새로운 용어를 명명하고 설명하고 있다고 합니다.
그래서 새로운 얕은 복사(카피 온 라이트)에 대한 용어가 나온 것인데, 이 부분은 다음 의문에서 함께 설명하겠습니다.

카피 온 라이트와 방어적 복사의 차이?

기존 값을 수정해야 하면서, 원본을 변경하지는 않으려고 할 때, 우리가 직접 계산 함수를 작성할 때는 계산 함수 내에서 copy by reference로 받아온 인자의 깊은 복사를 수행함으로써 지향점을 유지할 수 있습니다.
말 그대로 copy on write = 작성할 때 복사한다.

카피 온 라이트 예시코드 [출처: 다시 쓰는 함수형 프로그래밍 @teo]

const increase = (arr) => {
  arr = arr.slice() // array를 조작하기 전에 복사해서 사용한다.
  const value = arr[arr.length - 1]
  arr.push(value + 1)
  return arr
}

// spread 표기법을 쓴다면 더 간결하게 작성할 수 있다.
const increase = (arr) => [...arr, arr[arr.length - 1]]

이 예시의 increase 함수를 작성할 때 copy를 진행하는 것을 copy on write = 얕은 복사라고 합니다. 우리가 알고 있는 기존의 "얕은 복사"에 너무 얽매이지 않고 copy on write에 중점을 두고 이해하면 됩니다.

그렇다면 방어적 복사는 다른 라이브러리가 행하는 계산/액션 함수를 사용할 때 해당 함수를 직접 조작하지 못하므로, 깊은 복사를 수행한 인자를 넣어주는 것으로 원본 변경에 방어적인 태도를 취할 수 있습니다.

이렇게 되니 두 개념(카피 온 라이트, 방어적 복사)에 대한 차이는 이해는 되지만, 예시로 spreadstructuredClone()가 나눠져서 사용된 점은 책을 읽어봐야 정확히 알 수 있을 것 같습니다 :)



마무리

input으로 들어올 인자의 깊이 레벨이 1레벨 한정이거나, 고정된 깊이고 그 레벨을 알 수 있다면 spread operator를 사용하는게 좋지만, 그 이외의 경우는 다차원 깊은 복사를 수행할 수 있는 structuredClone()나 loadash의 cloneDeep() 등을 사용하는 것이 좋습니다.

글을 제대로 이해하지 못해 시작한 학습이지만, 의외로(?) 얻어갈 것이 많은 과정이었습니다. 함수형 프로그래밍에 관한 글 하나로 생각할 것들이 많이 생겼는데, 우선 제가 작성했던 코드로 실습하며 좀 더 체화가 되면 정리해보려고 합니다.


더 좋은 방법을 제시해주시거나, 틀린 점을 지적해주신다면 감사하겠습니다!

0개의 댓글