내용 보기
작성자
아롱이 (IP : 172.17.0.1)
날짜
2020-07-28 09:04
제목
[JavaScript] 깊은 복사와 얕은 복사에 대한 심도있는 이야기
모든 프로그래머에게 어린아이 예절 교육하듯이 끊임없이 중요하다고 강조되고 있는 이야기, “데이터를 깊게 & 얕게 때에 따라 잘 복사한다”. 기업 면접에서 가장 빈번하게 나오는 주제이기도 한데, 과연 “복사”라는 행위에 대해 어디까지 알고 있는가? 언어마다 복사라는 행위는 다르게 정의되어 있을 수 있다. 그러나 적어도, 지금까지 누군가에게 자신이 자바스크립트 개발자라고 말하고 다니고 있었다면 지금 이 자리에서 잠시 눈을 감고 스스로에게 질문해보자. 나는 자바스크립트로 복사에 대해 어디까지 알고 있었는지. 읽기 전에이 글은 여러분이 자바스크립트, 조금 더 명확하게는 ECMA-262 에 명시된 다음의 내용을 모두 알고 있다는 가정하에 작성되었다. 그리고, 이 글은 자바스크립트 명세서를 읽고 이해할 수 있을 정도의 수준은 되어야 제대로 이해할 수 있으니 난이도가 조금 있는 셈이다.
특히, 이터러블에 대해서 모른다면 해당 개념에 대해 먼저 공부하고 이 글을 읽기를 권한다. 설명을 시작하기에 앞서우선, 여러가지 예제를 가지고 설명할텐데, 위의 Array.prototype.slice이 방식은 아마도 제일 널리 알려진 방식일텐데, 주로 배열을 깔끔하게 복사할 때 사용할 수 있다. const arr = [1, 2, 3]; 복사가 깔끔하게 이뤄졌다. 문제가 없어보이겠지만, const arr = [1, 2, [3, 4]];
Spread Operator이터러블을 모르면 이제 여기서부터 이해하기 어려울 수 있다. 펼침 연산자(Spread Operator)의 등장으로 값의 복사가 전반적으로 매우 편리해졌다. 펼침 연사자가 호출될 때, 내부적으로는 iterator-looping 액션을 수행한다. 조금 더 자세히 설명하자면, 어떤 객체에
const b = [1, 2, 3]; 와 같은 작업을 수행한다면 내부적으로는 대략적으로 다음과 같은 일이 일어난다. if (!(Typeof(b) is Iterable)) { 위 과정이 이터러블 객체에 대한 복사인데, 객체가 이터러블임이 확인이되면 위의 코드는 명세서의 흐름을 알아보기 쉽게 자바스크립트 문법으로 변환한 것이며 실제는 훨씬 더 많은 검사 과정이 들어가있다. const arr = [1, 2, [3, 4]]; 펼침 연산자 역시 이와 같은 이유로 중첩 구조를 복사하지 못한다. 가장 처음에 언급했던 즉, 자바스크립트에서 “복사” 라고 알려져있는 거의 모든 기능은 이터러블 순회를 수행하도록 설계되어 있다. Object.assign가끔 const arr = [1, 2, [3, 4]]; MDN 에는
JSON.parse & JSON.stringify이제 이 글의 진짜 주제에 대해 본격적으로 다뤄보기로 한다. 위의 내용은 사실
const arr = [1, 2, [3, 4]]; 중첩구조 복사 역시 아주 깔끔하게 수행된 것을 볼 수 있는데, 이는 객체 순환을 통해 값을 옮겨담는 과정이 아닌, 문자열로 변경 후 그것을 다시 해석해 원본 객체로 변경하는 과정에서 자바스크립트의 문자열(string) 이라고 불리는 데이터 타입이 immutable primitive type, 즉 불변성의 형질을 띠는 원시 타입이기 때문이다. const obj = { d: new Date() }; 위의 예제는 원본을 복사한 후 바로 비교를 수행한 모습인데, 두 객체는 서로 동일하지 않다고 평가된 것을 볼 수 있다. 대체, 왜?Date 객체가 사실 문제는 여기서 끝나는게 아니다. ECMA2020 에 새롭게 들어온 기능인 JSON.stringify(BigInt(1)); 또 한가지 재밌는 점은, 위의 const a = []; 이 경우는 순회하지 않는 경우를 뜻하는 “acyclic” 이라는 단어가 아예 ECMA-262 명세서에 따로 언급되고 있다. 딱 하나만 더 예외를 보여주고 왜 그런것인지를 설명하면 될 듯하다. 함수 역시 const arr = [function(){}, () =>{}]; 앞에서 몇 번 언급했을 때 이미 눈치챈 사람도 있겠지만, 자바스크립트의 표준 국제 이름은 ECMAScript-262 다. 이는 ECMA 표준 국제 언어 중 262 라는 식별 번호를 가진 언어라도 이해할 수 있는데, 여기에는 순수하게 브라우저나 서버가 아닌 ECMAScript 라는 언어에 대해 기술해놓은 명세서가 존재하고, 이를 줄여서 보통 ECMA-262 라고 통칭한다. 마찬가지로, JSON 객체에 대한 명세서 역시 따로 존재하는데, 이 명세서도 ECMA 에서 관리되고 있으며 번호는 404 를 부여받았다. 그래서 ECMA-404 라고 통칭한다. 어쨌든, ECMA-404, 즉 JSON 명세서에는 다음과 같이 JSON 값으로 표현될 수 있는 종류를 명시해놓았는데 위의 그림과 같다. 오직 저 종류에 해당하는 값들만 JSON 값으로 인정하겠다는 뜻이다. ECMA-262, 즉 자바스크립트의 명세서에는 요약을 하자면 이렇다. JSON 값으로 판단될 수 있는건 ECMA-404 에 따르면 object 부터 null 까지 모두 7 가지 종류만 포함되고 나머지는 JSON 으로 분류될 수 없다. 이 종류는 ECMA-262 에도 내장 객체 타입의 상세한 종류에 자세히 나와있는데, 더욱 자세하게 표현하자면 어쨌든, 자바스크립트에서 함수에 해당하는 객체는 JSON 값으로 간주될 수 없다. 왜냐면 저 목록에 함수가 없기 때문. 함수는 일반 함수나 애로우 함수 모두 같은 함수로 분류된다. 제너레이터, async 함수 역시 모두 같은 함수로 분류된다. 함수 외에도
JSON.stringify(function() {}) // undefined Lodash 와 Ramda 를 이용한 비교하늘이 무너져도 솟아날 구멍이 있다고 하던가. 아예 방법이 없는 것은 아니다. 대표적으로 lodash 와 ramda 가 완전한 깊은 복사를 구현해놓았는데, 그 둘을 비교해보자. 코드의 구성은 전부 똑같고, 중첩 구조, 실험 결과는 두 라이브러리 모두 성공적으로 완전하게 깊은 복사를 수행했다고 보여진다. 이는 지금까지 내가 위에서 설명한 모든 내용을 전부 부정하는 꼴인데 이게 어떻게 가능한 것일까? lodash 에서 구현해놓은
그에 비해 ramda 는 추상화가 상대적으로 매우 잘되어 있는 편이다. 허나 lodash 역시 어느정도 경우의 수를 두고 체크를 하기에, 결국 lodash 와 ramda 는 복사라는 행위의 퍼포먼스가 떨어지더라도 정확성에 의미를 두고 있다는 것을 알 수 있다. 그러나 깊은 복사를 수행하기 위해 항상 lodash 와 ramda 를 프로젝트에 설치해야한다는 의미는 아니다. 자바스크립트에 왜 깊은 복사를 수행할 수 있는 메소드가 존재하지 않냐는 의문이 들 수 있는데, 이는 TC39 멤버인 Jordan Harband 가 설명해놓은 부분으로 대신 답변하겠다. 관심이 있다면 아래 이슈 역시 확인해보면 좋다. 결론자바스크립트에서 주로 알려져있는 깊은 복사를 하는 방식은 사실 모두 깊은 복사를 수행할 수 없다. 내부적으로 그렇게 모델링 되어있지 않기때문. 그렇기 때문에 일부 라이브러리들은 사용자의 요구를 채워주기 위해 다소 낮은 퍼포먼스를 띠더라도 정확하게 모든 요소를 복사하는 메소드를 구현해 놓았다. 그러나 역시 가장 베스트는, 깊은 복사를 하기 전에 과연 깊은 복사를 무리하면서까지 해야하는지 아키텍쳐 관점에서 다시 한 번 생각해봄이 좋지 않을까 하고 생각한다. 추가)
|
출처1
https://medium.com/watcha/%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%99%80-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%8B%AC%EB%8F%84%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%95%BC%EA%B8%B0-2f7d797e008a
출처2