Redux를 공부한 내용을 기록하고자 정리해보았습니다. 잘못된 부분이 있다면 댓글 부탁드립니다! 🙌
Redux
를 한마디로 정의하자면 자바스크립트 앱에서 예측 가능한 상태 관리를 가능하게 하는 컨테이너 입니다.
React
에선 컴포넌트 내에서 State를 관리 합니다. 형제 컴포넌트 간 데이터를 주고 받을 경우, 부모 컴포넌트에 State를 정의해 부모 컴포넌트를 통해서 주고 받습니다.
형제 컴포넌트 간 직접적인 데이터 전달은 불가능 합니다.
만약, 컴포넌트의 갯수가 많아지고, 데이터를 주고 받는 경우가 많아진다면, 거쳐야할 컴포넌트가 많아집니다. 그 때마다, 부모 컴포넌트에서 Props
로 데이터를 전달하는 것은 생산성과 코드의 가독성을 떨어뜨립니다.
이를 Redux
로 개선할 수 있습니다.
Redux
를 사용하기 전에, 세가지 원칙을 항상 기억하고 준수해야 합니다.
단방향 데이터 흐름
을 위해, 데이터를 저장하는 하나뿐인 저장 공간 Store
에서 데이터를 관리해야 합니다.
React
에서도 State
를 변경할 때, State
에 새로운 값을 할당하는 것이 아니고, setState
를 사용해 State
를 변경했습니다. Redux
도 마찬가지로, 불변성을 지키기 위해, Action
을 통해 State
를 업데이트합니다.
State
를 갱신하는 함수는 오직 순수 함수로만 갱신합니다.
Redux
는 Reducer
와 Flux
를 합친 단어입니다.
Flux
에 대해 간단하게 정리해보겠습니다.
Flux
는 데이터가 이동하는 패턴입니다.
그림에서 보시듯, 단방향 데이터 흐름입니다.
View
는 우리가 보는 화면, UI
입니다.
Store
는 데이터 및 상태가 저장되는 공간입니다.
Action
은 데이터 및 상태를 업데이트하기 위한 행동입니다.
Dispatcher
는 Action
을 Store
로 전달하는 전달자의 역할을 수행합니다.
그래서, 단방향 데이터 흐름인 Flux
패턴을 간략히 정리하자면,
View
에서 상태를 변경하기 위한 이벤트가 발생하면, 그 로직에 맞는 Action
을 Dispatcher
의 전달인자로 전달합니다.
Action
을 담은 Dispatcher
는 Store
로 전달하고, Store
에서 Action
을 수행하여 상태를 업데이트합니다.
업데이트된 저장 공간은 다시 View
를 통해 사용자에게 보여집니다.
그렇다면, Reducer
는 Flux
패턴에서 추가된 개념인데, 이 Reducer
는 어떤 역할을 할까요?
먼저, Redux
의 단방향 데이터 흐름을 간략히 짚어보겠습니다.
아직도 저 개념이 잘 와닿지 않아 제 나름대로의 예시를 생각해보았습니다. 적절한 예시가 아니라면, 피드백 부탁드립니다! 🙏
귀살대
귀멸의 칼날 세계관에서 혈귀를 죽이기 위해 만들어진 비공인 준 군사조직
구분 | View(UI) | Store | Action | Dispatcher | Reducer |
---|---|---|---|---|---|
예시 | 귀멸의 칼날 배경(세상) | 귀살대 | 임무 | 꺽쇠 까마귀 | 귀살대원 |
귀멸의 칼날의 배경은 다이쇼 시대라고 합니다.View
는 우리가 보는 귀멸의 칼날의 배경(세상) 입니다.
이 세상에서 어떤 사건(이벤트)가 발생합니다. 귀멸의 칼날 세계관에서의 사건은 민간인이 혈귀에게 잡아 먹힌 경우가 될 수 있겠습니다.
민간인이 혈귀에게 잡아먹히는 사건이 발생했습니다. 귀살대의 당주 우부야시키 카가야
는 이 소식을 듣고, 임무(Action
)를 정의합니다. 임무는 다음과 같습니다.
임무(action) {
수행자(type) : '렌고쿠 쿄쥬로',
대상(payload) : {
민간인을 죽인 혈귀 1
}
}
귀살대원들에게 이를 전달하기 위해, 꺽쇠 까마귀(Dispatcher
)에게 임무를 전달합니다.
꺽쇠 까마귀로부터 임무를 전달받은 귀살대원(Reducer
)들은 임무의 내용을 확인하고, 수행자(type
)가 자신이 아니면 임무를 수행하지 않습니다.
누가 수행해야 하는 임무인가?
임무의 수행자로 지정된 귀살대원은 임무를 수행합니다.
임무 수행에 있어 실패한다는 가정은 배제하겠습니다. 맡은 책무를 다 해야하니까요.
임무가 해결되면, 귀살대(Store
)에 업데이트됩니다. 민간인을 죽인 혈귀는 귀살대원이 죽였고, 귀멸의 칼날 세상은 그 혈귀가 없어진 새로운 상태로 갱신되었습니다.
여기서 Reducer
의 예시로 귀살대원을 들었습니다.
Reducer
는 Action
의 type
에 따라 Action
을 수행합니다. 이를 swith case
또는 if
조건문으로 구분할 수 있는데, 아래와 같이 Reducer
를 작성해볼 수 있습니다.
const 귀살대원 = (이 세상의 혈귀들, 임무) => {
switch (임무.수행자) {
case '렌고쿠 쿄쥬로': // 임무의 수행자가 렌고쿠라면 아래와 같이 수행한다.
return Object.assign({}, 이 세상의 혈귀들.filter((혈귀) => 혈귀.이름 !== 임무.대상)
//이 세상의 혈귀들의 목록 중, 임무에 담긴 대상을 제외한 나머지 혈귀들을 새로 반환한다.
default:
return 이 세상의 혈귀들;
} // 수행자가 렌고쿠가 아니라면, 임무를 수행하지 않는다.
}
예시가 적절한가요? 💁
뭔가 글을 쓰다보니 오히려 Redux
를 더 혼란스럽게 만드는 예시인 것 같습니다... 😓
Reducer
는 데이터 흐름에서 다음과 같은 위치에 위치합니다.
View -> Action -> Dispatcher -> Reducer(s) -> Store -> View
공식 문서의 말을 빌리면, Reducer
는 기본적으로 Action
에서 정보를 가져와 Store
에 저장된 이전 상태와 함께 새로운 상태로 축소(Reduce)
합니다.
위에 기술한 Flux
패턴과 다른 점은, Reducer
에 조건을 설정하여, 인자로 들어온 Action
의 타입에 맞는 Action
만 실행하고, 조건에 부합하지 않는 Action
이라면 기본값을 반환한다는 것입니다.
그래서, Reducer
를 아래와 같이 구현할 수 있습니다.
(prevState, action) => newState
첫번째 인자로 현재 상태를 받고, 두번째 인자로 Action
을 받습니다.
Action
의 구성은 아래와 같습니다.
{
type: 'TODO_ADD',
todo: { id: '0', name: 'learn redux', completed: false },
}
Reducer
의 두번째 인자로 들어온 Action
의 type
에 따라, 해당하는 type
에 대한 조건이 Reducer
에 존재한다면 함수를 실행하는 것입니다.
즉, Redux
에서 Action
은 type
을 꼭 지정해줘야합니다. 그 아래에 있는 데이터를 가리키는 부분은 payload
라 하여, 문자열부터 객체까지 다양한 payload
를 담을 수 있습니다.
if
조건문을 사용하거나, switch case
를 사용하여 결정할 수 있습니다.
쇼핑몰 홈페이지에서 장바구니에 물건을 추가하는 상황을 예시로 들어보겠습니다.
const itemReducer = (state, action) => {
switch (action.type) {
case ADD_TO_CART: // action type이 ADD TO CART인 경우
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
// state 데이터에 payload로 들어온 값을 추가한 새로운 배열을 할당하여 반환합니다.
default:
return state;
} // 다른 type의 Action이 들어오면 기본값을 반환합니다.
}
이렇게 Reducer
와 Action
을 구성할 수 있습니다. 이제, 구성한 요소들을 컴포넌트와 연결을 해야 합니다. 연결하기 위해 Redux hooks
를 사용할 수 있습니다.
useSelector()
useSelector()
는 컴포넌트와 State
를 연결하는 역할을 합니다. 이 메소드를 통해 Store
의 State
에 접근할 수 있습니다.
이 메소드를 적용하기 위해, Presentational component
와 Container component
개념을 알아야합니다.
Presentational component
는 사용자에게 어떻게 보여질 지의 기능만 수행하는 컴포넌트입니다.
Container component
는 사용자가 어떤 이벤트를 발생시키거나, 상태의 변화가 발생할 때 이를 어떻게 동작할 지, 그 기능을 수행하는 컴포넌트입니다. 즉, useSelector()
메소드는 이 Container component
에서 사용하는 것으로 State
에 접근할 수 있습니다.
useDispatch()
useDispatch()
는 Action
객체를 Reducer
로 전달해주는 메소드입니다. 이 메소드가 정의되는 위치는 Action
을 실행시키는 클릭, 키보드 입력 등 이벤트가 발생되는 컴포넌트가 될 수 있습니다.
글을 마무리하기 전에, Redux
를 사용하는 것으로 얻을 수 있는 이점들을 정리해보겠습니다.
Reducer
는 순수 함수로 작동하기 때문에, 다음 상태가 어떻게 변화될 지 예측이 가능합니다.
즉, 테스트 케이스를 작성하기 용이합니다.
컴포넌트가 많아져 State
또는 데이터를 Props
로 전달한다면, 컴포넌트에서 컴포넌트로, 컴포넌트에서 컴포넌트로... 소위 Props Drilling
이 발생합니다. Props
를 전달하는 과정에서 버그가 발생하면, Props
를 가진 모든 컴포넌트를 수정해야합니다.
Redux
를 통해 전역 상태에서 데이터를 관리한다면, Action
과 State
의 로그를 기록할 수 있어, 어떤 Action
에서 문제가 발생했는지 추적이 가능합니다.
제가 공부한 내용을 바탕으로 제 언어로 풀어서 정리해보았습니다. 잘못된 부분이 있다면 피드백 부탁드리며, 긴 글 읽어주셔서 감사드립니다! 😘