불변성을 지켜야 하는 이유?
데이터를 가공하는 과정에서 예상치 못한 변화를 막기 위해
자바스크립트에서 객체와 배열을 할당하는 변수는 참조값을 가진다. 참조에 의해 복사가 일어나기 때문에 string, number와는 다르게 복사했을 때 독립적이지 않다. 즉, 만약 객체 A를 새로운 변수 B에 할당 했다고 했을 때, A와 B는 동일한 참조를 가지게 되어 B 객체의 value만을 변경했다고 하더라도 A 객체에까지 영향을 미치게 된다. 자바스크립트를 처음 공부했을 때는 이 개념이 많이 헷갈렸어서, 간단한 예제를 준비했다. 아래 예시를 확인하면 좀 더 어떤 말인지 더 와닿을 거라고 생각한다.
const soyoon = {name: '소윤', age: '28'}
const soyoon2 = {name: '소윤', age: '28'}
const user = soyoon
// 객체 안의 내용이 일치해도, 두 객체는 서로 다른 참조값을 가지기 때문에 같은 객체가 아니다.
soyoon !== {name: '소윤', age: '28'}
soyoon !== soyoon2
// user는 soyoon의 참조값을 복사하기 때문에 같은 객체이다.
soyoon === user
// user.age로 user의 값만 바꿨지만, soyoon과 user는 같은 참조값을 가지기 때문에 값이 같이 바뀐다.
// user.age로 soyon.age도 같이 바뀐 것을 확인할 수 있다.
user.age = 30
console.log(soyoon.age) // 30
console.log(user.age) // 30
console.log(soyoon2.age) // 28
// soyoon2는 다른 참조값을 가지기 때문에 soyoon2.name을 변경해도 soyoon, user에 아무런 영향을 주지 않는다.
soyoon2.name = '정소윤'
console.log(soyoon.name) // '소윤'
console.log(user.name) // '소윤'
console.log(soyoon2.name) // '정소윤'
이처럼 객체/배열처럼 참조에 영향을 받는 타입들은 다룰 때 조심해야 한다. 개발을 할 때 대부분의 작업은 객체 혹은 배열로 이루어진 데이터를 적절하게 가공하는 일들로 이루어져 있기 때문에, 만약 예상하지 못한 곳에서 이를 잘못 다뤘을 때에는 코드 전체가 꼬일 수도 있다. 때문에 불변성을 유지하는 것이 중요하다.
액션의 최소화
앞선 챕터에서 반복적으로 나왔던 얘기는 바로 액션을 사용하는 곳을 최소화 하고 계산을 생성해야 한다는 것이었다. 불변성을 지킬 경우 함수 외부에 있는 데이터를 직접적으로 변경하지 않고, 오로지 input에 대한 가공 및 output을 return 하기 때문에 부수 효과가 없어진 계산이 된다. 액션, 계산이 뭔지 잘 모르겠다면 이전에 작성한 '액션, 계산, 데이터란?' 글을 참고해보면 좋을 것 같다!
[함수형 코딩] 액션, 계산, 데이터란?
프론트엔드에서 클린 코드를 고민했을 때, 코드 개선에 실질적인 도움이 되었던 부분이 액션 함수와 순수 함수에 대한 개념이었다. 기존에 짠 코드들을 봤을 때, 순수 함수는 거의 존재하지 않
soyoondaily.com
아래 작성한 카피온라이트와 방어적 복사는, '쏙쏙 들어오는 함수형 코딩'에서 불변성을 지키기 위해 사용되는 방법들이다. 각각 어떤 특징이 있고 두 가지는 어떤 차이가 있는지 비교해보려고 한다.
카피온라이트
카피온라이트는 copy-on-write를 나타낸다. 데이터를 변경할 때 원본을 그대로 가공하는 것이 아니라, 원본을 복사한 후 복사본을 변경하여 리턴하는 특징을 가지고 있다.
1. 복사본 만들기
2. 복사본 변경하기
3. 복사본 리턴하기
배열을 다루는 간단한 예제를 가지고 왔다. 책에 있는 내용 중 가장 간단한 예제를 가공했으며 카피온라이트의 각 단계를 주석으로 나타냈다. input으로 가공하려는 데이터를 받고, 이를 그대로 사용하는 것이 아닌 복사 과정을 거친 후 가공하여 결과물을 return 하고 있다.
const add_element = (array, element) => {
const newArray = array.slice(); // 1번: 복사
newArray.push(element); // 2번: 가공
return newArray; // 3번: 복사본 return
}
React에서 useState 값을 변경할 때도 이미 카피온라이트를 사용하고 있다. 아래는 객체를 다루고 있으며, 스프레드를 통해 기존 객체를 만들고 가공하고 해당 값을 새로운 state 값으로 return 하고 있다.
import { useState } from "react";
function App() {
const [user, setUser] = useState({ name: "soyoon", age: 28 });
const handleClick = () => {
setUser(prev => ({ ...prev, age: prev.age + 1 })); // 새로운 객체 생성
};
console.log("컴포넌트 렌더링됨!");
return (
<div>
<p>{user.name} - {user.age}</p>
<button onClick={handleClick}>나이 증가</button>
</div>
);
}
방어적 복사
방어적 복사는 신뢰할 수 없는 코드의 불변성을 지킬 때, 데이터가 바뀌는 것을 완벽하게 막아주는 원칙이다. 공용 라이브러리나 레거시 코드를 사용할 때, 넘겨지는 데이터의 원본이 바뀌는 것을 방어해준다. 데이터를 깊은 복사한 후 신뢰할 수 없는 코드로 복사본을 전달하며, 만약 신뢰할 수 없는 코드에서 변경될 수도 있는 데이터가 들어올 경우에는 또 다시 깊은 복사를 통해 데이터의 불변성을 지킨다.
function updateUserAddressSafe(user, newCity) {
// deepCopy는 깊은 복사 함수로 가정
const newUser = deepCopy(user);
updateUserAddress(newUser); // 레거시 코드로 이루어진 함수라고 가정
return deepCopy(newUser);
}
const user = { name: "soyoon", address: { city: "Seoul" } };
const updatedUser = updateUserAddressSafe(user, "Busan");
카피온라이트 vs 방어적 복사
구분 | 카피 온 라이트 | 방어적 복사 |
언제 사용? | 통제 가능한 데이터를 다룰 때 | 신뢰할 수 없는 코드와 데이터를 주고받아야 할 때 |
어디서 사용? | 안전지대 어디서나 사용 가능 | 레거시 코드, 라이브러리 등을 사용할 때 |
사용 방식 | 데이터 수정이 일어나기 직전에 복사 진행하여 사용 | 코드 사용 전에 복사 진행 |
복사 방식 | 수정이 필요한 부분에 대해 복사하므로 얕은 복사가 대부분 | 전체 데이터를 복사해야 하기 때문에 깊은 복사가 대부분 |
방어적 복사의 경우 대부분 깊은 복사를 진행하기 때문에, 얕은 복사가 대부분인 카피온라이트에 비해 비용이 더 크다. 중첩된 데이터가 있을 경우 전체를 복사해야 하기 때문이다. 때문에 가능하다면 카피온라이트를 사용하고, 카피온라이트 사용이 불가한 경우에만 방어적 복사를 하는 것이 좋다.
'개발' 카테고리의 다른 글
[함수형 코딩] 계층형 설계 - 직접 구현 (0) | 2025.04.02 |
---|---|
[시나브로 자바스크립트] History API와 SPA 라우팅 (3) | 2025.03.21 |
[함수형 코딩] 더 나은 액션 만들기 (2) | 2025.03.19 |
[시나브로 자바스크립트] Monorepo란? (5) | 2025.03.14 |
[함수형 코딩] 액션, 계산, 데이터란? (5) | 2025.03.12 |