리덕스란, 자바스크립트 애플리케이션에서 상태를 효율적으로 관리할 수 있게 도와주는 도구입니다. 복잡한 상태 관리가 이루어지는 SPA(Single Page Application)에서 특히 유용하게 사용됩니다. 물론, 리덕스는 리액트 뿐만 아니라 jQuery, Angular 등을 사용하는 애플리케이션에서도 사용할 수 있지만, 이번 글을 리액트와 함꼐 사용하는 리덕스에 대해서 공부해보겠습니다.
리액트를 사용하면서, 상태 관리하는 것은 매우 중요한 요소 중 하나입니다. 만약, 부모 컴포넌트에서 상태를 전달받는 자식 컴포넌트들이 있는 아래 모습과 같은 상황이 있다고 가정해 봅시다.
리액트에서 데이터 흐름은 단방향이기 떄문에, 부모 컴포넌트 레이아웃 안에 존재하는 자식 컴포넌트들은 부모 컴포넌트의 상태를 props로 전달받게 되죠, 그러다 부모 컴포넌트의 상태값이 변하게 되면, 자식 컴포넌트들은 변화된 상태에 맞게 data 혹은 UI를 변경합니다. 즉, 리랜더링 과정을 거치게 됩니다.
여기서 잠깐, 컴포넌트는 다음과 같은 몇 가지 상황에서 리랜더링 됩니다.
자신의 상태(state)값이 변경될 때
부모 컴포넌트가 리랜더링될 때
자신이 전달받은 props값이 변경될 때, 강제 업데이트(forceUpdate)함수가 실행될 때
즉, 위와 같은 상황에서 부모 컴포넌트의 상태값이 변한다면, 부모 컴포넌트는 리랜더링 될 것이고, 자식 컴포넌트들 또한 리랜더링 되게 됩니다. 그렇기 때문에 우리는 상태를 관리하는 데 있어서 상태 구성 요소를 최소화하기 위해 노력해야 합니다.
리액트를 사용하면서, 상태 관리를 하는 것은 매우 중요한 요소 중 하나입니다. 리액트로 만들 수 있는 단일 페이지 애플리케이션(SPA, Single Page Application)는 data 혹은 UI의 변화가 복잡, 다양해지는 경우가 많아집니다. 그에 따라 단일 페이지를 이루는 컴포넌트들의 데이터 교류 또한 복잡해지기 때문에 이를 효율적으로 관리할 방법이 필요합니다. 리덕스는 이러한 복잡한 상태 관리를 효율적으로 할 수 있게 도와주는 도구입니다.
만약, 아래 사진과 같은 레이아웃의 SPA가 있다고 가정해 봅시다.
리액트의 데이터 흐름은 단방향이기 때문에, 부모 컴포넌트 레이아웃 안에 존재하는 자식 컴포넌트들은 부모 컴포넌트의 상태를 props로 전달받게 됩니다. 위와 같은 레이아웃을 다이어그램 형태로 바꾸어 보면 아래와 같은 모습이 됩니다.
부모의 컴포넌트의 state를 전달받을 지식 컴포넌트들의 깊이가 1층밖에 되지 않으니, 충분히 쉽게 상태를 전달하고 관리할 수 있을 것 같습니다. 하지만, 아래와 같이 자식 컴포넌트의 깊이가 깊어진다면, 이야기가 달라지게 됩니다.
위와 같은 레이아웃을 다이어그램 형태로 표현하지면, 아래와 같은 모습이 됩니다.
위의 다이어그램으로 부모-자식 관계의 데이터 흐름에 맞게 상태를 전달하는 예를 두 가지 들어보도록 하겠습니다.
위의 형태와 같이 부모 컴포넌트에서 최하위 자식 컴포넌트까지 상태를 전달해 주어야 한다면, 부모-자식-자식-최하위 자식 경로를 통해 상태를 전달해주어야 합니다. 부모와 최하위 자식 사이에 있는 자식 컴포넌트들과는 무관한 상태 변화이더라도 최하위 자식 컴포넌트에게 상트를 전달하기 위해선, 거쳐야 되는 컴포넌트가 되죠, 이러한 형태의 부모-자식 관계의 컴포넌트가 더 깊어진다면 분명 상태 관리를 하는 데 있어 훨씬 더 까다로워질 것같습니다.
또 다른 예를 들어볼까요?
좌측에 하단의 자식 컴포넌트에서 발생한 이벤트에 의해 다른 루트에 있는 컴포넌트의 상태 변화를 불러일켜야 하는 상태에 대한 관리가 발생한다면 이또한 까다로워질 수 밖에 없겠죠.
위와 같이 스토어를 사용하여 상태를 컴포넌트 구조 바깥에 두고 스토어라는 중간자를 통해 상태를 업데이트하거나, 새로운 상태를 전달받습니다.
즉, 리덕스를 사용하면, 위와 같이 상태값을 컴포넌트에 종속시키지 않고, 상태관리를 바깥에서 할 수 있게 해줍니다. 결국 리덕스를 통해 많아진 상태구성 요소들을 보다 효율적으로 관리 할 수 있게 된것이죠!
하나하나 차근차근 알아보도록 하겠습니다.
상태의 변화가 필요할 때 , 우리는 액션을 발생시킵니다. 이 액션은 하나의 객체로 표현됩니다. 가령, 할일 목록을 추가를 한다든지, 삭제를 한다면 추가, 삭제에 관한 액션의 타입 정의가 있어야합니다.
{
type: "ADD_TODO",
data: {
id: 0,
text: "redux"
}
}
액션은 Type이라는 필드를 필수적으로 가지고 있어야 하고, 추가적으로 필요한 객체의 요소들은 필요에 의해 추가될 수 있습니다. 위의 data 필드과 같습니다.
액션 생성함수라 불리는 Action Creator는 액션을 만드는 함수입니다. 파라미터를 입력받아, 액션 객체 형태로 만들어줍니다.
function addTodo(data) {
return {
type: "ADD_TODO",
data
};
}
이 함수는 data라는 파라미터를 입력받아, 액션을 객체 형태로 반환하는 역할을 합니다.
리듀서는 변화를 일으키는 함수입니다.즉, 업데이트 로직을 정의하는 함수입니다. 이 함수는 이전 상태와 액션을 파라미터로 입력받습니다. 그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.
function reducer(state, action) {
...
return alteredState;
}
반환값은 로직에 의해 변화된 상태 값을 반환합니다.
스토어는 컴포넌트 외부에 있는 상태 저장소입니다. 스토어 안에는 현재 상태들, 리듀서, 그리고 몇 가지의 내장 함수를 포함하고 있습니다.
디스패치는 스토어의 내장함수 중 하나로, 액션을 발생시키는 역할을 합니다. 디스패치가 액션을 발생시켜 스토어에게 상태 변화가 필요하다는 것을 알립니다.
그렇게 호출된 액션은 리듀서 함수를 호출시키고, 액션에 맞는 로직대로 상태를 변화시키는 과정을 거치는 것입니다.
G 컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.
이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.
상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 됩니다.
추가, 삭제와 같은 각각의 액션타입을 정의합니다. 액션 함수는 각각의 액션 타입과 파라미터를 입력받아 액션을 객체 형태로 반환해줍니다. 상태의 변화가 필요해진다면, 디스패치가 액션을 발생시켜 스토어에게 알립니다. 스토어로 전달된 액션은 스토어의 리듀서 함수를 호출시키고, 호출된 리듀서 함수는 이전 상태와 액션타입을 파라미터로 전달받아 정의된 로직대로 현재 상태값을 변화시켜 변화된 상태를 반환합니다. 반환된 상태는 스토어에 저장됩니다.
하나의 애플리케이션에는 하나의 스토어를 사용하는 것을 권장합니다.
배열 형태의 상태가 있는데, 로직에 의해 배열에 새로운 값을 넣어주어야 한다면, 기존 상태에 새로운 값을 직접 push하는 것이 아니라, concat과 같은 함수를 통해 새로운 값을 이어붙여 생성한 새로운 배열로 교체하여 업데이트해야 합니다. 이러한 업데이트 방식을 사용하면 불변성이 유지됩니다.
불변성이 유지된 업데이트는 리덕스의 변화 감지 방법인 shallow equality에 적합한 상태 변화이기 때문에 이러한 업데이트 방법을 사용합니다.
순수한 함수라는 것은 동일한 입력을 받았을 때 언제나 동일한 출력을 내는 함수를 말합니다. 우리가 클릭을 통해 액션을 발생시키면, 배경색이 랜덤으로 바뀌는 로직을 작성한다면, 함수의 로직 내에서 랜덤값을 생성해야 합니다. 랜덤값을 생성한다는 뜻은 결국 매번 출력이 바뀐다는 뜻이기 때문에 순수하지 못한 함수입니다.
또한, 네트워크의 요청을 하는 작업도 마찬가지로 순수하지 못한 로직 중 하나입니다.
위의 설명만 보면은 Redux와 Context API는 사실상 차이가 거의 없어보입니다. 둘 다 전역 상태 관리를 위한 도구라는 공통점을 가지고 있기 때문이죠. 사실 Redux 또한 Context API를 가지고 만든 라이브러리입니다. 전역 상태 관리 측면에서는 차이점이 거의 없다고 봐도 무방하다는 의미입니다. (Context API는 high-frequency updates에 좋지 않은 성능을 보이지만 Redux는 그렇지 않습니다)
하지만 Redux는 Context API와 다르게 전역 상태 관리외에 다양한 기능을 제공합니다.
위 항목들은 모두 Redux가 Context API에 비해 가지는 강점입니다. redux-saga, redux-thunk, redux-devtools 등 다양한 추가 라이브러리를 통해 우리가 조금 더 상태 관리를 수월하게하고 긴밀하고 정확한 코딩을 할 수 있도록 합니다. 여러 라이브러리가 모여 Redux라는 하나의 프레임워크가 되어 개발자에게 큰 도움을 주고 있는 것으로 볼 수 있습니다. 하지만 Context API는 이런 부가적인 기능을 제공하지 않아 다른 라이브러리를 통해 구현해야합니다.
Redux가 많은 기능을 제공하지만 Context API에 비해 작성해야하는 코드도 많고 복잡하기 때문에 이런 부가 기능이 필요하지 않다면 Redux를 사용하지 않아도 됩니다.
https://velog.io/@youthfulhps/What-is-Redux-and-why-use-it