리액트애서 애플리케이션을 만들 때, 기본적으로는 보통 하나의 루트 컴포넌트 (App.js) 에서 상태를 관리합니다. 예를들어서, 투두리스트 프로젝트에서는, 다음과 같은 구조로 상태가 관리되고 있죠.
리액트 프로젝트에서는 대부분의 작업을 할 때 부모 컴포넌트가 중간자 역할을 합니다.
컴포넌트 끼리 직접 소통 하는 방법은 있긴 하지만, 그렇게 하면 코드가 굉장히 많이 꼬여버리기 때문에 절대 권장되지 않는 방식입니다. (ref 를 사용하서 이러한 작업을 할 수 있긴 하죠.)
App 에서는 인풋의 값인 input
값과, 이를 변경하는 onChange
함수와, 새 아이템을 생성하는 onCreate
함수를 props 로 Form 에게 전달해줍니다. Form 은 해당 함수와 값을 받아서 화면에 보여주고, 변경 이벤트가 일어나면 부모에게서 받은 onChange
를 호출하여 App 이 지닌 input
값을 업데이트 하죠.
그렇게 인풋 값을 수정하여 추가 버튼을 누르면, onCreate 를 호출하여 todos 배열을 업데이트 합니다.
todos 배열이 업데이트 되면, 해당 배열이 TodoItemList 컴포넌트한테 전달이 되어 화면에 렌더링 되죠.
이런식으로, App 컴포넌트를 거쳐서 건너건너 필요한 값을 업데이트 하고, 리렌더링 하는 방식으로 프로젝트가 개발됩니다.
이러한 구조는, 부모 컴포넌트에서 모든걸 관리하고 아래로 내려주는 것익 때문에, 매우 직관적이기도 하고, 관리하는 것도 꽤 편합니다. 그런데 문제는 앱의 규모가 커졌을 때 입니다.
보여지는 컴포넌트의 개수가 늘어나고, 다루는 데이터도 늘어나고, 그 데이터를 업데이트 하는 함수들도 늘어나겠죠. 그렇게 가다간 App 의 코드가 엄~ 청 나게 길어지고 이에 따라 유지보수 하는 것도 힘들 것입니다.
예를 들어 다음과 같은 구조의 프로젝트가 있다고 생각해봅시다.
Root 컴포넌트에서 G 컴포넌트에게 어떠한 값을 전달해 줘야 하는 상황에는 어떻게 해야 할까요?
A 를 거치고 E 를 거치고 G 를 거쳐야 합니다. 아! 근데 이걸 또 코드로 작성해가면서 해야하죠.
// App.js 에서 A 렌더링
<A value={5}>
// A.js 에서 E 렌더링
<E value={this.props.value} />
// B.js 에서 G 렌더링
<G value={this.props.value} />
그러다가 value 라는 이름을 anotherValue 라는 이름으로 바꾸는 일이 발생한다면요? 파일 3개를 열어서 다 수정해줘야하죠.
리덕스에 대한 설명은 여러가지 방식으로 할 수 있겠지만 저는 주로 이런 표현을 합니다. 리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 됩니다.
그림으로 설명하자면 다음과 같은 구조죠.
예를 들어서 B 에서 일어나는 변화가 G 에 반영된다고 가정을 해봅시다.
리덕스를 프로젝트에 적용하게 되면 이렇게 스토어 라는 녀석이 생깁니다. 스토어 안에는 프로젝트의 상태에 관한 데이터들이 담겨있죠.
G 컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.
이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.
예를들어 { type: 'INCREMENT' }
이런 객체를 전달 받게 된다면, 리덕스 스토어는 아~ 상태에 값을 더해야 하는구나~ 하고 액션을 참조하게 됩니다.
추가적으로, 상태값에 2를 더해야 한다면, 이러한 액션 객체를 만들게 됩니다:
{ type: 'INCREMENT', diff: 2 }
그러면, 나중에 이 diff 값을 참고해서 기존 값에 2를 더하게되겠죠. type 를 제외한 값은 선택적(optional) 인 값입니다. 액션에 대해선 나중에 더 자세히 알아볼게요.
액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야겠죠? 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부릅니다. 이 함수는 나중에 우리가 직접 구현하게 됩니다. 예를들어 type 이 INCREMENT 라는 액션이 들어오면 숫자를 더해주고, DECREMENT 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 되죠.
리듀서 함수는 두가지의 파라미터를 받습니다.
그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 되죠.
정리하자면 이렇습니다. 기존에는 부모에서 자식의 자식의 자식까지 상태가 흘렀었는데, 리덕스를 사용하면 스토어를 사용하여 상태를 컴포넌트 구조의 바깥에 두고, 스토어를 중간자로 두고 상태를 업데이트 하거나, 새로운 상태를 전달받습니다. 따라서, 여러 컴포넌트를 거쳐서 받아올 필요 없이 아무리 깊숙한 컴포넌트에 있다 하더라도 직속 부모에게서 받아오는 것 처럼 원하는 상태값을 골라서 props 를 편리하게 받아올 수 있죠.