Post

javascript의 깊은 복사

javascript의 깊은 복사

Javascript의 깊은 복사

  • javascript에서 primitive type은 값이 복사되어 전달되는 call by value 형식을 따른다.

  • 하지만, 객체 타입은 값이 복사되어 전달되는 것이 아닌 객체가 저장되어 있는 '메모리 주소'에 대한 참조 값 이 저장된다.

얕은 복사?

1
2
let user = { name: "joonamin", ...생략... }
let developer = user

앞 서 기술한 것 처럼, user 는 실제 값이 할당된 영역에 대한 참조 값을 저장한다.

하지만, 특정 function 내에서 side effect 없이 객체가 가진 ‘값’ 자체를 복사하기 위해서는 deep copy가 필요하다.

Screenshot 2024-07-27 at 4.39.18 AM

깊은 복사

javascript 진영에서는 과거까지만해도, 깊은 복사를 위한 객체 복제 내장 메소드 를 지원하지 않았다.

비교적 최근에는 structuredClone() 메소드를 제공해주는데, 이 방식이 등장하기 이전에는 어떠한 방식으로 깊은 복사를 구현하였는지 궁금하였다.

가장 단순한 방법

for 기반의 값 복사

1
2
3
4
5
6
let user = { name: "joonamin", ... 생략 ... }
let clonedUser = {}

for (let key in user) {
    clonedUser[key] = user[key]
}

기존 객체의 프로퍼티들을 순회하며 primitive 수준까지 프로퍼티를 잘게 쪼개어 복사하는 방식이다.

가장 단순하지만, 중첩된 객체들을 처리하기 위한 추가적인 로직이 필요하다.

Object.assign(dst, [src, ...])

1
2
3
4
5
let user1 = { name: "joonamin1", ... 생략 ... }
let user2 = { name: "joonamin2", ... 생략 ... }
let clonedUsers = {}

Object.assign(clonedUser, user1, user2)

위 방식 또한, for 기반의 값 복사 형식으로 수행된다. 병합되는 src 들이 모두 primitive 수준이라면 for 기반의 값 복사 방식 보다 짧은 코드 라인 수로 간단하게 작성이 가능하다. (이는 전개 연산자 (...) 또한 마찬가지!)

하지만, 특정 property가 중첩 객체의 형식을 가진다면 여전히 얕은 복사가 가진 문제점들을 해결할 수 없다.


직렬화 & 역직렬화

1
2
const obj = /* ... */
const copy = JSON.parse(JSON.stringify(obj))

직렬화 가능한 object를 직렬화 처리한 다음, 이를 다시 역직렬화 하는 방식이다.

이 방식은 serializable한 object에 대해서만 제약조건하에서 동작한다.

하지만, 위의 접근법은 흔히 우리가 접할 수 있는 cyclic한 object에 대해서는 적용할 수 없다는 단점이 있다.

1
2
3
4
const x = {}
const y = {x}
x.y = y // cycle: x.y.x.y.x.y.x...
const copy = JSON.parse(JSON.stringify(x)) // throws

또한, Maps, Sets, RegExps, Dates 같은 built-in 타입은 serialize 단계에서 정보 유실이 발생할 수 있으므로 주의해야한다.


Structured Clone

  • 하나의 영역에서 다른 영역으로 데이터를 보내기 위한 알고리즘
  • cyclic한 object와 다양한 built-in type을 지원해준다는 장점
  • 현재는 多 브라우저에서 deep copy를 위한 structured clone을 지원해줌

MessageChannel을 이용한 Structured Clone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Cloner {
    constructor() {
        this.pendingClones = new Map()
        this.nextKey = 0
        
        const channel = new MessageChannel()
        this.inPort = channel.port1
        this.outPort = channel.port2
        
        this.outPort.onmessage = ({data: {key, value}}) => {
            const resolve = this.pendingClones.get(key)
            resolve(value)
            this.pendingClones.delete(key)
        }
        this.outPort.start()
    }
    
    cloneAsync(value) {
        return new Promise(resolve => {
            const key = this.nextKey++
            this.pendingClones.get(key, resolve)
            this.inPort.postMessage({key, value})
        })
    }
}

const structuredCloneAsync = window.structuredCloneAsync = 
      Cloner.prototype.cloneAsync.bind(new Cloner)

const main = async () => {
    const original = {date: new Date(), number: Math.random()}
    original.self = original
    const clone = await structuredCloneAsync(original)
    
    // nested object에 대한 deep copy 확인
    console.assert(original !== clone)
    console.assert(original.date !== clone.date)
    
    // cyclic한 object에 대해서도 deep copy 여부 확인
    console.assert(original.self === original)
    console.assert(clone.self === clone)
    
    // value의 동등성 확인
    console.assert(original.number === clone.number)
    console.assert(Number(original.date) === Number(clone.date))
    
    console.log('assertions complete')
}
main()

browser간 MessageChannel을 이용한 방식으로 serialzation algorithm을 사용한다. 이벤트 기반의 동작이므로 cloning 또한 asynchronous하다는 특징이 있다.

history API를 이용한 방식

1
2
3
4
5
6
7
8
9
const structuredClone = obj => {
    const old = history.state
    
    history.replaceState(obj, null)
    const clonedObj = history.state
    
    history.replaceState(old, null)
    return clonedObj
}

browser의 history API를 사용한 방식은 synchronous 하게 동작하며, browser의 history를 임시 공간으로 활용하여 deep copy를 수행한다.

다만, history API는 cost가 큰 연산이므로 브라우저의 응답성을 떨어뜨릴 수 있다는 단점이 존재한다.

Notification API를 이용한 방식

1
2
3
4
5
const structuredClone = obj => {
    const n = new Notification("", {data: obj, silent: true})
    n.onshow = n.close.bind(n)
    return n.data
}

browser의 notification API를 활용한 방식이다. notification에 대한 permission을 요구하고 허가 되지 않을 경우 실패한다는 단점이 존재한다.

–

벤치마크

chrome.b67c4fa3

firefox.1f11f964

firefox.1f11f964

This post is licensed under CC BY 4.0 by the author.