테오님의 다시 쓰는 함수형 프로그래밍을 읽으며 생긴 의문을 풀어가는 과정을 정리한 글이며, 제 글은 함수형 프로그래밍과 관련된 내용을 설명하지 않으므로 먼저 읽어 보심을 추천드립니다 :)
함수형 프로그래밍의 3가지 요소 중 2번째 파트인 불변성에 카피 온 라이트(copy on write)와 방어적 복사에 대한 내용이 나옵니다.
이러한 용어는 글에서 다루고 있는 책인 <쏙쏙 들어오는 함수형 코딩>에서 새롭게 풀어 정의한 용어이고, 더 알려진 개념으로는 "순수함수"를 의미한다고 생각합니다.
카피 온 라이트 (Copy on Write)
계산을 여러 번 실행해도 외부를 변경하지 않아야 한다.
카피 온 라이트는 pass by value
로 값을 복사해서 원본을 건드리지 않는 함수를 작성하는 것을 말하는데, 이를 통해 액션 함수를 계산 함수로 만들 수 있습니다.
예시로는 slice()
나 spread 표기법
이 사용되었습니다.
그런데 테오님의 글에서는 "이러한 방식을 카피 온 라이트(Copy on Write) 혹은 얕은 복사라고 합니다."라고 되어있습니다.
이 때 "얕은 복사라고? 깊은 복사가 아니고..?"라는 의문이 들었습니다.
그리고 이후 <방어적 복사> 파트를 계속 읽게 되면, 우리가 수정할 수가 없는 라이브러리 함수를 사용하게 되는 특수한 경우나 mutation 함수를 이용해야 하는 경우에 대해 나옵니다.
// 액션을 써야하지만 라이브러리 함수라서 내가 수정할 수가 없다.
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 연산자를 사용하면 되는거 아닐까..?"
그럼 이어서 바로 세번째 의문이 따라옵니다.
"그럼 카피 온 라이트와 방어적 복사의 차이가 무엇인거지..?"
두 방법 모두 검색하면 "깊은 복사(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레벨 깊이를 뜻합니다.
답은 Yes일 수도, No일 수도 있습니다.
1레벨 깊이 한정이라면 깊은 복사가 맞지만, 그 이외의 경우는 얕은 복사라고 봐야 할 것 같습니다.
"spread
의 애매한 특성상 얕은 복사라고 작성하신 걸까?"
"하지만 카피 온 라이트 === spread의 사용
이라고 볼 수는 없지 않을까..?"
...
등등의 생각이 이어졌고, 태오님께 직접 질문을 드려 추가 설명을 받아 완전히 이해가 되었습니다.
책을 읽지 않은 상태에서 우리가 가진 지식의 깊은 복사/얕은 복사만 가지고 테오님의 글을 읽으니 헷갈렸던 부분인데, 해당 책에서는 기존의 용어가 왜 이걸 써야 하는지에 대한 설명을 하지 못한다고 생각해서 새로운 용어를 명명하고 설명하고 있다고 합니다.
그래서 새로운 얕은 복사(카피 온 라이트)에 대한 용어가 나온 것인데, 이 부분은 다음 의문에서 함께 설명하겠습니다.
기존 값을 수정해야 하면서, 원본을 변경하지는 않으려고 할 때, 우리가 직접 계산 함수를 작성할 때는 계산 함수 내에서 copy by reference
로 받아온 인자의 깊은 복사를 수행함으로써 지향점을 유지할 수 있습니다.
말 그대로 copy on write = 작성할 때 복사한다.
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에 중점을 두고 이해하면 됩니다.
그렇다면 방어적 복사는 다른 라이브러리가 행하는 계산/액션 함수를 사용할 때 해당 함수를 직접 조작하지 못하므로, 깊은 복사를 수행한 인자를 넣어주는 것으로 원본 변경에 방어적인 태도를 취할 수 있습니다.
이렇게 되니 두 개념(카피 온 라이트, 방어적 복사)에 대한 차이는 이해는 되지만, 예시로 spread
와 structuredClone()
가 나눠져서 사용된 점은 책을 읽어봐야 정확히 알 수 있을 것 같습니다 :)
input으로 들어올 인자의 깊이 레벨이 1레벨 한정이거나, 고정된 깊이고 그 레벨을 알 수 있다면
spread operator
를 사용하는게 좋지만, 그 이외의 경우는 다차원 깊은 복사를 수행할 수 있는structuredClone()
나 loadash의cloneDeep()
등을 사용하는 것이 좋습니다.
글을 제대로 이해하지 못해 시작한 학습이지만, 의외로(?) 얻어갈 것이 많은 과정이었습니다. 함수형 프로그래밍에 관한 글 하나로 생각할 것들이 많이 생겼는데, 우선 제가 작성했던 코드로 실습하며 좀 더 체화가 되면 정리해보려고 합니다.
더 좋은 방법을 제시해주시거나, 틀린 점을 지적해주신다면 감사하겠습니다!