티스토리 뷰

Redux를 다룰때 주의해야 할 점은 Reducer는 반드시 순수함수여야 한다는 점 입니다. 하지만 순수함수가 정확히 무엇이고 왜 순수함수여야만 하는지를 잘 모르는 경우가 많습니다. 이번 포스팅에서는 해당 내용에 대해 다뤄보도록 하겠습니다.

순수함수


순수함수를 한마디로 정의해보자면 동일한 인자가 주어졌을 때 항상 동일한 결과를 반환해야 하며 외부의 상태를 변경하지 않는 함수입니다. 쉽게 말하면 함수 내 변수 외에 외부의 값을 참조, 의존하거나 변경하지 않아야 합니다.

 

아래 예제의 함수들은 모두 순수함수가 아닙니다.

let value = '123';

function func1(abc) {
	value = '1234';
    return abc + 1;
}

function func2(abc) {
    return abc + value;
}

function func3(abc) {
    abc = 123;
    return abc + 1;
}


console.log(func1(12));	//13
console.log(func2(12));	//'12123'
console.log(func3(12));	//124

func1은 함수 외부의 value 변수를 수정하였고, func2는 함수 외부의 value 변수를 참조했습니다. 그리고 func3는 함수의 인자를 수정하였기 때문에 순수함수라고 할 수 없습니다.

 

이외 간단한 다른 경우는 서버에 데이터를 요청하여 그 값을 반환하는 함수역시 외부의 값을 참조하기 때문에 순수함수라고 할 수 없습니다.

 

Redux의 변경감지 정책


순수함수라는 개념은 생각보다 어렵게 다가오지 않았을거라 생각합니다. 순수함수가 무엇인지 이해하였지만 Redux의 Reducer는 왜 순수함수로 구현해야 하는지 의문이 아직 해결되지 않았습니다.

 

Redux의 State는 불변해야 한다는 특징을 가지고 있습니다. 불변한다는 것은 state가 변경되면 안된다는 뜻이 아니라 state가 수정되면 안된다는 뜻 입니다.

 

아래는 action에 따라 숫자를 증감시키는 counter reducer입니다.

function counter(state = initialState, action) {
	switch(action.type) {
		case types.INCREMENT:
			return { ...state, number: state.number + 1 };
		case types.DECREMENT:
			return { ...state, number: state.number - 1 };
		default:
			return state;
	}
}

state의 값을 변경할때는 Spread Operator(...)으로 기존 state의 값들을 복사한 뒤 number라는 항목만 변경하여 state를 반환합니다. 그 외에 default case에서는 state를 수정할 필요가 없기때문에 state 그대로를 반환합니다.

 

이렇게 reducer를 구현할 때 인자로 들어온 state를 직접 수정하지 않고 복사본을 만들어 수정하는 이유는 redux의 변경 감지 알고리즘에 있습니다.

 

redux는 reducer를 거친 state가 변경됐는지를 검사하기 위해 state 객체의 주소를 비교합니다. state의 복제본을 만들어 반환하면 이전의 state와 다른 주소값을 가르키기 때문에 state가 변경되었다고 판단합니다. 반대로 state를 복제하는것이 아닌 속성만 수정하여 반환하면 기존의 state 객체와 가리키는 주소값이 같기 때문에 변경감지가 되지 않습니다.

Redux는 왜 이렇게 설계되었나?


그렇다면 왜 객체의 속성 값들을 비교하지 않고 주소를 비교하는걸까 라는 의문이 생깁니다. 그 이유는 성능과 복잡성에 있습니다.

 

객체의 속성으로 비교하는 것은 깊은 비교(deep-compare)라고 합니다. 이는 개발자에게 더 편리할 수 있습니다. 하지만 state 객채에 모든 속성에 대해 변화를 감지하기 위해선 복잡하고 무거운 알고리즘이 필요합니다.

 

아래 두 함수는 각각 주소를 통한 비교화 속성을 통한 비교를 하는 알고리즘의 예제입니다.

// 객체의 주소 비교
const compareReference = (object1, object2) => object1 === object2;

// 객체의 속성 비교
const compareProps = (object1, object2) => {
    const object1Keys = Object.keys(object1);
    const object2Keys = Object.keys(object2);
    if (object1Keys.length !== object2Keys.length) {
        return false;
    }
    for (const key of object1Keys) {
    	if (typeof object1[key] === 'object' && typeof object2[key] === 'object') {
        	return compareProps(object1[key], object2[key]);
        } else if (object1[key] !== object2[key]) {
            return false;
        }
    }
    return true;
}

물론 실제로는 훨씬 더 복잡한 알고리즘이 필요하겠지만 지금 상태로도 충분히 속성비교 알고리즘이 복잡하다는것을 알 수 있습니다. 그렇다면 두 알고리즘의 성능은 얼마나 차이가 날까요?

 

아래 예제는 위에서 구현된 두 알고리즘의 성능을 측정하기 위해 작성한 테스트코드 입니다. 100만개의 속성을 가진 object를 생성하고 두 알고리즘의 수행시간을 측정해보았습니다.

describe('change detection', () => {
    const object = {};
    for (let n = 0; n < 1000000; n++) {
        object['prop' + n] = '123';
    }

    it('compare reference', () => {
        const object2 = object;
        const object3 = { ...object, prop2: 1234 };
        expect(compareReference(object, object2)).toBeTruthy();
        expect(compareReference(object, object3)).toBeFalsy();
    });

    it('compare properties', () => {
        const object2 = object;
        const object3 = { ...object, prop2: 1234 };
        expect(compareProps(object, object2)).toBeTruthy();
        expect(compareProps(object, object3)).toBeFalsy();

    });
});

/*
테스트 결과
√ compare reference (1956ms)
√ compare properties (4011ms)
*/

결과는 생각보다 큰 차이는 나지 않았지만 2배정도 차이를 보였습니다. 물론 state에 속성을 100만개씩이나 저장할 일이 극히 드물긴 하겠지만 속성의 개수가 적더라도 Redux를 사용하며 state가 비교되는 횟수를 감안하면 유의미한 측정결과라고 생각됩니다.

 

또한 state의 속성은 트리구조이기 때문에 자식 속성 밑에 또다른 속성들이 있을수도 있으며 객체의 주소 비교는 O(1)만큼의 비교를 하지만 속성 전체를 비교하는 deep compare는 O(n) 만큼의 데이터를 비교해야 하기 때문에 state의 속성이 많아지면 많아질수록 변경감지에 소요되는 시간이 늘어납니다.

마무리


처음에는 순수함수에 대해서만 다루려고 하다가 Reducer는 왜 순수함수여야 하는지 궁금증이 생겨 Redux에대해서도 다루게 되었습니다. 평소에 그냥 그렇구나 하고 지나쳤던 내용들도 파헤쳐보면 다 나름의 이유와 원리가 있다고 생각됩니다.

 

 

순수 함수란? (함수형 프로그래밍의 뿌리, 함수의 부수효과를 없앤다)

함수형 프로그래밍 함수형 프로그래밍 : 부수 효과를 없애고 순수 함수를 만들어 모듈화 수준을 높이는 프로그래밍 패러다임 * 부수 효과 = 외부의 상태를 변경하는 것 또는 함수로 들어온 인자의 상태를 직접 변..

jeong-pro.tistory.com

 

Vobour

vobour - We write together

www.vobour.com

댓글
  • 프로필사진 GT3TM 객체 속성 비교 코드 잘못됨.
    ...
    for (const key of object1Keys) { if (object1[key] !== object2[key]) { return false; } }
    ...
    위와 같이 하는 것은 속성 간 주소만 비교 하는 것임.
    compareProps(object1[key], object2[key]) 로 비교해야 deep compare 되는 것임.
    때문에 model의 depth가 깊어질 수록 성능이 엄청나게 떨어질 수 밖에 없음.
    2019.07.19 12:49
  • 프로필사진 박스여우 의견 감사합니다.

    해당 예제는 알고리즘을 최대한 간단하게 하기 위해 object의 속성이 다른 속성을 가질 수 없는 type으로 가정하여 작성하였습니다.

    의견 반영하여 예제 수정하였습니다.
    감사합니다
    2019.07.19 15:47 신고
댓글쓰기 폼
Total
354,596
Today
15
Yesterday
658
링크
«   2019/11   »
          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
글 보관함