일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Recoil
- component
- 반응형웹
- docker
- 웹팩
- typescript
- cicd
- Redux
- graphql
- javascript animation
- 정규표현식
- scrapping
- socket.io
- 성능최적화
- sequelize
- 포트포워딩
- express
- npx
- react
- go
- styled-component
- CDN
- AWS
- 웹크롤링
- Modal
- 회고
- route
- Today
- Total
프로그래밍 공부하기
Redux 본문
Redux란 데이터를 한 곳에서 관리할 수 있게 도와주는 JavaScript Library이다. 보통 React와 Redux를 함께 사용하지만 사실 Vanilla JavaScript에서도 Redux를 사용할 수 있다는 의미이다. 그렇다면 왜 Redux가 필요할까?
Redux의 장점
과거 프로젝트 보편적으로 사용되던 개발 디자인 패턴 중 하나는 MVC 패턴이다. 프로젝트를 모델-뷰-컨트롤러로 나누어 UI영역과 데이터 처리영역을 분리하여 개발하였다. 그런데 모델과 뷰가 많아질 수록 데이터의 흐름이 뒤엉켜 예측하기 어려운 코드를 발생시키는 단점이 있다.
하나의 데이터를 여러 뷰가 공유하고 있는 경우, 뷰1에서 이벤트가 발생하였을 때 이를 뷰2, 뷰3...에 알려주어야하는 로직이 필요하다. 또한 모델1의 데이터가 바뀌었을 때 다른 모델을 업데이트 해야하는 케이스도 발생할 수 있다. 심지어 데이터 변경이 비동기적으로 발생한다면 더더욱 복잡한 코드가 만들어질 것이다. 이를 어찌저찌 코드로 구현하였다 하더라도 기능들이 추가/수정될 때 수정한 부분과 관련 있는 데이터와 뷰들도 영향을 받을 수 있으므로 예상하지 못한 오류가 발생하고, 이를 해결하는 과정에서 코드 또한 원래의 의도를 파악하기 어려워 진다.
(참고 글에 의하면 양방향 흐름이 있는 위와 같은 그림의 구조는 이상적인 MVC 패턴은 아니다. 그러나 간단한 로직의 경우 종종 view 안에서 일부 로직을 처리하는 방식으로 구현하는 경우가 있기 때문에 양방향으로 표현한 것이다.)
반면 FLUX패턴은 단일 방향 데이터 흐름을 가진다. FLUX는 새로운 데이터가 들어오면 데이터 흐름이 처음부터 시작되어야 한다. 따라서 데이터 흐름이 뒤엉키지 않으며 코드를 예측하기 쉬워지고 데이터의 일관성이 향상된다. 이러한 FLUX 패턴의 컨셉이 적용된 라이브러리가 Redux이다.
(사실 Redux는 디스패쳐 개념이 없으므로 완전히 FLUX와 동일한 것은 아니다. 참고글)
React에서 Redux를 자주 쓰는 이유는 무엇일까? React는 위에서 아래로 단방향의 데이터 흐름을 가지므로 부모에서 가장 아래의 자식까지 데이터를 전달해 주기 위해 수많은 노드(컴포넌트)를 거쳐야한다. 그러나 데이터를 별도의 한 장소(Redux)에서 관리한다면 데이터를 전달받기 위해 중간 컴포넌트들을 거치는 과정을 생략할 수 있다!
Redux 예시
import { createStore } from "redux";
const counter = document.querySelector("span");
const add = document.querySelector("#add");
const minus = document.querySelector("#minus");
counter.innerText = 0;
const countReducer = (count = 0, action) => {
switch (action.type) {
case "ADD":
return count + 1;
case "MINUS":
return count - 1;
}
};
const countStore = createStore(countReducer);
const onChange = () => {
counter.innerText = countStore.getState();
}
countStore.subscribe(onChange);
add.addEventListener("click", () => countStore.dispatch({ type: "ADD" }));
minus.addEventListener("click", () => countStore.dispatch({ type: "MINUS" }));
add 버튼을 누르면 counter가 증가하고 minus 버튼을 누르면 counter가 감소하는 예시 코드이다. 위 코드를 작성할 때 나는 다음과 같은 과정으로 작성하였다.
const countStore = createStore();
먼저 앱에서 변화하는 데이터인 counter를 countStore라는 하나의 장소에서 관리하기로 하였다. 이는 위와 코드로 표현할 수 있다.
const countReducer = (count = 0) => {
//1. ADD를 클릭하면 count +1
//2. MINUS를 클릭하면 count -1
return count;
};
const countStore = createStore(countReducer);
Store에는 데이터의 변경을 처리하는 Reducer라는 함수가 필요하다. Reducer는 어떤 경우에는 데이터를 어떻게 처리할 것인지와 관련된 로직이 들어있다. 데이터를 처리한 후 변경된 데이터를 return하면 해당 데이터는 return된 값으로 변경된다. 또한 count의 초기 값도 Reducer에서 지정할 수 있다. 이 때 Redux의 목표 중 하나는 코드를 예측 가능하게 만드는 것이므로 파일 저장, HTTP 요청 난수 생성 등의 행동은 권장되지 않는다. Reducer는 countStore 생성 시 Reducer로 전달하여 등록한다.
add.addEventListener("click", () => countStore.dispatch({ type: "ADD" }));
minus.addEventListener("click", () => countStore.dispatch({ type: "MINUS" }));
이제 각 버튼에 click 이벤트가 발생하면 reducer의 로직이 호출될 수 있도록 코드를 작성한다. 이 때 사용되는 메소드는 dispatch이다. Store의 dispatch 메소드를 호출하면 해당 Store에 연결된 Reducer가 호출된다. 이 때 dispatch의 인자로 객체를 넘겨주면 해당 객체가 reducer로 넘어간다. 즉, dispatch할 때 넘겨준 인자를 통해 Reducer가 데이터 변경이 어떤 곳에서 발생되었는지를 구분할 수 있는 것이다. 이 때, dispatch의 인자 객체는 type이라는 프로퍼티가 존재해야 한다.
const countReducer = (count = 0, action) => {
switch (action.type) {
case "ADD":
return count + 1;
case "MINUS":
return count - 1;
}
};
dispatch로 전달된 인자는 reducer에서 action이라는 이름으로 불리운다. action.type을 통해 ADD 라면 +버튼, MINUS라면 -버튼이 눌렸다는 것을 알 수 있으므로 위와 같이 분기된 로직을 작성할 수 있다. 새로운 state를 리턴할 때 주의할 점은 변형된 state를 리턴하면 안된다는 것이다. 즉, 원본을 변형시키는 count++가 아닌, count+1을 리턴해야 한다.
const onChange = () => {
counter.innerText = countStore.getState();
}
countStore.subscribe(onChange);
마지막으로 Store의 subscribe 메소드는 데이터가 변화된 사실을 감지하였을 때 어떤 작업을 수행할 것인지를 지정할 수 있다. 위 코드의 경우 countStore에 있는 count가 변화한다면 onChange메소드를 호출할 것이다. 그리고 onChange 메소드는 DOM을 조작하여 사용자에게 변경된 데이터를 UI로 보여준다. 데이터의 현재 상태는 store의 getState() 메소드로 가져올 수 있다.
리덕스의 흐름은 위와 같이 정리할 수 있다. Dispatch를 통해 Reducer를 호출하고, Action 객체를 Reducer에 넘긴다. Reducer는 Action객체를 활용하여 새로운 State를 생성하는 것이다.
Redux in React
React에서 Redux는 어떻게 적용은 다음과 같이 진행한다.
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
첫 번째로 counter 예시와 똑같이 createStore로 Store를 만든다. createStore에는 reducer가 입력될 수 있으며, counter 예시에선 사용하지 않았지만, 두 번째 인자로 미들웨어를 넣어줄 수 있다.
const rootReducer = combineReducers({
itemReducer,
notificationReducer
});
store에 등록된 reducer는 다음과 같다. 위 코드이 경우 2개의 reducer를 등록하기 위해 combineReducers를 사용하였다.
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
최상위 컴포넌트인 App을 Provider로 감싼 후 store 속성을 넣어주어 App 하위 컴포넌트들이 store를 사용할 수 있도록 한다.
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return [...state, action.payload];
case REMOVE_FROM_CART:
return [];
default:
return state;
}
}
다음으로 reducer를 만들어 준다. counter 예제와 동일한 방식으로 만들 수 있다.
const state = useSelector(state => state.itemReducer);
const { items, cartItems } = state;
return (
{items.map((item, idx) => <Item item={item} key={idx} handleClick={() => {
handleClick(item)
}} />)}
);
state 값을 쓰고 싶은 컴포넌트에서 useSelector()를 이용하여 state를 가져와서 사용한다.
const dispatch = useDispatch();
dispatch({type: REMOVE_FROM_CART });
state 값을 변경하고 싶은 곳에서 useDispatch()를 사용한다. 내 경우처럼 reducer가 여러개인 경우 모든 reducer에 dispatch의 인자인 객체(action)가 전달되며, action.type에 의해 분기되어 처리된다.
참고 사이트
'Web > [JS] FrontEnd' 카테고리의 다른 글
Modal 만들기 (0) | 2021.03.27 |
---|---|
전체 화면 애니메이션 만들기 (0) | 2021.03.27 |
이벤트 버블링과 이벤트 캡처 (0) | 2021.01.17 |
innerHTML, innerText, textContent (0) | 2020.12.30 |
CDN과 Javascript Library (0) | 2020.12.29 |