[React] 리액트 상태관리 - Redux

ssjeu·2022년 6월 7일
0

React

목록 보기
2/6
post-thumbnail

1. Redux란?

1.1 Redux 개념

Redux는 리액트 생태계에서 가장 사용률이 높은 오픈소스 JavaScript 상태관리 라이브러리state를 이용해 웹 사이트 혹은 애플리케이션의 상태 관리를 해줄 목적으로 사용한다.

상태값을 컴포넌트에 종속시키지 않고 컴포넌트의 바깥에서 상태 관리 함으로써 효율적으로 글로벌 상태 관리 가능

state : 리덕스에서는 저장하고 있는 상태값(data)으로 딕셔너리 형태({[key]: value})형태로 보관

1.2 Redux는 언제 쓸까?

  • 앱의 여러 위치에서 필요한 많은 양의 상태들이 존재할 때 (전역 상태가 필요하다고 느껴질 때)
  • 상태들이 자주 업데이트 될 때
  • 상태를 업데이트 하는 로직이 복잡할 때
  • 앱이 중간 또는 큰 사이즈의 코드를 갖고 있고 많은 사람들에 의해 코드가 관리될 때
  • 상태가 업데이트되는 시점을 관찰할 필요가 있을 때

1.3 Redux와 ContextAPI 차이점

(이전글) [React] 리액트 상태관리 - ContextAPI

1. 미들웨어 (Middleware)

  • 리덕스에는 미들웨어(Middleware)라는 개념 존재
  • 리덕스로 상태 관리를 할 때에는 useReducer를 사용할 때 접했던 개념인 리듀서 함수를 사용
  • 리덕스의 미들웨어를 사용하면 액션 객체가 리듀서에서 처리되기 전에 원하는 작업 수행 가능
  • 미들웨어는 주로 비동기 작업을 처리 할 때 많이 사용

2. 유용한 함수와, Hooks

  • Context API 와 useReducer 에서는 Context 생성, Context 의 Provider 설정, 전용 커스텀 Hook 생성하여 사용, 리덕스에서는 이와 비슷한 작업을 편리하게 해줄 수 있는 여러 기능들이 존재
  • connect 함수: 리덕스의 상태 또는 액션 생성 함수를 컴포넌트의 props 로 받아올 수 있음
    useSelector, useDispatch, useStore 과 같은 Hooks: 손쉽게 상태를 조회하거나 액션을 디스패치 가능
  • connect 함수와 useSelector 함수에는 내부적으로 최적화가 잘 이루어져있어서 실제 상태가 바뀔때만 컴포넌트가 리렌더링, 반면에 Context API를 사용할 때에는 그러한 최적화가 자동으로 이루어져있지 않기 때문에 Context 가 지니고 있는 상태가 바뀌면 해당 Context 의 Provider 내부 컴포넌트들이 모두 리렌더링

3. 하나의 커다란 상태

  • Context API 를 사용해서 글로벌 상태를 관리 할 때에는 일반적으로 기능별로 Context를 만들어서 사용하는 것이 일반적
  • 반면 리덕스에서는 모든 글로벌 상태를 하나의 커다란 상태 객체에 넣어서 사용하는 것이 필수, 때문에 매번 Context를 새로 만드는 수고로움을 덜 수 있음

2. Redux 기본 용어

Store(스토어) - Action(액션) - Reducer(리듀서)

1. Store (스토어)

  • 상태가 관리(데이터가 저장)되는 오직 하나의 공간
  • 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담음
  • 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근

2. Action (액션)

  • 앱에서 스토어에 운반할 데이터 (= 주문서)
  • 상태 변화가 필요하다면 액션 발생 필요, 액션생성함수로 액션 생성
  • 자바스크립트 객체로 type 필드 필수 요소로 가짐
// type은 이름 같은 것, 임의의 문자열을 정해 작성
{type: 'CHANGE_STATE', data: {...}}

// 액션생성함수
const changeState = (new_data) => {
	return { // 액션을 리턴
		type: 'CHANGE_STATE',
		data: new_data
	}
}

3. Reducer (리듀서)

  • reducer: 변화를 일으키는 것 (사전 뜻)
  • 저장된 상태(=데이터)를 변경하는 함수
  • Action을 Store에 바로 전달하지 않고 리듀서에 전달해 리듀서가 주문을 보고 Store의 상태를 업데이트
  • 액션 생성 함수를 부르고 → 액션을 만들면 → 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서 → 새로운 데이터를 만들고 → 리턴
// 임의로 정한 기본 상태값
const initialState = {
	name: 'aaa'
}

function reducer(state = initialState, action) {
	switch(action.type){

		// action의 타입마다 케이스문을 걸어 액션에 따라 새로운 값 return
		case CHANGE_STATE: 
			return {name: 'bbb'};

		default: 
			return false;
	}	
}

4. Subscribe (구독), Dispatch (디스패치)

  • 구독 : 스토어의 내장 함수 중 하나로 리스너 함수를 파라미터로 넣어 호출하면 상태가 업데이트될 때마다 호출 (일종의 이벤트 리스너)
  • 디스패치 : 스토어의 내장 함수 중 하나로 액션 객체를 넘겨줘서 상태를 업데이트 하는 유일한 방법 (이벤트 트리거)

3. Redux 3가지 원칙

1. Single source of truth

  • 하나의 애플리케이션 안에는 하나의 스토어만 사용하자는 원칙
  • 동일한 데이터는 항상 같은 곳에서 가지고 옴 (스토어라는 하나뿐인 데이터 공간)

2. State is read-only

  • 리액트에서는 setState 메소드를 활용해야만 상태 변경 가능
  • 리덕스에서도 액션이라는 객체를 통해서만 상태를 변경 가능

3. Changes are made with pure functions

  • 변화를 일으키는 리듀서 함수는 순수한 함수여야만 함
  • 순수 함수 조건
- 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
- 파라미터 외의 값에는 의존하면 안된다.
- 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어서 반환한다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.

4. Redux 사용하기

4.1 Duks 구조

  • 보통 리덕스 사용 시 모양새대로 action, actionCreator, reducer 분리하여 작성
  • 하지만 덕스 구조는 모양새로 묶는 대신 기능으로 묶어 작성
    -> 같은 기능을 가진 action, actionCreator, reducer 한 파일에 작성

4.2 Redux 사용 예시

  1. 패키지 설치 : yarn add redux react-redux
    기본 파일 생성 : src > redux > modules

  2. 데이터 저장할 공간 store 생성 (redux 파일 아래)

// store.js
 import {createStore, combineReducers} from "redux";
 import cat from "./modules/cat";

 const rootReducer = combineReducers({cat});
 const store = createStore(rootReducer);

 export default store;
  • reducer만이 store에 있는 data 수정 가능
  • 리듀서 여러 개 생성 가능, 생성한 리듀서 모두 묶어 rootReducer 생성
  • 미들웨어(여러 개 가능) 모두 묶어 enhancer 생성 후 rootReducer랑 enhancer 엮어 store 생성
  1. action 생성 : "어떤 식으로 수정을 해줘!"

  2. reducer 생성 및 store에 연결

  • reducer를 통해 새로운 상태값을 만들고 새 상태값을 Store에 저장
// cat.js (src > redux > modules)
// 1. action 타입 잡아주기
const CHANGE_NAME = "cat/CHANGE_NAME";

// 2. 초기화
const initial_state = {name: "펄이 고양이", age: 5};

// 3. 액션생성함수 생성
export const changeName = (name) => {

    return {type: CHANGE_NAME, name};
}

// 4. reducer 작성
export default function reducer(state = initial_state, action={}){
    // state = initial_state: state에 설령 아무 값이 오지 않는다하더래도 initial_state 을 기본값으로 할당

    // 리듀서는 실제적으로 store에 있는 data값 변경, switch문으로 각각의 액션 타입에 알맞는 작업할 수 있도록 작성
    switch(action.type){
        case "cat/CHANGE_NAME": {
            // 액션생성함수에서 리턴하는 값과 동일한 형태로 리턴
            // 그래서 이름을 바꾸고 싶다면 action.name으로 작성해야함
            return {...state, name: action.name};
           // 스프레드 문법: 불변성 유지때문에 직접 객체 수정하지 않으려고 변경 사항 반영한 새 객체 만들어 넘김
        }
        default:
            return state;
    }   
}

// 5. store에 넣기 (store.js) -> 6. 컴포넌트와 provider사용해 연결 (index.js)
// -> 컴포넌트에서 쓸 준비 완료!
// 7. 컴포넌트에서 제대로 데이터 가져오기 redux hook사용해서 (App.js)
  1. 컴포넌트에서 사용할 준비 : provider 사용해 store 연결
  • component는 새로운 상태값을 props를 통해 받아와 다시 렌더링
// index.js
// provider 로 store 연결, 어떤 store를 가져올래도 import
import { Provider } from 'react-redux';
import store from "./redux/store";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
      <Provider store={store}>
      <App />
      </Provider>
  </React.StrictMode>
);
  1. 컴포넌트에서 데이터 사용 (구독, 수정) : redux hook 사용
// App.js
// 7. redux hook: useSelector: 구독, useDispatch: 수정 요청
import { useSelector, useDispatch } from "react-redux";

// 9. 어떤 액션을 해!
import {changeName} from "./redux/modules/cat";

function App() {
    //8. data 가져오기
    const cat = useSelector(state => state.cat);
    console.log(cat);

    //9. button누르면 수정할거야
    const dispatch = useDispatch();
    return (
        <div className="App">
            <p>name::::{cat.name}</p>
            <button onClick={() => {
                dispatch(changeName("루비")); // 어떤 액션을 해!
            }}>이름 바꾸기</button>
        </div>
    );
}

export default App;


Redux tool kit

  • 위와 같이 redux 사용 시 작성해야 되는 구조가 많음
  • 보일러 플레이트 -> redux tool kit으로 좀 더 간편하게 사용 가능

참고 문헌

0개의 댓글