Javascript/React

깊은 복사와 얕은 복사, 그리고 의존성 목록

kyoulho 2024. 7. 13. 19:05

대부분의 언어에서는 원시 타입(숫자, 불리언 등)과 읽기 전용 문자열은 값을 새로운 메모리 공간에 복사하여 독립적인 변수를 생성하는 깊은 복사를 수행한다. 반면에 객체와 배열 같은 참조 타입은 주소(참조)를 복사하여 같은 객체나 배열을 공유하게 되는 얕은 복사를 기본적으로 수행한다. 타입스크립트에서는 읽기 전용 문자열도 컴파일 타임에서 크기를 알 수 있어 깊은 복사가 발생할 수 있다. 이 복사 방식은 변수와 객체의 독립성을 유지하고 메모리 관리를 보장하기 위한 중요한 개념이다.

 

리액트 훅에서 깊은 복사 여부가 중요한 이유는 대다수 훅 함수에 필요한 의존성 목록 때문이다. 

const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>)=>{
    const newForm = form	// 얕은 복사
    const newForm = Object.assign({}, form)	// 깊은 복사
    
    newForm.name = e.target.value
    setFrom(newForm)
}, [form])

 

위는 얕은 복사와 깊은 복사를 사용하는 예제이다. 

리액트 프레임워크는 내부적으로 form 상태에 변화가 생겼는지를 form === newForm 형태로 비교한다. 그런데 객체 타입의 복사는 항상 얕은 복사이므로 이 비교값은 항상 true이다. 따라서 리액트는 form에 아무런 변화가 없다고 간주한다. 따라서 리액트는 웹 페이지를 다시 렌더링 하지 않으므로 <input>에 값을 입력해도 from에 반영되지 않는다.

 

Object.assign() 함수를 사용하면 깊은 복사가 일어나 form === newFrom이 항상 false가 되어 웹페이지를 다시 렌더링함으로 정상적으로 동작한다. 그런데 이러한 형태로 작성하는 것은 번거롭다.

전개 연산자 구문으로 코드를 좀 더 간결하게 구현해 보자.

 

객체에 적용하는 전개 연산자 구문

앞선 코드에서 Object.assign 호출을 전개 연산자로 구현한 것이다.

const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>)=>{
    const newForm = {...form}	// 깊은 복사
    newForm.name = e.target.value
    setFrom(newForm)
}, [form])

 

전개 연산자와 함께 객체의 속성값 일부를 변경할 수도 있다.

const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>)=>{
	// 깊은 복사와 name 속성값 변경이 동시에 일어남
   const newForm = {...form, name: e.target.value}
   setForm(newForm)
}, [form])

 

타입스크립트 객체 반환 구문

앞 코드에서 setForm() 호출은 다음처럼 콜백 함수로 구현할 수 있다. 이렇게 하면 form을 useCallback의 의존성 목록에 추가하지 않아도 되므로 useCallback을 호출할 때 선호하는 방식이다.

()로 감싸지 않으면 타입스크립트 컴파일러가 {...form, name: e.target.value} 코드를 객체 구문이 아니라 복합 실행문으로 인식하게 된다. 때문에 소괄호로 감싸주도록 한다.

const onChangeName = useCallback((e: ChangeEvent<HTMLInputElement>)=>{
    setForm(form => ({...form, name: e.target.value}))
}, [form])

 

배열에 적용하는 전개 연산자 구문

전개 연산자는 배열에도 적용할 수 있으며 깊은 복사를 일으키므로 numbers === newNumbers는 항상 false이다.

즉, numbers === newNumbers는 리액트의 의존성 목록 아이템으로 사용할 수 있다.

const numbers = [1, 2, 3]
const newNumbers1 = [...numbers, 4] // [1, 2, 3, 4]
const newBumbers2 = [4, ...numbers] // [4, 1, 2, 3]