도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

리덕스 패턴과 안티 패턴 (Redux Patterns and Anti-Patterns)

리덕스(Redux)Flux와 비슷한 아키텍쳐로 어플리케이션의 상태 값을 관리하기위한 놀랍게도 심플한 라이브러리다. Affirm에서는, 우리는 특히 리덕스의 time-travel에 관심이 있었다. 우리는 핵심(core) 사업으로 간단한(transparent) 고객 대출을 제공하고 있다. 따라서 사용자의 관점에선 전체 대출 신청 과정을 재생(replay) 할 수 있는 것은 매우 가치 있는 일이다.

리덕스는 프레임워크라기보다는 오히려 패턴을 강제하는 데 도움이 되는 일련의 함수 세트(a set of functions)라고 할 수 있다. 그렇긴 하지만, 좋은 패턴을 강제하는 것을 주의깊게 신경쓰지 않는다면, 리덕스를 결정하는 것을 후회하게 될지도 모른다. 이 글에서는, Affirm에서 구축한 몇가지 리덕스 모범 사례(best practice)와 일반적인 함정에 대해서 살표 보도록 하겠다. 

ImmutableJS

ImmutableJS는 불변(immutable)하는 데이터 구조를 위한 라이브러리다. 우리가 이 라이브러리를 사용하고 싶은 2가지 이유가 있다.

첫번째는 불변데이터를 사용하는 여러가지 이점이 있다는 것입니다. 이것에 대해 얘기하는 많은 기사들이 있지만, 결국 요지는 참조적 투명성(referential transparency)에 관한 것이다. 참조 포인터를 바꾸지 않은 상태로, 객체 변형(mutate)을 허락 한다면, 프로그램에 대해 논리적으로 추론하는 것은 어렵다.

더 높은 수준에서는, 변할 수 있는(mutable) 데이터는 프로그램을 위한 분석의 한 형태로 수학을 사용하는 것을 어렵게 만든다. x가 x와 항상 동일하지 않은 수학 이론을 상상해 보자. 실제로 말하자면, 어떤 객체가 변경 여부를 확인하기위해 깊은 대조(deep comparison) 대신 불변 데이터(immutable)를 사용하여 간단한 참조 비교(===)를 자주 사용 할 수 있다. 이것이 정말 도움이 되는 예제는 리액트(React) 컴포넌트를 리랜더링 할 필요가 있는지 결정 할 때이다. 

ImmutableJS를 사용하는 두 번째 이유는 성능이다. 당신이 데이터 구조의 변화를 일으킬 때, 내부 데이터 구조를 계속해서 복제 한다면 immutable 데이터는 가비지 컬렉션(garbage thrashing) 대상이 될 수 있다. 이 문제를 해결하기 위해, ImmutableJS는 효과적으로 immutable 데이터를 수정하기 위해 지속적인(persistent) 데이터 구조를 사용하고, 내부 데이터를 복제하지 않고 새 참조를 반환 한다. 

연결 리스트(linked list)가 어떻게 작동되는지 생각 해보자: 항목에 추가된 새 연결 리스트를 생성하려면, 간단하게 이전 헤드를 가리키는(points) 새 노드를 만들고 해당 노드에 대한 참조를 반환하면 된다. 이전 목록이 유지되고 기본 데이터가 새 목록과 공유된다. 따라서 우리는 내부의 mutation이 없는 것을 보장 하는 한, 우리는 불변 데이터(immutable data)로 효율적으로 작업 할 수 있다.

하지만, ImmutableJS는 제대로 사용하는 경우에만 유용다고 할 수 있다. 내가 본 2가지 일반적인 실수가 있는데, 첫 번째는 다수개의 mutation을 적용하는 방법이다. 한 개의 Map에 여러 값을 설정한다고 생각해보자. 예를 들어, 아래에서 로딩을 false로 설정하고 사용자 속성을 업데이트 해보자. 

state.set('loading', false).set('user', user)

Map에 값을 설정하면 ImmutableJS는 Hash Mapped Array Trie를 사용하여 중간 상태(intermediate state)를 유지하기 위해 재정렬(rearranging)를 시도 할 것이다.  이것은 실제로 우리가 중간에 변경 되는 값(intermediate steps)이 아닌 마지막 결과 값만 필요하기 때문에 불필요한 작업을 수행 한다. 그 대신에 withMutations 함수를 사용하여 이러한 업데이트를 일괄 처리하고 재정렬을 한번 만 할 수 있다.

state.withMutations(s => s.set('loading', false).set('user', user))

또 다른 안티 패턴은 데이터 작업이 필요할 때마다  .toJS()를 통해 원시(raw) 자바스크립트 객체로 변환 하는 것이다. 이것은 Affirm에서 본 전형적인 예이다.

const mapStateToProps = (state) => ({
loans: state.get('loans').toJS(),
})

이렇게 사용 한다면, ImmutableJS를 사용하여 얻는 성능(performance) 이점을 완전히 잃어버린다. 왜나하면 이 작업을 수행 할 때마다 객체를 복제하기 때문이다.  대신 데이터를 ImmutableJS 객체로 남겨두고, ImmutableJS의 .get.getIn과 같은 메소드를 사용해야 한다.

const mapStateToProps = (state) => ({
loans: state.get('loans'),
})

Redux Actions

다음은 우리가 대출금을 지불하는 Redux action의 예제이다. 

이것은 잘 작동하는 코드인데, Redux를 처음 접한다면 아마 모든 action이 이와 비슷 할 것이다. 즉, 위의 코드에는 몇 가지 안티 패턴이 있는데, 이를 하나씩 살펴보자.

첫 번째 문제는 로직에 에러를 사용하는 방식이다. 로직에 try-catch를 사용하면 코드의 실제 오류를 쉽게 숨길 수 있게되어 콘솔에서 오류가 발생하는 것을 알 수 없게 될 수 있다. 예를 들어, response.status의 철자를 틀렸다고 가정해보자. 그러면 exception이 발생하여 버그 추적을 매우 어렵게 만든다. 그렇기 때문에 catch 문을 제거해 보자.

다음으로는 action의 의도(intention) 부분과 action의 실행(implementation) 부분을 분리시켜야 한다. 이 action에서 HTTP 요청을 설정(configure)하고 보내는 것에 관심을 가져야 할 이유가 없다. 그렇기 때문에 이것을 따로 분리 시켜 보자.

이것은 이제 구현(implementation) 보다는 의도(intention)를 반영하는 action이 됐다. 리팩토링하는 동안 ES7의 async-await 구문을 사용하여 Pythonid of Doom을 처리해 보자 (참고 : Babel state-0 preset 설정을 babel-polyfill과 함께 사용해야 함).

개선이 되긴 했지만, 아직은 리팩토링이 더 필요하다.

또 다른 문제는 우리가 지나치게 dispatch 작업을 하고 있어서, 불필요한 리렌더링(re-render)을 하고 있다는 것이다. 대부분의 사람들은 우리가 action을 dispatch 할 때마다 그것을 reducer에 공급(feed)하고 새로운 상태(state)를 렌더링한다는 것을 깨닫지 못한다. 위의 예제에서 보면, closeModalmakePaymentSuccess 또는 makePaymentFailed로 인해 바로 두번의 렌더링이 실행 된다.

closeModal action을 dispatch하는 것은 이 action의 의도를 혼란스럽게한다. makePayment는 모달 창 닫기와 관련되어서는 안된다. 대신 성공(success) 또는 실패(fail)한 action을 받으면 reducer에서 이 로직을 수행해야 한다.

방금 돌 한개로 두 마리의 새를 잡았다! 마지막으로해야 할 일은 우리의 동기식(synchronous) 및 비동기식(asynchronous) action을 개별 파일로 리팩토링하는 것이다. 이렇게하면 비동기(async) action을 훨씬 더 테스트 가능하게 만든다. 그 이유는, 동기(sync) action을 수행(spyOn)할 수 있고 언제 어디서나 호출이 가능하기 때문이다.

이 테스트는 실제로는 함수 자체의 역(inverse)이라고 할 수 있다. 사실 이것은 함수를 테스트 할 필요가 있는지 여부를 묻는다. 나는 이것이 훌륭한 코딩 실천의 신호라고 생각한다.

Redux Reducer

이제 우리가 action을 정리 했으니 위에서 정의한 reducer 함수를 살펴 보자.

withMutations를 좋은 시작으로써 적절히 사용하고 있다고 할 수 있지만 구현과 로직을 분리하지 못해 코드를 여러 곳에서 반복해서 사용하고 있다. 우리가 여기서 할 수있는 일은 각 mutation을 그 자체의 함수로 분리하고, 그것들을 compose하여 사용하는 것이다. 아래를 확인해보자.

여기에서는 withMutations를 호출하고 각 mutation 함수를 적용하는 파이프(pipe) 함수를 사용한다. 이것은 reducer를 훨씬 더 가독성있게 만든다. 또한, reducer를 테스트하는 것도 쉬워진다. 실제로는 action과 마찬가지로 함수 자체의 역(inverse)이 된다.

다시 한번, 우리는 이 함수를 테스트 할 만한 가치가 있는지 스스로에게 물어볼 수 있다. 우리가 실제로 보장하고 싶은(ensure) 것은 UI에서 기대하는 방식으로 그 mutators가 상태 값(state)을 변경 시키고 있다는 것이다. 그래서 이것들에 대한 단위 테스트를 작성할 수 있지만, UI가 다른 형식으로 상태(state)를 기대하는 경우에는 유용하지 않다. FlowType이나 TypeScript와 같은 정적 유형 검사기(static type checker)를 사용하면 아마도 더 좋을 것이다. 이것에 대해서 다음에 기회가 되면 다뤄 보도록 하겠다.

결론

Redux는 매우 명확(clean)하고, 성능(performant)이 뛰어나고, 이해할 수(understandable)있는 코드로 이어질 수 있지만 잘못 사용하면 오히려 해가 될 수 있다. 이러한 종류의 소프트웨어 패턴에 대해 더 자세히 배우고 싶다면 이 영상을 확인해 보면 좋을 것이다. 이것은 좋은 코드를 작성하는 방법에 대한 몇 가지 고차원적인 개념을 잘 설명하고 있다.



이 글은 Chet Corcos의 Redux Patterns and Anti-Patterns을 번역한 글입니다. 전문 번역가가 아니라 오역이 있을 수 있으니 참고 바랍니다. 원문은 아래에서 확인 하실 수 있습니다.


리뷰