함수형 프로그래밍순수 함수불변성그렇다면 상태의 변경이란?const 키워드 변수원시 타입과 객체 타입원시 타입객체 타입그런데 불변성은 반드시 지켜야 하는가?지키면 좋은 점들이 있다일치 연산자 ===값에 의한 비교참조에 의한 비교만약 객체 타입에서 값에 의한 비교를 해야 한다면,만약 순환 참조 객체끼리 값에 의한 비교를 한다면,⇒ 불변성을 지켜 효율적인 일치 연산!불변성을 지키는 방법그런데 불변성이 무조건 옳은가?객체 지향 프로그래밍참고
함수형 프로그래밍
순수 함수
- 수학의 함수를 프로그래밍으로 가져온 모델
- 수학에서는 상태라는 개념이 없으므로, “상태를 변경한다”는 개념도 없음
- 하지만 프로그래밍에서는 상태가 존재하며, mutation(변이)가 발생할 수 있음
⇒ 그렇기에 함수형 프로그래밍에서는 불변성을 중요하게 여김
불변성
- 상태를 변경하지 않는 것
그렇다면 상태의 변경이란?
- 메모리에 저장된 값을 변경하는 모든 행위
let a; // 변수 선언으로 `a` 변수로 접근할 수 있는 메모리 공간을 마련 a = 1; // 마련된 메모리 공간에 `1`이라는 값을 할당 a = 2; // 메모리에 저장된 `1`값을 `2`라는 값으로 변경(재할당) = 상태 변경!
const
키워드 변수
const
키워드로 변수를 선언하여 재할당, 즉 상태 변경을 방어하여 불변성을 지키자! ⇒ ❌
원시 타입과 객체 타입
원시 타입
boolean
null
undefined
number
string
symbol
- 변경 불가능한 값 ⇒ 불변성 ⭕
const user = { name: "원시타입" }; const name = user.name; // 원시 타입 string 할당 user.name = "재할당"; console.log(name); // "원시타입" console.log(user.name); // "재할당"
- 값에 의한 호출 방식
- 새로운 메모리 공간에 값을 복사하여 할당한 후 함수로 넘기는 방식
객체 타입
Array
Object
등등
- 불변성이 깨질 수 있는 타입
- 참조에 의한 호출 방식
- 인자로 넘기는 변수가 가리키고 있는 메모리 공간의 주소를 함수로 넘기는 방식
- 불변성을 지키고자 한다면, 객체 타입을 사용할 때에는 신경 써야 할 점들이 늘어남 😥
그런데 불변성은 반드시 지켜야 하는가?
지키면 좋은 점들이 있다
- 무분별한 상태의 변경을 막아 상태를 추적할 수 있게 만듦 ⇒ 개발자 입장에서 디버깅이 쉬워짐!
- 효율적인 일치 연산이 가능하여 참조 타입의 변화를 쉽게 알 수 있음
일치 연산자 ===
값에 의한 비교
- 원시 타입에서의 일치 연산자
- 값이 일치하는 지 확인
const 원시_1 = 1; const 원시_2 = 1; console.log(원시_1 === 원시_2); // true
참조에 의한 비교
- 객체 타입에서의 일치 연산자
- 참조가 일치하는 지 확인
const 객체_1 = { a: 1 }; const 객체_2 = { a: 1 }; console.log(객체_1 === 객체_2); // false
만약 객체 타입에서 값에 의한 비교를 해야 한다면,
- 객체 타입은 값을 중첩할 수 있기에 얕은 비교가 아닌 깊은 비교를 해야 함
const valueEqual = (a, b) => { const a_keys = Object.keys(a); const b_keys = Object.keys(b); if (a_keys.length !== b_keys.length) return false; // 두 객체 간의 키 길이 비교 for (const key of a_keys) { if (!b_keys.includes(key)) return false; // 동일한 키를 가지는 지 확인 const a_value = a[key]; const b_value = b[key]; if (typeof a_value !== typeof b_value) return false; // 동일한 값 타입인지 확인 if (typeof a_value === "object") { return valueEqual(a_value, b_value); // 중첩 객체 값 비교를 위한 재귀 } return a_value === b_value; } }
valueEqual
함수는 사실 깊은 비교를 정확하게 수행할 수 없다. 예외 조건이 있어 진정한(?) 깊은 비교는 코드가 더 복잡해진다
(ex. valueEqual({ a: Number.NaN }, { a: Number.NaN }); // false
)만약 순환 참조 객체끼리 값에 의한 비교를 한다면,
- 무한 재귀 호출 😥
const a = {}; const b = {}; a.c = b; b.c = a; valueEqual(a, c);

⇒ 불변성을 지켜 효율적인 일치 연산!
- 객체 타입의 경우 불변성을 지키지 않는다면,
- 참조에 의한 비교로 객체가 변경 되었는지 확인할 수 없음 (값에 의한 비교로 변경 되었는지 확인 해야 함 😥)
const 객체_1 = { a: 2 }; const 객체_2 = 객체_1; 객체_1.a = 10000; // 상태 변경 = 불변성 훼손 console.log(객체_1 === 객체_2); // true
- 불변성을 지킨다면,
- 참조에 의한 비교로 객체가 변경 되었는지 확인할 수 있음
const 객체_1 = { a: 2 }; const 객체_2 = {...객체_1}; console.log(객체_1 === 객체_2); // false;
불변성을 지키는 방법
- 불변 객체로 만들기
Object.freeze
,Object.seal
- 중첩 객체는 불변 객체가 아님 😥
- 방어적 복사를 통해 새로운 객체를 생성
Object.assign
, 전개 연산자- 깊은 복사가 아닌 얕은 복사 😥
⇒ Immer.js 라이브러리나 Immutable.js 라이브러리를 사용하면 됨
그런데 불변성이 무조건 옳은가?
- 불변성은 함수형 프로그래밍에서 중요한 개념일 뿐, 객체 지향 프로그래밍에서는 다른 관점!
객체 지향 프로그래밍
- 상태를
private
접근 제한자로 외부에서 접근 및 변경할 수 없도록 관리하며, 메서드를 통해서만 상태를 변경할 수 있도록 설계
- 만약 불변성을 지키기 위해 상태를 변경할 때마다 객체를 생성한다면, 객체 생성 비용에 따라 성능에 영향을 끼칠 수 있음
⇒ 상황에 맞게 잘 선택하자!