Redux 구성요소
- Store
- State
- Action
- Redux State에 변화를 주기 위한 행동 == Redux Store 에 저장된 데이터에 변화를 주기 위한 행동
- Redux Action은 JavaScript 객체 형태로 존재한다.
- Action Creator
- Action을 생성하는 생성자 역할
- Action 객체를 생성하는 역할을 하는 JavaScript 함수
- Reducer
- 컴퓨터 프로그래밍 분야에서 Reduce는 입력에 어떤 처리를 해서, 원하는 결과로 축소시키는 과정을 의미
- 따라서 Reducer 는 Action 이 발생하면, Action을 실제로 처리하는 역할을 하는 함수
Redux 구성요소와 동작 방식
- Redux 동작 방식을 구성 요소와 함께 설명하면,
- 사용자가 버튼 클릭 → Redux Action 발생 → Reducer가 Action 처리 → State 변화
Immutability
- Redux의 데이터는 읽기 전용이며 맘대로 바꿀 수 없고, State 변화는 Pure function을 통해 이루어져야 한다는 원칙이 있다.
- 이 때 불변성(Immutability)가 적용된다.
- Redux State 는 읽기 전용이다 → State를 변경하기 위해서는 새로운 State를 생성해야 한다.
- 새로운 State를 생성 하는 과정은 Reducer 라고 불리는 Pure function 을 통해 이루어진다.
- 위 그림처럼, Reducer는 이전 상태에 정해진 어떤 변화를 준 뒤에 다음 상태를 리턴하게 된다.
- 이 과정에서 Pure function 처럼 작동한다는 것은, 입력으로 받은 이전 상태를 직접 변경하는 것이 아니라 새로운 상태 객체를 만들어서 리턴한다는 것이며, 또한 같은 입력에 대해서는 항상 같은 결과를 리턴한다는 뜻이다.
- 즉, Redux State 생성 후에는 값을 바꿀 수 없고, 값을 바꾸기 위해서는 새로운 State를 변경해야 한다는 것!
- JavaScript 이러한 불변성을 적용하기 위해 사용하는 immer 라는 라이브러리가 있다.
- state에 불변성을 적용하기 편리하게 도와주는 도구이며,
- 흔히 사용하는 Redux Toolkit 은 불변성을 지키기 위해 내부적으로 immer를 사용하고 있다.
Redux를 사용하는 경우
- 하나의 상태를 여러 컴포넌트에서 접근해야 할 경우
- 하나의 state에 수많은 컴포넌트가 접근해야할 필요가 있을 때, 이 state를 특정 컴포넌트의 Local state로 선언해서 관리한다면 값을 가져다 쓰기에 굉장히 불편하다.
- 예를 들어, 여러 컴포넌트에서 사용자의 로그인 여부에 따라서 다른 UI를 보여주고자 할 때, 로그인 여부를 나타내는 state에 접근이 필요하다.
- 이러한 경우, 로그인 여부를 Redux 를 통해 관리한다면 모든 컴포넌트들이 손쉽게 로그인 여부를 알 수 있다.
- 한 곳에서 상태들을 관리하고 싶은 경우
- 리액트로 개발을 하다보면 수많은 컴포넌트와 state들이 생기게 되는데, Redux 를 사용하면 상태들을 Redux Store라고 불리는 하나의 저장소에서 편리하게 관리할 수 있다.
- 하지만, 모든 State를 Redux Store에 넣는 것이 정답은 아니다.
- 의도치 않게 특정 state에 접근할 수 있는 통로를 열어주게 되어서, 예상치 못한 side effect 가 생길 수 있기 때문
- 그래서 꼭 필요한 state (여러 컴포넌트에서 접근해야 하는 state)만 Redux 를 통해 관리하는 것이 좋다.
Redux vs Context API
Context API
- Context API는 2018년에 리액트 버전 16.3에 정식으로 포함되어 릴리즈 되었다.
- Context는 리액트 컴포넌트들 사이에서 데이터를 기존의 props를 통해 전달하는 방식 대신 component tree를 통해 곧바로 컴포넌트로 전달하는 새로운 방식을 제공한다.
- 이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있다.
- 아래 그림은 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 일반적인 방식을 보여준다.
- 이 방식의 단점은 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터(예: 로그인 여부, 프로필 정보 등)를 전달하려면 반복적인 코드가 많이 생기고 지저분해진다는 것이다.
- 예를 들어, 위 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면 최소 2번을
props
로 전달해야 하며, 만약 데이터를 전달하려는 컴포넌트가 10단계 밑에 있다면 10번이나 props
를 타고 하위 컴포넌트로 내려가야 한다.
- 이러한 불편한 점을 개선하기 위해 생겨난 것이 바로 Context이다.
- 아래 그림은 방금 전과 동일한 기능을 구현하기 위해 Context를 사용한 것이다.
- Context를 사용하면 일일이
props
로 전달할 필요 없이, 이 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있다.
- 여기까지만 보면 Context가 하는 역할과 용도는 Redux와 매우 유사하다.
- 두 기술 모두 여러 단계에 걸쳐서 props를 통해 데이터를 전달하는 prop drilling이라고 불리는 문제를 해결하기 위한 방법이라는 점
- 실제로 내부적으로 Redux는 리액트 Context API를 사용하여 컴포넌트 Tree를 따라 스토어에 있는 데이터들을 전달 한다.
Redux와 Context API의 차이점
- Redux를 사용하게 되면, 앞에서 설치했던
redux-devtools
라는 강력한 도구를 사용할 수 있다.
redux-devtools
는 모든 상태의 변화를 시각적으로 확인할 수 있게 해주며, 이전 상태로 돌아가서 하나씩 Action을 실행하면서 디버깅 할 수도 있기 때문에, 애플리케이션 규모가 커질수록 꼭 필요한 기능이다.
- Context API는 특정 Context에 의존하는 컴포넌트들을 분리시킬 수 있다.
- 여러 개의 Context를 만들고 해당 Context와 관련이 있는 컴포넌트들만을 묶어서 분리시킬 수 있다는 것
- 이렇게 분리를 시키면 해당 Context와 관련이 없는 컴포넌트는 애초에 데이터에 접근이 불가능해지기 때문에, 의도치 않게 발생할 수 있는 side effect를 사전에 방지할 수 있다.
- 데이터를 처리하는 방식
- Redux는 전체 애플리케이션의 데이터를 Redux Store라고 불리는 하나의 거대한 객체로 관리하고, 사전에 정의된 Action과 Reducer를 통해서만 상태를 변경할 수 있다.
- Context는 오로지 하나로 구성되어 있지도 않을 뿐만 아니라, 데이터를 별도로 관리하지도 않는다.
- state를 들고 있는 것이 아니라 state를 전달하기 위한 통로의 역할만 하는 것
- 만약 Context API를 사용해서
state
를 업데이트 하려면, 상위 컴포넌트의 state
에 의존할 수밖에 없다. (상위 컴포넌트에서 state
를 업데이트하고, 변경된 값을 Context라는 통로를 통해 하위 컴포넌트로 전달만 함)
일반적으로 규모가 크고 관리해야할 상태가 많다면 Redux를 사용하고, 규모가 작고 관리해야 할 상태가 적다면 Context API를 사용한다.
[실습] Redux를 사용하여 Counter app 만들기
- Redux는 JavaScript 라이브러리이기 때문에 React와 관계없이 JavaScript를 사용하는 곳이라면 어디든지 연동해서 사용할 수 있다.
html
파일에 script
로 Redux 를 불러오고, Counter app 을 구현해보자.
<!DOCTYPE html>
<html>
<head>
<title>Counter</title>
<script src="https://unpkg.com/redux@4.2.1/dist/redux.min.js"></script>
</head>
<body>
<p>Counter: <span id="value">0</span></p>
<div>
<button id="increment">+</button>
<button id="decrement">-</button>
</div>
<script>
function counter(state, action) {
if (typeof state === "undefined") {
return 0;
}
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
}
var store = Redux.createStore(counter);
var valueElem = document.getElementById("value");
function render() {
valueElem.innerHTML = store.getState().toString();
}
render();
store.subscribe(render);
document
.getElementById("increment")
.addEventListener("click", function () {
store.dispatch({ type: "INCREMENT" });
});
document
.getElementById("decrement")
.addEventListener("click", function () {
store.dispatch({ type: "DECREMENT" });
});
</script>
</body>
</html>
References