[FP] 불변성

yongkini ·2024년 8월 26일
0

Functional Programming

목록 보기
7/10

불변성 in FP

: React와 Redux 등을 쓰면서 불변성에 대해서는 익히 들어서 알고 있었다. 하지만, 생각해보면 왜 불변성을 유지해야하지? 라는 생각을 제대로 해본 적이 없다. 단지 React, Redux 등의 인터페이스가 불변성을 유지하는 방향으로 이뤄져있기 때문에 거기에 맞춰서 primitive 가 아닌 예를 들어, 배열, 객체 등을 불변성을 유지하는 방향으로 계속해서 새로운 주소값을 사용하여(Array.slice or Object.assign 등) 새로 만들어주곤 했다.

: 근데 함수형 프로그래밍에서도 불변성 개념이 상당히 중요하다고 한다. 결론적으로, 아직까지도 나는 불변성이 함수형에서도 왜 중요한지는 100% 이해했다고는 못하겠다. 하지만, 이번에 vuejs 프로젝트를 진행하면서도 불변성을 유지하지 않아서 생기는 몇가지 문제점을 직접 경험했고, 그 디버깅 추적하는 시간 자체를 없애기 위해서라도 불변성은 충분히 챙길만한 개념이라는 생각이 든다.

그리고 특히 함수형에서 불변성이 중요한 이유는 상태 관리와 연관이 있다. 상태란 어느 한 시점에 찍은 모든 객체에 저장된 데이터의 스냅샷이다. 하지만, JS는 상당히 이런 상태 관리에 취약한 언어라서 불변성 관리가 필요하다.

JS는 불변성에 취약한 언어다.

class 키워드로 객체를 만든 후에 getter, setter를 설정해놓아도, 특정 객체의 프로퍼티에 직접 접근해서 해당 값을 변경하는게 가능하다(불변성이 깨짐). 예를 들어,

class Person {
    constructor(firstname, lastname, ssn) {
        this._firstname = firstname;
        this._lastname = lastname;
        this._ssn = ssn;
        this._address = null;
        this._birthYear = null;
    }

    get ssn() {
        return this._ssn;
    }

    get firstname() {
        return this._firstname;
    }

    get lastname() {
        return this._lastname;
    }

    get address() {
        return this._address;
    }

    get birthYear() {
        return this._birthYear;
    }

    set birthYear(year) {
        this._birthYear = year;
    }

    set address(addr) {
        this._address = addr;
    }

    toString() {
        return `Person(${this._firstname}, ${this._lastname})`;
    }
}

저번 블로깅에서 본 이 클래스로 객체를 하나 만들고 getter를 쓰지 않고

const yongki = new Person(....);
yongki._firstname = 'jihoon';

이런식으로 인터페이스를 무시해버리고 직접 접근해도 접근이 되고, 수정이 된다는 것이다. 이렇게 되면 결론적으로 불변성이 깨지게 되고, 프로젝트가 동작하는 동안 어떤 변수가 생길지 개발자가 통제하기 어려워진다는 문제가 생긴다. 그래서 이번 블로깅에서는 JS를 쓸 때 이런 부분을 통제하는 방법들을 알아보고자 한다.

클로저 패턴을 사용한 값-객체 패턴


: codesandbox를 통해 값-객체 패턴을 이용해, 좀 이상한 프로그램이지만 ㅎㅎ
1) Get My Age 란 버튼을 누를 때마다 내 나이가 갱신돼서 출력되고,
2) firstname@일/월/년도@지역 이 형식으로 input에 입력하고 Get Children Name 버튼을 누르고, Get Information 버튼을 누르면, 나의 자식 정보를 출력한다.

이런 값-객체 패턴을 사용하면, 외부에서는 앞서 말한 케이스처럼 직접적으로 state에 접근할 수 없게 된다(객체 내의 프로퍼티들). 따라서, getter, setter가 제대로 된 역할을 할 수 있게 되고, 불변성도 지킬 수 있게 된다.

하지만, 이러한 값 객체는 이상적인 패턴이긴 하지만, 실세계의 문제를 전부 모형화하기엔 무리가 있다. 따라서, 객체지향 형태를 유지하면서도(앞에서처럼) 불변성을 유지하는 방법을 찾아보자.

Object.freeze를 사용해서 가동부를 깊이 동결

위의 케이스에서 만약에 만든 객체를 Object.freeze를 사용해 동결하면

const yongki = Object.freeze(new Person(....));
yongki._firstname = 'jihoon'; // error

이렇게 된다. Object.freeze() 함수는 객체의 writable 속성을 false로 바꿔서 수정을 못하게 만든다.

그럼, 깊이 동결한다는 의미는 뭘까?. Object.freeze()는 동결을 해주지만, 중첩된 객체가 있으면 그 객체는 동결하지 못한다. 예를 들어, Person 객체의 프로퍼티 중에 _address 값이 있고, 그 _address 값은 Address 라는 클래스의 객체로 관리한다고 할 때, Object.freeze만으로는 Address까지는 동결하지 못한다. 그래서 재귀함수 로직을 써서 중첩된 객체 모두를 동결하는 방법을 쓸 수 있다.
** 최상위 객체만 동결되는걸 얕은 동결 이라고 한다.

function deepFreeze(obj) {
	if(isObject(obj) && !Object.isFrozen(obj)) {
		Object.keys(obj).forEach(name => deepFreeze(obj[name]));
		Object.freeze(obj);
	}
	return obj;
}	

객체 그래프를 렌즈로 탐색/수정

: 리액트나 리덕스에서 state 업데이트를 할 때 보통 개발자가 직접 카피 온 라이트(=copy-on-write)를 해주는데, 그걸 직접하지 않고, ramda 모듈의 lens를 사용해서 좀 더 편하게 불변성을 유지하는 방향을 말한다.

ramda lens를 사용하여 나만의 pushup, pullup, squat 데일리 카운팅 서비스를 만들어봤다.

마무리

  • 최근에 vuejs3 플젝을 하면서 앞서 말한 ramda의 lens로 pinia state 업데이트를 처리할까 했었는데, vuejs는 따로 얕은 비교를 통해 비교하는(리렌더링 할 때) 로직이 없는 것 같았다. 옛날에 immer js처럼 그냥 같은 주소값에 넣어도 리렌더링이 잘돼서 굳이 불변성을 유지하는 방향으로의 코드 설계가 오히려 리소스 낭비라고 생각해 적용하지 않았다. 하지만, 이부분은 좀 더 알아봐야하는게 비슷한 문제가 몇개 있었기에 확언은 할 수 없다. 따라서, 다음 플젝에서는 ramda 렌즈를 활용해서 작업을 해봐야겠다.
  • 클로저 패턴도 플젝을 할 때 충분히 적용할 수 있는 패턴이라고 생각하고, 쓸 때가 있으면 적용해보자.
profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글