리액트 애플리케이션은 컴포넌트기반으로 구축되어 있기 때문에, 상태를 관리하지 않게 된다면, 컴포넌트가 자체적으로 상태를 가지게 된다. 즉, 일관성과 유지보수를 어렵게 만들 수 있다. 쉽게 말해서, 상위 컴포넌트에서 하위컴포넌트로 상태를 전달할때는 props를 통해 전달하는데, 리액트의 경우 컴포넌트안의 컴포넌트 동작이 많기 때문에, 여러 계층의 컴포넌트를 거치면서 props가 너무 많아지고 복잡해질 수 있다. 그러므로, 상태 관리를 통해 데이터를 효율적으로 관리한다.
react와 함께 사용하는 상태관리는 여러종류가 있다.
Redux
Recoil
MobX
사실 상태를 관리하는것에 있어, 위와같이 다양한 방식이 있다. 주 목적은 모두 효율적인 사용을 위해서지만, 왜 이렇게 종류가 많을까? 이는 각 종류마다 특징이 분명하기 때문에, 더 효율낼 수 있기 때문이다.
상태관리라 하면, 가장 대표적인 라이브러리이다. Redux의 공식문서에 다르면, Redux는 애플리케이션의 상태를 중앙 집중식으로 관리하여 예측 가능하고 디버깅하기 쉬운 상태 관리를 제공
한다고 한다. 사실 여기서 궁금한게 있다.
어떻게 예측하나?
이런 의문은 일단, 간단하게, Redux를 동작을 살펴본 뒤, 알아보자.
① Action은 객체로 표현이 되며, 어떤 상태변화를 할 것인지 생성된다.
② 그리고, 이러한 생성된 Action함수는 Dispatch가 동작하여 Reducer로 전달된다.
③ Reducer는 그런 상태변경을 받아, 새로운 상태를 생성한다.
④ 그리고 이것은 Store에 저장이 된다.
⑤ 이후 다시 사용자 화면에 업데이트 한다.
위에서 동작방식을 간단하게 알아봤다. 그러면, 리덕스의 장점인 예측가능은 어떻게 하는 것일까? 예측이라는 말을 잘 생각해야한다. 예측은 예상과는 다른 개념
이다. 즉, 컴퓨터과학의 관점으로 보게 된다면, 예측을 정해진 동작흐름에 따라, 다음 실행될 로직이 무엇인지 알 수 있다는 것이다.
Redux의 엄격한 단방향 데이터 흐름에 따라, 위의 동작 사진처럼, 다음과정에서 어떤 것이 실행이 되어야 하는지 판단할 수 있으며, 이는 자연스럽게 디버깅 및 추적이 좋다.
다른 특징으로는, 데이터 불변성이라는 장점이 있다. 이는, 상태가 변경될 때, 새로운 객체를 생성하여 기존 상태를 변경하지 않는 것을 의미하는 것인데, 다음 코드를 보자.
const initialState = {
counter: 0,
user: {
name: 'CHO YOON CHAN',
age: 26
}
};
위와같은 코드가 있을때, 상태를 변경하려면, 기존의 값을 변경해야한다. 하지만 Redux는 해당 객체를 복사해서, 상태를 생성하기 때문에, 기존 데이터를 유지할 수 있는것이다.
const incrementCounterReducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return {
...state,
counter: state.counter + 1
};
default:
return state;
}
};
최종적으로 Redux는 중앙 집중식 상태 관리를 제공하여 애플리케이션의 상태를 단일 저장소에서 관리허며 단방향흐름, 데이터 불변성이라는 특징을 통해, 상태를 추적하기 용이하며 예상치 못한 상태 변화를 방지할 수 있다.
Recoil은 공식문서에도 이렇게 표현이 되어있다.
공식문서의 말만 보면, React는 Recoil과 떼어놓을수 없는 관계인 것 같다. Recoil은 React를 만든 Facebook에서 만든 상태관리로, Recoil을 사용하기 위해서는 React가 설치되어야한다. Recoil이 나오게된 계기는 공식문서를 보면, 파악할 수 있다. 호환성과 단순성때문에, 외부 전역상태 관리를 사용하는것보다, 내부 전역 상태관리를 사용하는 것이 좋다. 즉, 동작하는 것에 있어서 가장 React스러운 상태를 유지하는 것이다.
ⓐ 컴포넌트 상태는 조상으로 올리면 공유 가능하지만, 큰 트리를 다시 렌더링할 수 있다.
ⓑ Context는 하나의 값만 저장할 수 있고, 다양한 값들을 저장하기 어렵다.
📌이런 제한으로 인해 최상위와 최하위의 상태를 효율적으로 분리하기 어려워진다.
Recoil 위와 같은 문제를 효율적ㅇ로 처리하며, 다른 상태관리와 비교하면, React 로컬 상태와 동일한 간단한 get/set 인터페이스를 가지면서도(필요한 경우 리듀서 등으로 캡슐화할 수 있음) boilerplate- free api를 얻게하는 방식으로 추천이 된다.
boilerplate-free란 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 칭하는 말인 boilerplate에서 free를 붙여 중복되는 표준 코드를 배제하여 코드를 간결하게 유지하고, 불필요한 복잡성을 줄이는 데 도움이 될 수 있다는 의미한다. 더 자세히 말하면, boilerplate 코드 또한, 코드를 초기에 반복적인 작업을 줄여 신속하다는 장점이 있지만 이것을 또 free하게 하여, 기존의 단점이었던, 너무 많거나 복잡하면 코드의 가독성과 유지 보수성이 떨어지는 점을 개선하여, 더중코드를 간결하게 유지한다는 말이다.
Recoil의 동작방식은 그래프를 만든다고 생각하면 된다.
그러면 Atom, Selector의 차이는 뭐냐?
- Atom은 일반적으로 컴포넌트 간에 공유되는 상태를 나타내며, 여러 컴포넌트에서 동일한 상태를 공유할 때 사용된다. 실행은 atom() 함수를 사용하여 정의된다.
- Selector는 Atom의 값을 이용하여 새로운 값을 계산한다. 이 계산은 순수 함수로 이뤄지며, 계산된 값을 캐시하고 필요할 때 다시 계산하여 성능을 최적화할 수 있다. 계산을 하는데 사용되는 이유는, Atom이 맞추고 있는 초짐인 동기와 달리, Selector의 초점은 비동기적 처리이기 때문이다. 실행은 selector() 함수를 사용하여 정의된다.
간단히 말해서, Atom은 상태를 나타내고 직접 값을 저장하며, Selector는 Atom을 기반으로 값을 계산하고 파생된 데이터를 제공한다. 그리고 종속관계이다. 이들을 각각 개별적으로 사용할 수도 있으며, 조합하여 더 유연한 상태관리를 만들수도 있다. 각각의 특성인, Atom을 사용하여 컴포넌트 간에 공유되는 기본적인 상태를 관리하고, Selector를 사용하여 이러한 상태를 기반으로 파생된 데이터를 계산하거나 필요한 변환을 수행하는것인데, 아래에 예시를 두겠다.
예를들어, Atom을 사용하여 사용자의 이름이나 로그인 상태와 같은 기본적인 정보를 저장하고, Selector를 사용하여 해당 정보를 기반으로 사용자의 프로필 이미지 URL을 계산하거나 다양한 필터링 또는 정렬 기능을 구현하는 방법이 있다. 이렇게 함으로써 상태와 계산된 데이터를 분리하여 코드를 더 잘 구조화하고 유지보수하기 쉽게 만들 수 있다.
여기서 한번더 생각해보자. react를 만든 페이스북이, 상태관리를 위해서 recoil을 만들었는데, 분명 친화적인 무엇인가가 있을것이다.
가장 큰 이유는, 기존 react의 상태관리 로직을 효율적으로 해결할 수 있으며, 내부 스케쥴러에 접근할 수 있기 때문이라고 생각한다. 기존의 상태 관리 라이브러리들은 React 라이브러리가 아니기 때문에 React의 내부 스케줄러에 직접적으로 접근할 수 없다. 그러나 Recoil은 내부적으로 React의 상태를 활용하며, 이는 동시성 모드와 같은 새로운 기능들을 사용하는 데 있어 큰 장점으로 작용한다.
React 16 버전부터 소개된 동시성모드는 기존의 싱글스레드 언어인 js의 이벤트루프 대처방식에서의 한계인 대규모 데이터나 복잡한 UI를 다룰 때 렌더링이 지연되거나 반응이 느려지는 문제가 발생할 수 있는점에서 비롯되었다. 이러한 제약은 사용자 경험을 저하시키고, 성능 문제를 야기할 수 있는데, 이는 리엑트의 원칙을 벗어나는 말이다. 이벤트 루프를 우회하는 방식이 아닌, 더 효율적으로 작업을 관리하고 실행하는 방식을 도입하기위해 내부적인 코드를 변경한 것이다.
따라서, 동시성모드를 계속 업데이트중인 리엑트에, 내부적으로 스며들 수 있는 Recoil을 통해, 친화적이라고 말을 할 수 있는 것이다.
최종적으로 Recoil은 리엑트에 친화적인 리엑트 라이브러리로 기존의 다른 외부 상태관리 라이브러리에 비해, 간단한 get/set 인터페이스, 보일러플레이드프리한 것이 특징이다.
쉽다.
렌더링 최적화를 쉽게 할 수 있다.
구조가 자유롭다
라는 철학을 가진, 상태관리 라이브러리이다. 실제로, 프로젝트에 도입해서, 사용을 해봤으며, 한마디로 정말 쉬웠다.
// Observable 생성
const userStore = observable({
num: 1
});
// Action을 사용하여 Observable 상태 변경
const updateUser = action((num) => {
userStore.num = num;
});
// Computed를 사용하여 Observable 상태의 값 계산
const nextNum = computed(() => userStore.num + 1);
// Reaction을 사용하여 Observable 상태의 변화에 대응
reaction(() => userStore.num,
(newNum, reaction) => {
if (newNum > 10) {
reaction.dispose();
}
}
);
// Action을 사용하여 Observable 상태 변경
updateUser(2);
①동작이 발생하면 Acion이 실행된다.
②Action이 실행되면, observable 상태가 업데이트된다. 그대로 Reaction을 사용하거나 계산이 된다.
③Computed를 사용하여 observable 상태의 파생된 값을 계산한다.
④Reaction을 사용하여 observable 상태의 변화에 대응한다.
⑤Action을 사용하여 Observable 상태 변경한다.
추가로 구현에 있어서는, MobX v6 이후부터 makeObservable 함수를 사용하여 클래스의 속성을 Observable로 만들며, observable, action, computed, reaction과 같은 데코레이터를 사용하여 클래스를 정의하는 것은 여전히 가능하지만, MobX 개발팀이 앞으로 makeObservable 함수를 권장한다. 실제 프로젝트에서는 makeObservable을 사용했다.
가끔 인터넷 글에 MobX는 불변성을 지킬 필요가 없다고한다. 왜일까?
불변성은 상태 변화가 있을 때 기존 상태를 변경하는 대신 새로운 상태를 생성하는 패턴을 의미한다. 일반적으로 이 패턴은 상태 변화를 추적하고 예측 가능하게 만들어준다. 예를 들어, React에서는 불변성을 유지하는 것이 상태를 업데이트하는 더 안전한 방법으로 알려져 있다.
리엑트가 불변성유지를 지향하는 이유는 가상 DOM을 사용하여 렌더링을 최적화하기 때문이다. 이러한 최적화를 위해서는 이전 상태와 현재 상태를 비교하여 변경된 부분만을 업데이트할 수 있어야 하는데, 비교에 있어,불변성은 쉽게 처리해주고, 변경된 객체의 참조를 새로운 객체와 비교함으로써 렌더링 성능을 향상시키기 때문이다.
그러나 MobX는 자동으로 반응적으로 관리되는 상태를 추적하고 업데이트하기 때문에, 명시적인 불변성을 지키지 않아도 된다. MobX는 observable(관찰 가능한 객체)를 사용하여 상태를 관리하며, 해당 객체 내부의 속성이 변경되면 MobX는 이를 자동으로 감지하고 관련된 리액션(reaction)들을 업데이트하기 때문이다. 이런 동작방식으로 인해, 일이 새로운 객체를 생성하고 불변성을 유지하는 번거로움을 덜어줄 수 있는 장점이 있다.
최종적으로 MobX는 간단한 상태관리 라이브러리로, 상태를 store에 저장하여, 직접적으로 건들지 않아도 되는, 객체지향적인 특징과 불변성을 크게 고려하지 않아도 된다는 장점이 있다.
위에서 정리한 특징을 종합해서, 아래와 같은 결론을 내렸다.
🔗대규모 서비스의 복잡한 상태 관리가 필요한 경우 :
Redux
Redux는 단방향 데이터 흐름을 사용하여 상태를 관리하므로 애플리케이션의 규모가 커지고 복잡해질수록 유지보수가 용이하다. 또한 불변성을 유지하여 상태 변화를 추적하기 쉽고, 시간 여행 디버깅을 지원하여 버그를 찾고 수정하기에 유리하기 때문이다.
🔗비동기 작업이 많은 상황에서 효율적인 상태 관리가 필요한 경우 :
Recoil
리엑트 친화적인, Recoil은 비동기 작업을 효율적으로 처리할 수 있다. 예를 들어 데이터 로딩 상태를 전역으로 관리하고 각각의 컴포넌트에서 이를 활용하여 처리가 직관적이다.
🔗 초기 설정이나 작은 프로젝트에 적합한 간단한 상태 관리가 필요한 경우 :
MobX
MobX는 초기 설정이나 작은 프로젝트에 적합한 간단한 상태 관리를 제공한다. 간단한 설정으로도 상태 관리를 할 수 있어 러닝커브가 낮다.
🔗 객체 지향 프로그래밍 스타일을 선호하는 경우 :
MobX
객체의 속성을 직접 수정하면 자동으로 상태가 갱신되므로, 객체 지향 프로그래밍 스타일을 선호 하는 경우 적합하다.
실제 써본 상태관리 라이브러리를 근거 있게 분석해보며, 여러 상황에 따라, 어떤 라이브러리를 사용해야하는지 판단할 수 있었던 좋은 시간이었다.