수업을 들으면서 처음 Redux에 대한 생각은 이렇게 깔끔하게 props drilling을 해결 할 수 있다는게 대단하다고 생각했다.
사용법도 겉보기에는 어려워 보이지 않고 정해진 값만 가지고 그냥 이렇게 쓰는거래 ~ 하면 이보다 쉬운게 없다고 느껴졌는데..
5개월밖에 공부하지 못했지만 확실하게 알고있는건 이 업계에서 효율적이면서 쉽게 사용할 수 있는건 굉장히 어렵게 공부해야 정확하고 용도에 맞게 잘 쓸 수 있다는 것..
그래서 정리해보려고 한다 . Redux
Redux
공식문서에 Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너입니다. 라고 적혀있다.
제목에서 '예측가능한' 을 명시하지 않은 이유는 본문에서 강조를 하고 싶어서인데,
Redux의 전개방식, 원칙, 필요한 요소를 정확하게 이해했을 때 왜 예측이 가능한가를 정확히 알게될 것이고 그러면 기억에 오래 남기 때문이다.
Redux의 원칙 3가지
Redux의 원칙은 전개방식과 요소가 어떻게 필요한지를 슬쩍 암시해준다.
하나씩 설명하면서 원칙을 지켜야 하는 이유와 원칙을 따라가면서 얻게되는 이점을 찾아보자.
1. 앱의 상태값을 하나의 전역 저장소(스토어) 안에 객체 트리 구조로 저장한다.
객체 트리 구조가 뭐지?
{
users: [
{ id: 1 , name: '홍길동', gender: 'Male'},
{ id: 2 , name: '신사임당', gender: 'Female'},
{id: 3, name : '니체', gender: 'Male'},
{id: 4, name : '이완용' gender: 'Male'}
],
greaeFilter : 'SHOW_GREAT'
}
스토어에서는 예제처럼 여러 객체가 중첩된 구조를 이루면서 서로 다른 속성과 데이터 타입을 가질 수 있는데, 이는 우리가 익히 봐왔던 JSON 타입과 비슷하게 보인다.
이런 객체 트리 구조를 가진 스토어를 createStore를 통해 생성하고 생성한 스토어안에 여러 상태값을 저장한 뒤 Provider에 store를 넣어줌으로써 모든 컴포넌트에서 상태값에 쉽게 접근할 수 있다.
으.. Provider는 또 뭐야..
provider는 react 애플리케이션에서 제공하는 redux 스토어에 존재하는 매서드인데 이 친구는 redux의 스토어가 가지고 있는 상태 값을 하위 컴포넌트에 전파하는 역할을 맡고있다.
이런 진행 로직을 가지고 있어 상태값을 가져오는 시점을 정확히 알 수 있기 때문에 디버깅이 쉬워지고 전역에서 가져온 상태값은 직렬화 상태로 전달되어 추가적인 작업이 없이도 쉽게 사용할 수 있겠다.
store를 사용해서 상태값을 어디서든 가져올 수 있다는 것이 Redux에서는 상태값이 반드시 필요하다는 추측을 할 수 있겠다.
2. 상태값은 읽기 전용이다.
상태값은 읽기 전용이기 때문에 스스로 변할 수 있는 데이터가 아닌 불변객체이다..
만약 상태값을 교체해야만 하는 상황이 필요하다면 액션(action)을 사용해야 한다.
즉 상태값을 변경할 때는 개발자가 직접 어떠한 코드를 작성해야하고, 변경이 시작되는 시점부터 변경이 끝나 새로운 상태값으로 교체되는 순간까지가 모두 개발자의 개입이 필요하다.
슬슬 '예측 가능한'이 어떤 느낌인지 감이 잡힐 것 같은데, 추가로 설명을 이어나가겠다.
액션이라는 객체가 가진 보통의 형태는 이렇다.
{
type: 'INCREAMENT_COUNT'
payload : { id: '니체', count : 1}
}
payload는 아주 익숙하다. 상태값을 저장하는 store의 형식과 아주 유사한데, 이는 액션이 상태값을 어떻게 변화시킬지에 대한 정보를 담고 있기 때문이다.
물론 액션은 상태값을 변화시키는 출처이기 때문에 payload가 반드시 필요한 것은 아니다.
type을 넣어 액션을 식별할 수 있으면 액션 내의 로직으로도 변화를 시킬 수 있기 때문에 payload는 선택적으로 사용이 가능하다.
정리하자면 액션은 상태값을 변경시키기 위해 필요한 존재이며 어떤 값을 어떻게 변경시킬지 또한 개발자가 선택하여 입력할 수 있다.
1번에서 말하고 싶었던 주제가 있었는데, 이제 말할 수 있을 것 같다.
상태값은 불변하고 변화할 때는 외부의 존재가 개입해야 한다는 것.
그렇기 때문에 정확한 직렬화가 가능하고 추가로 디버깅의 난이도를 현저하게 떨어트리게 된다.
이로써 첫번째 원칙에서 추측한 상태값이 반드시 필요하다. 라는 것과 상태값을 변경시키기 위해서는 액션 또한 필요하다. 라는 Redux의 요소 두가지를 추측할 수 있게 되었다.
3. 변화는 순수 함수로 작성되어야 한다.
액션에 '의해' 상태값이 어떻게 '변화'하는지 지정하는 순수함수 리듀서(reducer)를 작성해야한다.
리듀서의 보편적인 형태는 이렇다
function reducer(state, action){
리턴할 변수 = 상태를 업데이트 시키는 로직
return 리턴할 변수;
}
1번과 2번에서 다룬 상태값과 액션을 사용해서 최종적으로 값을 변경하는 함수는 리듀서이다.
리듀서는 상태값과 액션을 전달인자로 받아 변경된 새로운 상태객체를 '생성'해서 반환해야하는 규칙이 적용된다.
즉, Redux의 모든 과정은 개발자가 개입하여 어떤 상태값을 어떻게 변경할지에 대한 액션 그렇게 지정한 상태값과 액션을 사용하는 리듀서까지 직접 지정을 해주는 것이다.
그렇다.
개발자는 Redux를 사용하면서 자연스럽게 어느 시점에서 어떻게 바뀌는지가 예측이 가능하다. 그래서 Redux는 예측이 가능한 상태관리 앱이 될 수 있는 것이다.
특히 Redux는 동작이 state -> action -> reducer -> state -> action -> reducer... 이렇게 단방향으로 진행되며,
순수 함수 reducer를 사용하기 때문에 앱 내에서 상태가 어떻게 변경되는지를 알 수 있어 큰 장점을 가지고 있다.
원칙을 통해 진행되는 방식을 알아보니 Redux에 대해 조금은 이해가 되는 것 같다.
전체적인 Redux의 Flow는 후술하여 이해를 도울 생각이다.
그러면 Redux를 사용하는 과정에서 state와 action, reducer는 알아봤으니 더 필요한 요소가 뭐가 있는지도 알아보자
대표적으로는 dispatch가 있다.
dispatch 함수는 Redux에서 상태값을 변경시키기 위해 사용되는 함수다.
???? 그거 리듀서로 한다고 했는데??
정확히 말하자면 dispatch는 액션 객체를 가져와서 리듀서에 전달을 한다.
??? 그냥 리듀서로 바로 하면 되는데 왜 ?
액션을 바로 리듀서에 보내면 편한 것 처럼 보이지만 Redux의 핵심은 예측 가능성과 상태값의 불변성을 유지하는 것이다. 또한 dispatch를 통해 액션을 전달하면 미들웨어를 활용할 수 있고 비동기 작업을 처리할 수 있기에 매우 중요하다. 그리고 Redux의 상태 관리 목적에도 적합하지 않다.
Redux의 상태 관리 목적은 복잡한 상태 관리를 효율적으로 해결하고 예측이 가능하도록 관리함으로써 유지보수성과 디버깅의 원활함을 목적으로 하기 때문에 dispatch를 거치는 것이다.
이 목적은 앱(프로젝트)의 규모가 커지거나 복잡해질 때 특히 더욱 효과를 크게 느낄 수 있다.
그렇기에 dispatch는 필수불가결한 존재이고 단순한 Redux의 동작에서 세부적인 조작 및 기능을 추가할 수 있다.
그럼 dispatch의 형태도 한번 알아보면 좋겠다.
dispatch는 store에서 참조하는 함수이므로 형태보다는 여태까지 본 Redux가 어떻게 작용하는지 전체적인 예시를 짧게 써보겠다.
const INCREMENT = 'INCREMENT'; // 액션을 인지하는 타입을 선언
const incrementAction = () => ({
type: INCREMENT // INCREMENT액션을 생성하는 함수
});
const counterReducer = ( count = 0, action) => {
//리듀서 함수 여기에는 state를 선언하는 부분이 없기 때문에 count=0을 통해 초기값을 선언
switch (action.type) {
case INCREMENT:
// 전달인자로 받은 action의 타입이 어떤 것이냐에 따라서 적용할 수 있도록 설정
return count + 1;
default:
// 원하는 액션이 들어오지 않았을 때 변경없이 기본값으로 리턴하는 default값
// (액션에 의해 변경되지 않은 본래 상태값
return count;
}
};
const store = createStore(counterReducer);
// 스토어를 생성하고 인자로 countReducer를 통해 state를 입력 (현재 count = 0)
console.log(store.getState());
// 출력값 = 0 아직 어떠한 액션을 전달받지 못했고 상태값의 변경이 일어나지 않았기 때문에
store.dispatch(incrementAction());
// 디스패치를 통해 액션을 전달하고 전달한 액션의 타입에 맞는 변경을
// Reducer에서 파악 후 원하는 액션 전개
console.log(store.getState());
// 출력값 = 1 increamentAction()은 INCREMENT라는 타입을 가지고 있고
// counterReducer의 case로 INCREMENT가 count의 값을 +1 해주는 액션이기 때문
이렇게 진행되는 코드를 보니 이해가 좀 더 명확하게 되는 것 같다.
앱 전체에서 Redux 작동하는 방식은 이렇다.
store에서 리듀서를 호출하고 리듀서는 return을 통해 초기 상태값을 지정하고 앱이 최초로 랜더링 할 때 store의 상태값을 랜더링하고 상태값이 업데이트 될 때 (디스패치 되었을 때 ) 받은 함수가 호출된다.
예를 들어, 상태값이 count = 0; 으로 초기에 지정되었을 때 onClick이벤트에 있는 dispatch 함수를 실행시키고 action을 발생시킨다. 그 후 store에서 지정된 count(상태값)와 action을 reducer에 전달하고, reducer는 받은 count와 action을 통해 변경된 값을 return한다. 물론 전달받은 action이 없거나 잘못된 경우에는 기존의 count를 반환하게 될 것이다.
그 후 store에서 상태값이 변경되었음을 확인하게 되면 변경된 요소들을 강제 리랜더링하여 화면에 변경된 값을 출력한다.
이로써 전반적인 Redux에 대한 설명은 마무리가 된 것 같다. Redux의 아주 기초적인 부분과 원칙, 요소들을 살짝 맛만 본 수준이지만 이 것을 이해하고 Redux를 사용한다면 훨씬 도움이 될 것이라고 생각한다.
모든 API와 라이브러리 등은 개발자에게 편의를 제공하기 위해서 나온 것들인데 역설적으로 공부를 하지 않고 "이렇게 쓰라니까 이렇게 써야지~" 하다보면 공부하는 시간보다 검색하는 시간이 더 걸리게 되는 것 같다.
다음에는 RestAPI에 대하여 포스트를 할까 생각중이다. 보는 모든 독자들에게 도움이 되었으면 좋겠다.