Unit4 - [React] 상태 관리
Redux
컴포넌트와 상태를 분리하여 전역에서 상태 관리를 해줄 수 있게 해주는 상태 관리 라이브러리
상태는 동적으로 표현되는 데이터이다.
Side Effect
함수의 입력 외에도 함수의 결과에 영향을 미치는 요인
ex) 네트워크 요청, API 호출
Presentation 컴포넌트
어떤 데이터가 들어오는지 상관하지 않고, 설사 데이터가 가짜 데이터라 할지라도 컴포넌트는 표현(presentation) 그 자체에 집중
상태를 구분
로컬 상태, 전역 상태
로컬 상태는 특정 컴포넌트 안에서만 관리되는 상태이며,
전역 상태는 프로덕트 전체 혹은 여러 가지 컴포넌트가 동시에 관리하는 상태
다른 컴포넌트와 데이터를 공유하지 않는 폼(form) 데이터는 대부분 로컬 상태입
ex)input box, select box
전역 상태는 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태
ex) 상품선택여부 현재 선택한 탭 장바구니에 담긴 물품
서로 다른 컴포넌트가 사용하는 상태의 종류가 다르면 서로 다른 출처가능
서로 다른 컴포넌트가 사용하는 상태의 종류가 같다면 , 같은 출처가 가능
하나의 출처 = 전역공간
데이터 무결성?
테이터의 정확성을 보장하기 위해 데이터의 변경,수정시 제한을 두어
안정성 저해를 막고 데이터 상태들을 항상 옳게 유지한다.
=> 동일한 데이터는 항상 같은 곳에서 데이터를 가지고 온다.
상태관리 툴 문제 해결
*전역 상태 저장소 제공
*props drilling(프로퍼티 내려꽂기) 문제를 해결
상태 관리 툴이 없어도 충분히 규모 있는 애플리케이션을 만들 수 있다.
Props Drilling
Props Drilling은 상위 컴포넌트의 state를 props를 통해 전달하고자 하는 컴포넌트로 전달하기 위해 그 사이는 props를 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 데이터를 전달하는 현상
문제점
- 코드의 가독성이 매우 나빠진다..
- 코드의 유지보수 또한 힘들어진다.
- state 변경시 Props 전달 과정에서 불필요하게 관여된 컴포넌트들 또한 리렌더링이 발생하여 웹성능에 악영향을 줄 수 있다.
해결방법
state는 될 수 있으면 가까이 유지
상태관리 라이브러리를 사용
=>전역으로 관리하는 저장소에서 직접 state를 꺼내 쓸수 있다.(Redux, Context api, Mobx, Recoil 등)
예제1
mport React, { useState } from 'react';
import styled from 'styled-components';
const Container = styled.div`
border: 5px solid green;
padding: 10px;
margin: 10px;
position: relative;
`;
const Quantity = styled.div`
text-align: center;
color: red;
border: 5px solid red;
padding: 3px;
font-size: 1.2rem;
`;
const Button = styled.button`
margin-right: 5px;
`;
const Text = styled.div`
color: ${(props) => (props.color ? props.color : 'black')};
font-size: ${(props) => (props.size ? props.size : '1rem')};
font-weight: ${(props) => (props.weight ? '700' : 'inherit')};
`;
export default function App() {
const [number, setNumber] = useState(1);
const plusNum = () => {
setNumber(number + 1);
};
const minusNum = () => {
setNumber(number - 1);
};
console.log('Parents');
return (
<Container>
<Text weight size="1.5rem">
[Parents Component]
</Text>
<Text>
Child4 컴포넌트에 있는 버튼을 통해
<br /> state를 변경하려고 합니다.. 🤮
</Text>
<Text weight color="tomato">
Props Driling이 발생!!
</Text>
<Quantity>{`수량 : ${number}`}</Quantity>
<Child1 plusNum={plusNum} minusNum={minusNum} />
</Container>
);
}
function Child1({ plusNum, minusNum }) {
console.log('Child1');
return (
<Container>
<Text>[Child 1 Component]</Text>
<Child2 plusNum={plusNum} minusNum={minusNum} />
</Container>
);
}
function Child2({ plusNum, minusNum }) {
console.log('Child2');
return (
<Container>
<Text>[Child 2 Component]</Text>
<Child3 plusNum={plusNum} minusNum={minusNum} />
</Container>
);
}
function Child3({ plusNum, minusNum }) {
console.log('Child3');
return (
<Container>
<Text>[Child 3 Component]</Text>
<Child4 plusNum={plusNum} minusNum={minusNum} />
</Container>
);
}
function Child4({ plusNum, minusNum }) {
console.log('Child4');
return (
<Container>
<Text>[Child 4 Component]</Text>
<Button onClick={plusNum}>👍</Button>
<Button onClick={minusNum}>👎</Button>
</Container>
);
}
예시2
function Child3({ greeting, setGreeting }) {
console.log('Child3');
return <Component>Child3 : {greeting} </Component>;
}
function Child6({ greeting, setGreeting }) {
console.log('Child6');
return (
<Component>
Child6
<button onClick={() => setGreeting(greeting + '!')}>👋</button>
</Component>
);
}
vs
function Child3() {
const greeting = useSelector((state) => state);
console.log('Child3');
return <Component>Child3 : {greeting} </Component>;
}
//Child3 컴포넌트는 Redux에서 상태를 가져오기 위해 useSelector를 사용하고 있다.
상태를 출력하고 있다.
function Child6() {
console.log('Child6');
const dispatch = useDispatch();
const addBang = () => {
dispatch({ type: 'AddBang' });
};
return (
<Component>
Child6
<button onClick={addBang}>👋</button>
</Component>
);
//Child6 컴포넌트는 Redux에서 상태를 업데이트하기 위해 useDispatch를 사용
//AddBang 타입으로 dispatch하는 addBang 함수와 버튼을 출력
dispatch는 Redux에서 액션을 발생시키는 함수이다.
액션은 상태(state)를 변경하기 위한 정보를 가지고 있는 일종의 객체로
dispatch 함수를 사용하면, 액션을 발생시켜서 상태를 변경할 수 있다.
dispatch 함수는 useDispatch 훅을 통해 가져올 수 있다.
useDispatch 훅을 사용하면, dispatch 함수를 컴포넌트 내부에서 바로 사용할 수 있다.
ex) onClick 이벤트 핸들러에서 dispatch 함수를 호출하여 액션을 발생시킬 수 있음
Redux에서는 이러한 dispatch 함수를 통해 액션을 발생시키고, 액션을 처리하는 리듀서(reducer)를 통해 상태를 변경한다. 상태의 변경은 useSelector 훅을 통해 상태를 읽어오는 컴포넌트들에게 자동으로 반영된다.
리듀서(reducer)란? Redux에서 상태(state)를 변화시키는 함수
dispatch 함수를 호출하면 리덕스 스토어에 정의된 리듀서 함수가 호출되어 상태를 변경
useSelector 훅을 사용하여 상태를 조회한 후, dispatch 함수를 사용하여 해당 상태를 변경하는 것이 일반적인 리덕스의 상태 관리 방식
첫 번째 코드와 두 번째 코드의 가장 큰 차이는 전역 상태 관리 방식이다.
첫 번째 코드에서는 useState를 사용하여 각 컴포넌트의 상태를 관리한다. 이 방식은 리덕스보다 간단한 방식으로 상태를 관리할 수 있지만, 컴포넌트 계층 구조가 깊어질수록 상태를 전달하기 위해 props drilling이 필요할 수 있다. 따라서 상태를 전역적으로 관리하고 싶지 않거나, 간단한 상태 관리가 필요한 경우 유용하다.
두번째 코드에서는 useSelector와 useDispatch를 사용하여 리덕스를 활용하여 전역 상태를 관리한다
. 이 방식은 리덕스를 사용하기 위한 추가적인 라이브러리와 코드 작성이 필요하다.
리덕스를 사용하는 경우, 전체 애플리케이션의 상태를 관리하고 싶은 경우에는 유용하다.
첫 번째 코드에서는 Child6에서 setGreeting을 사용하여 해당 컴포넌트의 상태를 변경하고 있지만, 두 번째 코드에서는 Child6에서 dispatch를 사용하여 전역 상태를 변경하고 있다.
Redux
React에서는 상태와 속성(props)을 이용한 컴포넌트 단위 개발 아키텍처를 배웠다면, Redux에서는 컴포넌트와 상태를 분리하는 패턴
React 비효율
1. 해당 상태를 직접 사용하지 않는 최상위 컴포넌트, 컴포넌트1, 컴포넌트2도 상태 데이터를 가짐
2. 상태 끌어올리기, Props 내려주기를 여러 번 거쳐야 함
3. 애플리케이션이 복잡해질수록 데이터 흐름도 복잡해짐
4. 컴포넌트 구조가 바뀐다면, 지금의 데이터 흐름을 완전히 바꿔야 할 수도 있음
Redux는, 전역 상태를 관리할 수 있는 저장소인 Store를 제공함으로써 이 문제들을 해결
- 상태가 변경되어야 하는 이벤트가 발생하면, 변경될 상태에 대한 정보가 담긴 Action 객체가 생성
- 이 Action 객체는 Dispatch 함수의 인자로 전달
- Dispatch 함수는 Action 객체를 Reducer 함수로 전달
- Reducer 함수는 Action 객체의 값을 확인하고, 그 값에 따라 전역 상태 저장소 Store의 상태를 변경
- 상태가 변경되면, React는 화면을 다시 렌더링
Redux에서는 Action → Dispatch → Reducer → Store 순서로 데이터가 단방향으로 흐른다.
Store
Store는 상태가 관리되는 오직 하나뿐인 저장소의 역할
import { createStore } from 'redux';
const store = createStore(rootReducer);
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// 1
import { Provider } from 'react-redux';
// 2
import { legacy_createStore as createStore } from 'redux';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
const reducer = () => {};
// 4
const store = createStore(reducer);
root.render(
// 3
<Provider store={store}>
<App />
</Provider>
);
필요한 모듈과 컴포넌트를 불러오고, Redux store를 생성하여 Provider로 감싸주었다.
이제 전역 상태 저장소인 store를 사용할 수 있는 환경이 구성
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미
상태는 읽기 전용으로 Action 객체가 있어야만 상태를 변경할 수 있음
변경은 순수함수로만 가능하다
Reducer
Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { legacy_createStore as createStore } from 'redux';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
const count = 1
// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {
// Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
switch (action.type) {
//action === 'INCREASE'일 경우
case 'INCREASE':
return state + 1
// action === 'DECREASE'일 경우
case 'DECREASE':
return state - 1
// action === 'SET_NUMBER'일 경우
case 'SET_NUMBER':
return action.payload
// 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
default:
return state;
}
}
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.
// 1
//const reducer = () => {};
const reducer = counterReducer;
// 2
const store = createStore(reducer);
root.render(
// 3
<Provider store={store}>
<App />
</Provider>
);
위 코드는 Redux를 사용하여 전역 상태 저장소를 설정하고, Counter 예제를 구현한 것이다.
Counter 예제에서는 counterReducer 함수를 작성하여 사용하였다.
이 예제에서는 count 변수를 초기 상태로 사용하였으며, counterReducer 함수에서는 각각 INCREASE, DECREASE, SET_NUMBER 액션에 대해 적절한 상태 변화를 구현하도록 작성하였다.
이후 counterReducer 함수를 reducer 변수에 할당하고,
createStore 함수에 reducer를 인자로 넣어 Store를 생성한다.
마지막으로 Provider 컴포넌트로 store를 감싸주고, App 컴포넌트를 렌더링한다.
여러 개의 Reducer를 사용하는 경우, Redux의 combineReducers 메서드를 사용해서 하나의 Reducer로 합친다.
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
counterReducer,
anyReducer,
...
});
Action
action 객체를 생성하는 함수를 만든다.(액션 생성자)
// payload가 필요 없는 경우
const increase = () => {
return {
type: 'INCREASE'
}
}
// payload가 필요한 경우
const setNumber = (num) => {
return {
type: 'SET_NUMBER',
payload: num
}
}
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { legacy_createStore as createStore } from 'redux';
// 1
//Action Creator 함수
const increase = () => {
return {
type: 'INCREASE'
}
}
//Action 객체를 반환하는 함수
//{type: 'INCREASE'}를 반환한다.
//이를 통해 Store의 상태를 변경할수 있다.
// 2
const decrease = () => {
return {
type: 'DECREASE'
}
}
const count = 1;
// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {
// Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
switch (action.type) {
//action === 'INCREASE'일 경우
case 'INCREASE':
return state + 1;
//INCREASE 타입일 경우 이전 상태에서 1을 더한 값을 반환
// action === 'DECREASE'일 경우
case 'DECREASE':
return state - 1;
//DECREASE 타입일 경우 이전 상태에서 1을 뺀 값을 반환
// action === 'SET_NUMBER'일 경우
case 'SET_NUMBER':
return action.payload;
//SET_NUMBER 타입일 경우 액션 객체에 담긴 값을 반환
// 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
default:
return state;
//이 외의 타입일 경우 이전 상태를 그대로 반환
}
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.
};
//이전 상태를 state로, 액션 객체를 action으로 받아와서 이전 상태를 변경한 새로운 상태를 반환
//Reducer 함수는 createStore 함수와 함께 사용되어 Store를 만들 때 필요
// 3
export { increase, decrease };
//Store는 애플리케이션의 전역 상태를 저장하고, Reducer 함수에 의해 관리
const store = createStore(counterReducer);
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<Provider store={store}>
<App />
</Provider>
);
Dispatch
Dispatch는 Reducer로 Action을 전달해주는 함수
// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );
// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(5) );
Action 객체를 전달받은 Dispatch 함수는 Reducer를 호출
이제 연결 => Redux Hooks를 이용하면된다.
Redux Hooks
useSelector(), useDispatch()
useDispatch() 는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환
import React from 'react';
import './style.css';
// 1
import { useDispatch } from 'react-redux';
// 2
import { increase, decrease } from './index.js';
export default function App() {
// 3
const dispatch = useDispatch();
console.log(dispatch);
const plusNum = () => {
// 4
dispatch(increase());
};
const minusNum = () => {
// 5
dispatch(decrease());
};
return (
<div className="container">
<h1>{`Count: ${1}`}</h1>
<div>
<button className="plusBtn" onClick={plusNum}>
+
</button>
<button className="minusBtn" onClick={minusNum}>
-
</button>
</div>
</div>
);
useDispatch 훅을 사용하여 dispatch 함수를 가져오고, 각각의 버튼 클릭 이벤트 핸들러에서 액션 생성 함수 increase()와 decrease()를 실행시켜 dispatch 함수로 전달해준다.
액션 객체를 리듀서 함수로 전달하여 상태를 변경할 수 있다.
useSelector()
useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드
import React from 'react';
import './style.css';
// 1
import { useDispatch, useSelector } from 'react-redux';
import { increase, decrease } from './index.js';
export default function App() {
const dispatch = useDispatch();
// 2
const state = useSelector((state) => state);
//useSelector를 사용하여 전역 저장소(store)에 저장된 state를 가져올 수 있다.
//useSelector의 콜백 함수의 인자에 Store에 저장된 모든 state가 담긴다.
//그대로 return을 하게 되면 Store에 저장된 모든 state를 사용할 수 있다.
// 3
console.log(state);
const plusNum = () => {
dispatch(increase());
};
const minusNum = () => {
dispatch(decrease());
};
//useDispatch를 사용하여 액션을 dispatch 할 수 있다.
//여기서 increase()와 decrease()는 action creator 함수로, 액션 객체를 반환
return (
<div className="container">
{/* 4 */}
<h1>{`Count: ${state}`}</h1>
<div>
<button className="plusBtn" onClick={plusNum}>
+
</button>
<button className="minusBtn" onClick={minusNum}>
-
</button>
</div>
</div>
);
}
이 코드는 React에서 Redux를 사용하는 기본적인 방법을 보여준다.
따라서 index.js와 App.js를 관련시켜서 설명해보자면,
코드는 React-Redux를 사용한 상태 관리 코드이다.
먼저 index.js 파일에서는 createStore 함수를 사용하여 Store를 생성하고,
counterReducer 함수를 Reducer로 등록한다.
이 때, Reducer는 현재 Store의 상태와 Action을 인자로 받아 새로운 상태를 반환하는 함수이다.
그리고 increase, decrease와 같은 Action 생성 함수를 정의한다.
이 함수들은 액션을 생성하는 순수 함수로, type 속성과 필요에 따라 payload 속성을 가진 객체를 반환한다.
이 객체는 Reducer 함수에서 인자로 받아 상태를 갱신하는데 사용된다.
마지막으로, App.js 파일에서는 Provider 컴포넌트를 사용하여 하위 컴포넌트에서 Store에 접근할 수 있도록 한다. useSelector 훅을 사용하여 Store에 저장된 상태 값을 가져와서 h1 태그에 렌더링하고,
useDispatch 훅을 사용하여 Action을 디스패치하는 함수를 가져와서 버튼 클릭 이벤트에 연결한다.
이를 통해 버튼 클릭 시, Store의 상태 값을 변경할 수 있다
.
Redux의 세 가지 원칙
동일한 데이터는 항상 같은 곳에서 가지고 와야 한다는 의미
상태는 읽기 전용이라는 뜻으로 Redux의 상태도 직접 변경할 수 없음을 의미한다.
변경은 순수함수로만 가능하다
dispatch란?
dispatch는 Redux에서 Action 객체를 Reducer 함수로 전달하여 state를 업데이트하는 함수
Redux에서는 상태 변화를 일으키는 모든 작업을 액션(Action)이라는 객체로 표현한다.
액션은 type 필드를 가지며, 이를 통해 어떤 변화를 일으키는지 구분한다.
액션을 발생시키기 위해서는 dispatch 함수를 사용한다.
dispatch 함수는 Reducer 함수에 액션 객체를 전달한다.
Reducer 함수는 현재 상태와 전달받은 액션 객체를 바탕으로 새로운 상태를 계산한다.
이후 계산된 새로운 상태를 Store에 저장하게 되고, 이를 통해 컴포넌트에서 상태를 불러와 사용할 수 있게 된다.
즉, dispatch 함수는 액션을 발생시키고, Reducer 함수를 실행시켜 Store에 저장된 state를 업데이트하는 역할을 한다.
provide 컴포넌트란?
<Provider> 컴포넌트는 리액트 애플리케이션에서 Redux store를 사용할 수 있도록 해주는 컴포넌트이다.
Provider는 Redux 앱에서 반드시 최상위 루트 컴포넌트 안에 위치해야 한다.
Provider는 하위에 있는 모든 컴포넌트에게 Redux store에 접근할 수 있는 props를 제공한다.
Provider의 역할은 단순하다.
Redux store를 props로 받아 하위 컴포넌트들에게 전달한다.
그래서 하위 컴포넌트에서는 useSelector와 같은 Redux 훅을 사용하여 store의 데이터에 접근할 수 있다.
Redux의 구조 리팩토링
Actions
index.js
export const INCREASE = 'INCREASE';
export const DECREASE = 'DECREASE';
export const increase = () => {
return {
type: INCREASE,
};
};
export const decrease = () => {
return {
type: DECREASE,
};
};
Reducers
index.js
import { initialState } from './initialState.js';
import { INCREASE, DECREASE } from '../Actions';
const count = initialState;
// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
export const counterReducer = (state = count, action) => {
// Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
switch (action.type) {
//action === 'INCREASE'일 경우
case INCREASE:
return state + 1;
//INCREASE 타입일 경우 이전 상태에서 1을 더한 값을 반환
// action === 'DECREASE'일 경우
case DECREASE:
return state - 1;
//DECREASE 타입일 경우 이전 상태에서 1을 뺀 값을 반환
// action === 'SET_NUMBER'일 경우
case 'SET_NUMBER':
return action.payload;
//SET_NUMBER 타입일 경우 액션 객체에 담긴 값을 반환
// 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
default:
return state;
//이 외의 타입일 경우 이전 상태를 그대로 반환
}
// Reducer가 리턴하는 값이 새로운 상태가 됩니다.
};
initialState.js
export const initialState = 1;
Store
index.js
import { legacy_createStore as createStore } from 'redux';
import { counterReducer } from '../Reducers';
export const store = createStore(counterReducer);
App.js
import React from 'react';
import './style.css';
import { useSelector, useDispatch } from 'react-redux';
import { increase, decrease } from './Actions';
export default function App() {
const dispatch = useDispatch();
const state = useSelector((state) => state);
console.log(state);
const plusNum = () => {
dispatch(increase());
};
const minusNum = () => {
dispatch(decrease());
};
return (
<div className="container">
<h1>{`Count: ${state}`}</h1>
<div>
<button className="plusBtn" onClick={plusNum}>
+
</button>
<button className="minusBtn" onClick={minusNum}>
-
</button>
</div>
</div>
);
}
index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './Store';
const rootElement = document.getElementById('root');
console.log(rootElement);
const root = createRoot(rootElement);
console.log(store);
root.render(
<Provider store={store}>
<App />
</Provider>
);