heyday2024 님의 블로그
[React 숙련 1주차(4)] Redux: useSelector, dispatch action 객체, payload, ducks 본문
[React 숙련 1주차(4)] Redux: useSelector, dispatch action 객체, payload, ducks
heyday2024 2024. 11. 6. 14:51useState 사용 시, 어떤 컴포넌트에서 생성한 state를 다른 컴포넌트로 보고자 할 때,
Props를 통해서 부모 컴포넌트에서 자식 컴포넌트로 그 값을 보내주었음.
이 과정에서 크게 3가지 불편한점이 발견됨:
1. 컴포넌트에서 컴포넌트로 State를 보내기위해서는 반드시 부-모 관계가 되어야함!
2. 조부모 컴포넌트에서 손자 컴포넌트로 값을 보내고자 할때도 반드시 부모 컴포넌트를 거쳐야함.
즉, 정작 부모컴포넌트에서는 그 값이 필요가 없어도 단순히 손자 컴포넌트에게 전달하기 위해 불필요하게 거쳐야만 하는 것을 의미.
(조부모 → 부모 → 손자)
3. 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없음.
==> 이러한 useState의 불편함 때문에 만들어진 도구가
Redux
리덕스는 상태 관리를 돕기 위한 라이브러리로, 컴포넌트 간 상태를 전역적으로 관리하고 공유할 수 있게함!!!
- 리덕스 사용으로 State를 공유하고자 할때 부-모 관계가 아니여도 되고, 중간에 의미없이 컴포넌트를 거치지 않아도 됨.
- 또한, 자식 컴포넌트에서 만든 State를 부모 컴포넌트에서도 사용할 수 있게됨.
Global state vs Local state
- Local state (지역상태) 란?
- 컴포넌트에서 useState를 이용해서 생성한 state. 좁은 범위 안에서 생성된 State..
- Global state (전역상태)란?
- Global state는 컴포넌트에서 생성되지 않음. 중앙화 된 특별한 곳에서 State들이 생성됨.=> “중앙 state 관리소”
==> Global state는 애플리케이션 전반에 걸쳐 일관성을 유지하는 데 도움을 주지만, 남용하면 불필요한 렌더링을 일으킬 수 있음. Local state는 해당 컴포넌트의 독립성을 높이지만, 공유가 어려움
중앙 State관리소에서 State를 생성하고, 만약 어떤 컴포넌트에서 State가 필요하다면 컴포넌트가 어디에 위치하고 있든 상관없이 State를 불러와서 사용 할 수 있게 된다.
==> 이렇게 특정 컴포넌트에 종속되어 있는 것이 아니라 “중앙 state 관리소”에서 생성된 State를 Global state라고 한다. 그리고 이러한 값들을 관리하는 것을 전역 상태 관리 라고 함.
<참고: Redux 구성요소 간략히 정리>
- Store: 애플리케이션의 모든 상태를 저장하는 단일 저장소임으로, 여러 컴포넌트에서 상태를 일관되게 접근 가능함.
- Action: 상태 변화를 일으키는 순수 객체로, type과 payload로 구성됨.
- Reducer: 상태 변화를 정의하는 함수임으로, 현재 상태와 액션을 인자로 받아서 새로운 상태를 반환함.
- Dispatch: 특정 액션을 스토어에 전달해 상태를 변경함.
Redux는 이러한 구조를 통해 상태 관리의 예측 가능성을 높이고, 코드의 유지보수성을 향상시킴
근데 우린 이미 이전에 위와 같은 문제 때문에 useContext 썼는데요????
Context API가 있음에도 Redux로 Global state를 관리하면 좋은 이유?
1. 성능 최적화
- Context API: 상태가 변경되면 해당 Provider의 하위 모든 컴포넌트가 리렌더링됨. 이로 인해 불필요한 렌더링이 발생할 수 있으며, 성능에 악영향을 미칠 수 있음. 따라서 이러한 문제를 해결하기 위해 복잡한 최적화가 필요함. 예를 들어, 메모이제이션(React.memo)을 통해 특정 컴포넌트를 리렌더링하지 않도록 관리해야 함.
- Redux: 상태 변경 시, 선택적으로 관련된 컴포넌트만 업데이트됨. connect 함수를 사용하여 특정 상태에만 구독할 수 있기 때문에 성능 관리가 더 용이함. 이를 통해 불필요한 렌더링을 줄이고 애플리케이션의 성능을 최적화할 수 있음.
2. 상태 로직의 중앙화와 일관성
- Context API: 상태가 여러 컴포넌트에 걸쳐 분산될 수 있으며, 각 컴포넌트에서 필요한 상태를 개별적으로 관리해야 함. 이로 인해 상태 로직의 일관성이 떨어질 수 있고, 복잡한 애플리케이션에서는 관리가 어려워질 수 있음.
- Redux: 모든 상태를 단일 저장소(store)에 저장함으로써 상태 로직이 중앙화됨. 모든 상태 변경은 리듀서(reducers)를 통해 처리되기 때문에 일관성이 높아지고, 예측 가능한 상태 관리가 가능함. 이로 인해 디버깅 및 테스트가 더 쉬워짐.
3. 강력한 미들웨어와 개발 도구
- Context API: 기본적으로는 미들웨어를 지원하지 않음. 비동기 작업이나 복잡한 로직을 처리하기 위해 추가적인 구현이 필요함. 따라서 복잡한 기능을 구현하는 데 있어 한계가 있을 수 있음.
- Redux: 다양한 미들웨어(예: redux-thunk, redux-saga)를 지원하여 비동기 작업, 로깅, 상태 변경에 대한 추가 처리를 쉽게 구현할 수 있음. 또한, Redux DevTools와 같은 강력한 개발 도구를 제공하여 상태 변화를 시각적으로 모니터링하고, 과거 상태로 롤백하는 등의 기능을 지원함. 이를 통해 개발자는 상태 변화와 애플리케이션 동작을 더욱 쉽게 이해하고 관리할 수 있음. ==>미들웨어: 애플리케이션의 요청과 응답 사이에서 처리되는 중간 소프트웨어 계층
리덕스란,
“중앙 state 관리소”를 사용할 수 있게 도와주는 패키지(라이브러리) .
==> "프론트엔드 개발자들은 “리덕스”를 전역 상태관리 라이브러리 라고 많이 표현합니다. 전역 상태, 즉 Global State를 의미하고 그것을 관리하게 도와주는 라이브러리 (패키지) 이기 때문입니다."
위와 같이 폴더 구조를 생성하세요.
- src 폴더 안에 redux 폴더를 생성
- redux 폴더 안에 config, modules 폴더를 생성
- config 폴더 안에 configStore.js파일을 생성
각각의 폴더와 파일은 역할이 있습니다.
- redux : 리덕스와 관련된 코드를 모두 모아 놓을 폴더 입니다.
- config : 리덕스 설정과 관련된 파일들을 놓을 폴더 입니다.
- configStore : “중앙 state 관리소" 인 Store를 만드는 설정 코드들이 있는 파일 입니다.
- modules : 우리가 만들 State들의 그룹이라고 생각하면 됩니다. 예를 들어 투두리스트를 만든다고 한다면, 투두리스트에 필요한 state들이 모두 모여있을 todos.js를 생성하게 되텐데요, 이 todos.js 파일이 곧 하나의 모듈이 됩니다.
Provider를 가져올 때, react-redux와 react에서 사용하는 Provider는 서로 다름. 각 라이브러리에서 제공하는 Provider의 역할이 다르기 때문임.
1. react의 Provider
- 용도: React의 Context API에서 제공하는 Provider로, 특정 컨텍스트의 값을 하위 컴포넌트에 제공하는 데 사용됨.
import React, { createContext } from 'react';
const MyContext = createContext();
const MyProvider = ({ children }) => {
const value = {}; // 제공할 값
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
};
2. react-redux의 Provider
- 용도: Redux의 스토어를 React 애플리케이션에 연결하기 위해 사용하는 Provider임. Redux의 store를 애플리케이션 전체에 제공하여, 하위 컴포넌트에서 스토어의 상태와 액션을 사용할 수 있게 함.
import { Provider } from 'react-redux';
import store from './store';
const App = () => {
return (
<Provider store={store}>
<MyComponent />
</Provider>
);
};
모듈의 구성요소 살펴보기
- initialState === 초기 상태값.
- 초기값은 꼭 객체가 아니어도 됩니다. 배열이 되어도 되고, 그냥 원시데이터가 돼도 됩니다. 그리고 객체에도 여러개의 변수를 넣어줄 수 있습니다.
// 초기값이 0
const initialState = 0;
// 초기값이 0이 있는 배열
const initialState = [0];
// 초기값이 number = 0, name = '석구'인 객체
const initialState = {
number: 0,
name: '석구'
};
- Reducer === 변화를 일으키는 함수.
- 현재 상태와 액션을 인자로 받아서 새로운 상태를 반환
-
// 리듀서 const counter = (state = initialState, action) => { switch (action.type) { default: return state; } };
- 우리가 useState()를 사용할 때, number라는 값을 바꾸고 싶으면 setNumber를 사용했습니다.
==> 리덕스에서는 리듀서가 이 역할을 합니다.// 예시 코드 const onClickHandler = () => { setNumber(number + 1); // setState를 이용해서 state 변경 }
- 우리는 리듀서 인자 첫번째 자리에서는 state를, 두번째 자리에서는 action 이라는 것을 꺼내서 사용할 수 있습니다.
// src/redux/modules/counter.js
// counter 리듀서
const counter = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default counter; // 여기
- 카운터 모듈을 스토어에 연결하기
- configStore.js에 아래 코드를 추가하면, 스토어와 모듈이 연결됩니다. 이렇게 스토어와 모듈을 연결시키는 코드는 우리가 모듈을 추가할 때마다 똑같이 진행해주면 됩니다.
// src/redux/config/configStore.js
// 원래 있던 코드
import { createStore } from "redux";
import { combineReducers } from "redux";
// 새롭게 추가한 부분
import counter from "../modules/counter";
const rootReducer = combineReducers({
counter: counter, // <-- 새롭게 추가한 부분
});
const store = createStore(rootReducer);
export default store;
스토어와 모듈 연결 확인하기
- useSelector = 스토어 조회
우리가 생성한 모듈을 스토어에 잘 연결했는지 확인하는 방법은 컴포넌트에서 스토어를 직접 조회하면 됩니다
react-redux에서 제공하는 useSelector 라는 훅을 사용합니다.
// 1. store에서 꺼낸 값을 할당 할 변수를 선언합니다.
const number =
// 2. useSelector()를 변수에 할당해줍니다.
const number = useSelector()
// 3. useSelector의 인자에 화살표 함수를 넣어줍니다.
const number = useSelector( ()=>{} )
// 4. 화살표 함수의 인자에서 값을 꺼내 return 합니다.
// 우리가 useSelector를 처음 사용해보는 것이니, state가 어떤 것인지 콘솔로 확인해볼까요?
const number = useSelector((state) => {
console.log(state)
return state
});
- 컴포넌트에서 스토어를 조회할 때:
- 브라우저를 켜고, 콘솔을 보면 아래 이미지처럼 객체가 보이고, 그 안에 counter 라는 값이 있는 것을 볼 수 있습니다. 우리가 만든 counter 라는 모듈의 state가 보이는 것을 알 수 있습니다. 이렇게 화살표 함수에서 꺼낸 state라는 인자는 모든 리덕스 모듈의 최상위 객체를 의미하는 Root State인 것입니다. 이제 우리는 어떤 컴포넌트에서도 접근 할 수 있는 스토어를 가지게 되었습니다. 만약 우리가 컴포넌트에서 number라는 값을 사용하고자 한다면 아래 코드처럼 꺼내서 사용하면 됩니다.
// src/App.js
import React from "react";
import { useSelector } from "react-redux"; // import 해주세요.
const App = () => {
const counterStore = useSelector((state) => state); // 추가해주세요.
console.log(counterStore); // 스토어를 조회해볼까요?
return <div></div>;
}
export default App;
- view에서 액션이 일어난다.
- view 로직 안에서 dispatch를 이용해서 action을 reducer로 전달함.
- action에 의한 reducer 함수가 실행되기 전에 middleware가 작동함.
- middleware에서 명령내링 일을 수행한 후, reducer 함수를 실행함.
- reducer의 실행 결과를 store에 저장함
- 그러면서 자연스럽게 중앙 저장소인 store에 그 결과값이 없데이트된것임으로 store의 state에 subscribe하고 있던 모든 UI에 변경된 값을 전달함.
<counter.js 모듈의 state 수정 기능 만들기 (+ 1 기능 구현해보기)>
어떻게 counter.js 모듈에 있는 state의 값을 변경할 수 있을까?
리덕스에서는 값의 수정은 리듀서에서 일어남!
만약에 counter.js 모듈에 있는 number에 +1을 하고 싶다면?
- 리듀서에게 보낼 number를 +1 하라는 “명령”을 만든다.
- 명령을 보낸다.
- 리듀서에서 명령을 받아 number +1을 한다.
(1) 리듀서에게 보낼 “명령” 만들기
리듀서에게 number에 +1을 하라고 명령을 보내야함.
- 리덕스에서는 그 명령을 Action 이라고 함.
- 즉, 리듀서에게 내가 어떤 action을 하길 원하는 데 그 표현(명령)을 미리 정해주는 것.
===> 액션 객체로 표현해줄 수 있음.
액션 객체는 반드시 type이라는 key를 가져야 함.
왜냐하면 우리가 이 액션 객체를 리듀서에게 보냈을 때 리듀서는 객체 안에서 type이라는 key를 보기 때문.
// 예시 코드
//number에 +1 을 하는 액션 객체
{ type : "PLUS_ONE" };
앞으로 우리는 리덕스 모듈에 있는 state을 변경하기 위해서는 그에 해당하는 액션 객체를 모두 만들어줘야 함.
(2) “명령”(액션 객체) 보내기
이제 명령을 만들었으니, 우리는 리듀서에게 액션객체를 보내야 함.
==> useDispatch라는 훅을 이용해서 리듀서에게 해당 액션 객체를 보낼 수 있음.
- react-redux에서 import 해서 사용할 수 있으며, 우리가 만든 액션 객체를 리듀서로 보내주는 역할을 하는 훅임.
==> useDispatch라는 훅을 사용하기 위해서는 컴포넌트 안에서 아래와 같이 먼저 코드를 작성해서 dispatch라는 변수를 생성해줘야함. 이렇게 생성한 dispatch는 함수이고, 이 dispatch를 사용할 때 () 를 붙여서 함수를 실행하게 됨!
// src/App.js
import React from "react";
import { useDispatch } from "react-redux"; // import 해주세요.
const App = () => {
const dispatch = useDispatch(); // dispatch 생성
return (
<div>
<button>+ 1</button> {/* 버튼을 하나 추가해주세요. */}
</div>
);
};
export default App;
그리고 dispatch를 사용할 때 ( ) 안에 액션객체를 넣어주면 됨.
// src/App.js
import React from "react";
import { useDispatch } from "react-redux"; // import 해주세요.
const App = () => {
const dispatch = useDispatch(); // dispatch 생성
return (
<div>
<button
// 이벤트 핸들러 추가
onClick={() => {
// 마우스를 클릭했을 때 dispatch가 실행되고, ()안에 있는 액션객체가 리듀서로 전달된다.
dispatch({ type: "PLUS_ONE" });
}}
>
+ 1
</button>
</div>
);
};
export default App;
위처럼 디스패치를 이용해서 액션객체를 리듀서로 보낼 수 있음.
(3) dispatch를 이용해서 보낸 액션객체를 받기
// src/redux/modules/counter.js
// 초기 상태값
const initialState = {
number: 0,
};
// 리듀서
const counter = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
// 모듈파일에서는 리듀서를 export default 한다.
export default counter;
리듀서에 action을 콘솔로 찍어보겠습니다.
// src/redux/modules/counter.js
// 초기 상태값
const initialState = {
number: 0,
};
// 리듀서
const counter = (state = initialState, action) => {
console.log(action); // 여기에 console.log(action) 추가
switch (action.type) {
default:
return state;
}
};
// 모듈파일에서는 리듀서를 export default 한다.
export default counter;
리듀서있는 action은 우리가 App.js에서 dispatch로 보낸 그 액션객체임을 알 수 있습니다.
마지막으로, 액션객체 명령대로 리듀서가 state값을 변경하는 코드 구현하기
- 컴포넌트로부터 dispatch를 통해 액션객체를 전달 받는다.
- action 안에 있는 type을 스위치문을 통해 하나씩 검사해서, 일치하는 case를 찾는다.
- type과 case가 일치하는 경우에, 해당 코드가 실행되고 새로운 state를 반환(return) 한다.
- 리듀서가 새로운 state를 반환하면, 그게 새로운 모듈의 state가 된다.
// src/modules/counter.js
// 초기 상태값
const initialState = {
number: 0,
};
// 리듀서
const counter = (state = initialState, action) => {
console.log(action);
switch (action.type) {
// PLUS_ONE이라는 case를 추가한다.
// 여기서 말하는 case란, action.type을 의미한다.
// dispatch로부터 전달받은 action의 type이 "PLUS_ONE" 일 때
// 아래 return 절이 실행된다.
case "PLUS_ONE":
return {
// 기존 state에 있던 number에 +1을 더한다.
number: state.number + 1,
};
default:
return state;
}
};
// 모듈파일에서는 리듀서를 export default 한다.
export default counter;
- action이 {type: “PLUS_ONE”} 이기 때문에, 리듀서 안에 있는 스위치문은 action.type을 조회히고 이것이 일치하면 로직을 기반으로 새로운 state반환.
추가적으로....useSelector로 변경된 state값 확인하기
// src/App.js
import React from "react";
import { useDispatch, useSelector } from "react-redux";
const App = () => {
const dispatch = useDispatch();
// 👇 코드 추가
const number = useSelector((state) => state.counter.number);
console.log(number); // 콘솔 추가
return (
<div>
{/* 👇 코드 추가 */}
{number}
<button
onClick={() => {
dispatch({ type: "PLUS_ONE" });
}}
>
+ 1
</button>
</div>
);
};
export default App;
우리가 의도한대로 버튼을 누를때마다 number가 1씩 증가는 것을 화면에서도, 콘솔에서도 볼 수 있음.
정리
- 액션객체란, 반드시 type이란 key를 가져야 하는 객체이다. 또한 리듀서로 보낼 “명령"이다.
- 디스패치란, 액션객체를 리듀서로 보내는 “전달자” 함수이다.
- 리듀서란, 디스패치를 통해 전달받은 액션객체를 검사하고, 조건이 일치했을 때 새로운 상태값을 만들어내는 “변화를 만들어내는" 함수이다.
- 디스패치(dispatch)를 사용하기위해서는 useDispatch() 라는 훅을 이용해야 한다.
- 디스패치는 스토어의 내장함수 중 하나입니다.
- 우선, 디스패치는 액션을 발생 시키는 것 정도로 이해하시면 됩니다.
- dispatch 라는 함수에는 액션을 파라미터로 전달합니다.. dispatch(action) 이런식으로 말이죠.
- 액션객체 type의 value는 대문자로 작성한다. (JS에서 상수는 대문자로 작성하는 룰이 있음)
만약에 액션객체의 value를 변경할 일이 생긴다면?
PLUS_ONE, MINUS_ONE 이라는 value 대신 이 액션객체가 counter 모듈안에 있음을 강조하기 위해 counter/를 각각의 value앞에 붙여주길 원함. 근데 이렇게 수정하려면 각각의 PLUS_ONE, MINUS_ONE이 쓰였던 부분들을 다 수정해줘야할 것임.(만약 수정해야할 곳이 100, 200, 300 군데... 라면??? 😨😨)
그리고...내가 과연 이 과정에서 PLUS를 PLU 이런식으로 써서 에러를 안 낼 자신이 있는가...😭😭.?
하드 코딩으로 발생할 수 있는 이러한 human error 때문에
액션 객체를 한 곳에서 관리할 수 있도록 "함수"와 액션 value를 상수로 만들기 시작함!!
===> Action creator (특정 액션 객체를 쉽게 만들기 위해 사용하는 함수)
// src/modules/counter.js
// 추가된 코드 👇 - 액션 value를 상수들로 만들어 줍니다. 보통 이렇게 한곳에 모여있습니다.
const PLUS_ONE = "PLUS_ONE";
const MINUS_ONE = "MINUS_ONE";
// 추가된 코드 👇 - Action Creator를 만들어 줍니다.
export const plusOne = () => {
return {
type: PLUS_ONE,
};
};
export const minusOne = () => {
return {
type: MINUS_ONE,
};
};
// 초기 상태값
const initialState = {
number: 0,
};
// 리듀서
const counter = (state = initialState, action) => {
switch (action.type) {
case PLUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다.
return {
number: state.number + 1,
};
case MINUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다.
return {
number: state.number - 1,
};
default:
return state;
}
};
export default counter;
==> 이런 식으로 action value를 상수로 만들었음, 그리고 action creator 함수가 추가됨.
<만들어진 action creator 사용해보기>
1. export 된 action creator를 import하기
2. dispatch()에 있던 액션객체 지우고, action creator를 넣어서, 이 함수를 통해 생성됭 객체 이용하기.
// src/App.js
import React from "react";
import { useDispatch, useSelector } from "react-redux";
// 사용할 Action creator를 import 합니다.
import { minusOne, plusOne } from "./redux/modules/counter";
const App = () => {
const dispatch = useDispatch();
const number = useSelector((state) => state.counter.number);
return (
<div>
{number}
<button
onClick={() => {
dispatch(plusOne()); // 액션객체를 Action creator로 변경합니다.
}}
>
+ 1
</button>
{/* 빼기 버튼 추가 */}
<button
onClick={() => {
dispatch(minusOne()); // 액션객체를 Action creator로 변경합니다.
}}
>
- 1
</button>
</div>
);
};
export default App;
왜 Action creator를 사용해야 하나요?
(1) 휴먼에러 (오타) 방지
액션객체의 type value를 상수로 만들어놓았기 때문에, 개발툴에서 자동완성등의 보조 기능을 지원받을 수 있습니다. 그래서 의도치 않은 휴먼에러(오타)를 없앨 수 있어요.
(2) 유지보수의 효율성 증가
우리가 만든 Action Creator가 만약 100군데에서 쓰이고 있는 상태에서 혹여나 그것을 바꾸어야 하는 상황이 오더라도 단 한번의 수정으로 100군데에 모든 수정사항을 반영할 수 있습니다.
(3) 코드 가독성
모듈 파일에서 Action Creator가 일목요연하게 정리가 되어있으면, 내가 아닌 다른 개발자가 보았을 때 해당 모듈이 가지고 있는 모든 Action들을 한눈에 알 수 있게 됩니다. 즉 그 자체가 Action 들의 리스트업을 해주는 역할을 갖게 되는 것 입니다.
(4) 리덕스 공식문서에서 소개되고 있는 방법
리덕스팀에서도 위와 같은 사유에 근거하여 공식적으로 안내하고 있는 방법일 것 입니다.
만약 1로 정해진 기능을 만드는 것이 아니라 증가 시킬 숫자를 카운터 프로그램을 사용하는 사용자가 직접 정할 수 있게 기능을 만드려면 어떻게 해야 할까요?
위를 가능하게 하려면 사용자가 “N을 더해” 라고 N을 같이 리듀서에서 보내야 함. 하지만, 지금까지 사용했던 type은 데이터의 유형을 정한 것이고, 각 유형에 따라 정해놓은 조건문에만 맞춰서 실행되었음, N이라는 받은 정보를 직접적으로 활용해야할 때 우리는 payload를 사용함.
Payload
액션 객체가 전달하는 데이터....
type | 액션의 유형을 정의하고 리듀서에서 조건문을 통해 사용됨 | (ex) "INCREMENT_BY" |
payload | 리듀서가 상태 변경에 사용하는 실제 데이터 | (ex) 5 (또는 { count: 5 }) |
==> type은 액션의 성격을 정의하고, payload는 그 액션이 수행할 구체적인 데이터를 제공하는 차이가 있음.
-
이렇게 State를 변경하는데 있어 만약 리듀서에게 어떤 값을 같이 보내줘야 한다면 payload를 액션객체에 같이 담아 보낸다.// payload가 추가된 액션객체 {type: "ADD_NUMBER", payload: 10} // type뿐만 아니라 payload라는 key와 value를 같이 담는다.
<참고> 왜 payload라는 이름을 쓰는가?? 사실 굳이 payload라는 이름을 딱 정해서 쓸 필요는 없음
{type: "ADD_NUMBER", num: 10} // ??
{type: "ADD_NUMBER", number: 10} // ??
{type: "ADD_NUMBER", data: 10} // ??
{type: "ADD_NUMBER", myNumber: 10} // ??
{type: "ADD_NUMBER", myNum: 10} // ??
{type: "ADD_NUMBER", payload: 10}
==> payload가 아닌 다른 프로퍼티 이름을 사용해도 상관없음. 하지만, "커뮤니티 best practice" 로 공유되면서 많은 개발자들이 데이터는 payload라는 프로퍼티에 담아주고 있기 때문에 일반적으로 payload라고 씀. (커뮤니티 컨벤션)
<payload를 이용하여 기능 구현하기?
- 사용자가 입력한 값을 받을 input 구현하기
- Action Creator 작성하기
- 리듀서 작성하기
- 구현된 기능 테스트 하기
//리듀서라는 함수를 만들거임
const initialState = {
number: 0,
};
//초기값(객체)--> initialState가 꼭 객체일 필요는 없은 어떤 타입에 값도 가능
// 변화를 일으키는 함수 === 리듀서 함수
// 변화의 종류는 action.type에 담겨있음
// action.type 별로 case를 나눠서 switch구문에 활용가능
// 나중에 이 reducer로 중앙 state, 즉, store를 변화시킬 수 있음.
// 리듀서는 꼭 return문으로 값을 반환해주어야함!!!
// action creator 만들기
const ADD_NUMBER = "ADD_NUMBER";
const REMOVE_NUMBER = "REMOVE_NUMBER";
export const addNumber = (payload) => {
return {
type: ADD_NUMBER,
payload
};
}
export const removeNumber = (payload) => {
return {
type: REMOVE_NUMBER,
payload
};
};
const counter = (state = initialState, action) => {
// type: 액션 객체의 속성 중 하나로, 어떤 종류의 상태 변경을 요청하는지를 나타내는 문자열임
// 상태를 업데이트하기 위해 액션을 디스패치할 때, 각 액션은 특정한 타입을 가지며, 이 타입을 기반으로 리듀서가 상태를 어떻게 업데이트할지를 결정하게 됨.
switch (action.type) {
case ADD_NUMBER:
return {
number: state.number + action.payload
};
case REMOVE_NUMBER:
return {
number: state.number - action.payload,
};
default:
return state;
}
};
export default counter;
- counter.js
- payload가 필요한 Action Creator에서는 함수를 선언할 때 매개변수 자리에 paylaod를 넣어줘야 합니다. 왜냐하면 Action Creator를 사용하는 컴포넌트에서 리듀서로 보내고자 하는 payload를 인자로 넣어줘야 하기 때문입니다.
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addNumber, removeNumber } from "./redux/modules/counter";
import { useSelector } from "react-redux";
const App = () => {
const counterReducer = useSelector((state) => {
return state.counter;
})
const [input, setInput] = useState('');
const dispatch = useDispatch();
return (
<div>
{counterReducer.number} <br />
<input type="text" value={input} onChange={(e) => {
return setInput(+e.target.value);
}} />
<button
onClick={() => {
dispatch(addNumber(input));
}}
>
+1
</button>
<button
onClick={() => {
dispatch(removeNumber(input));
}}
>
-1
</button>
</div>
);
};
export default App;
- App.jsx
Ducks 패턴이란?
Ducks 패턴은 Redux 앱을 구성할 때 사용하는 방법론 중 하나로, 일반적으로 분산되어 있던 액션 타입, 액션 생성자, 리듀서를 하나의 파일로 구성하는 방식을 말합니다. Redux 관련 코드의 관리를 보다 간결하고 모듈화하여 관리할 수 있도록 돕습니다.
- Erik Rasmussn 라는 개발자가 이것을 패턴화하여 작성하는 것을 제안...
Duck 패턴으로 작성하기
Erik Rasmussen 이 제안한 Ducks 패턴은 아래의 내용을 지켜 모듈을 작성하는 것 입니다.
- Reducer 함수를 export default 한다.
- Action creator 함수들을 export 한다.
- Action type은 app/reducer/ACTION_TYPE 형태로 작성한다.
// counter.js (Ducks 패턴을 사용하는 카운터 모듈 예시)
// 1. 액션 타입 정의 (네임스페이스 포함)
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
// 2. 액션 생성자 정의
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
// 3. 초기 상태 정의
const initialState = {
count: 0,
};
// 4. 리듀서 정의
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
}
(외부 라이브러리로서 사용될 경우 또는 외부 라이브러리가 필요로 할 경우에는 UPPER_SNAKE_CASE 로만 작성해도 괜찮다.)
그래서 모듈 파일 1개에 Action Type, Action Creator, Reducer 가 모두 존재하는 작성방식입니다.
'프론트엔드 부트캠프' 카테고리의 다른 글
[React 숙련 1주차(6)] React Router DOM : hooks, Dynamic Route, useParams, 중첩된 라우트 (1) | 2024.11.07 |
---|---|
[React 숙련 1주차(5)] Redux: todolist, RTK (1) | 2024.11.07 |
[React 숙련 1주차(3)] Memoization - (React.memo, useCallback, useMemo), Custom Hooks (0) | 2024.11.05 |
[React 숙련 1주차(2)] 리액트 훅: useState, useEffect, useRef, useContext (4) | 2024.11.05 |
[React 숙련 1주차(1)] Styled-Components (2) | 2024.11.04 |