Redux

seul_velog·2022년 8월 25일
0

React

목록 보기
11/11

Redux

리덕스는 전역데이터를 어떻게 효과적으로 관리할 것인지에 대한 라이브러리로, 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 더욱 효율적으로 관리할 수 있으며 글로벌 상태 관리도 손쉽게 할 수 있다.

  • 리덕스는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리이다.
  • 리덕스는 리액트에서 사용하기 위해 만들어진 라이브러리 이지만 react 뿐만이 아니라 JavaScript 환경 혹은 Angular 등과 같은 프레임워크에서도 사용된다.



리덕스와 Context API

🧐 Context 와 다른점은 뭘까?

1. 미들웨어(Middlewatr)

리덕스로 상태 관리를 할 때 리듀서 함수를 사용한다. 리덕스의 미들웨어를 사용하면 액션 객체가 리듀서에서 처리되기 전에 우리가 원하는 작업들을 수행할 수 있다.

  • 특정 조건에 따라 액션이 무시되게 만들 수 있다.
  • 액션에 콘솔을 출력하거나 서버쪽에 로깅을 할 수 있다.
  • 액션이 디스패치 됐을 때 이를 수정해서 리듀서에게 전달되도록 할 수 있다.
  • 특정 액션이 발생했을 때 이에 기반하여 다른 액션이 발생되도록 할 수 있다.
  • 특정 액션이 발생했을 때 특정 자바스크립트 함수를 실행시킬 수 있다.

2. 유용한 함수와 Hooks

connect 함수를 사용해서 리덕스의 상태 또는 액션 생성 함수를 컴포넌트의 props로 받아올 수 있으며 useSelector, useDispatch, useStore 와 같은 Hooks 를 사용하면 손쉽게 상태를 조회하거나 액션을 디스패치 할 수 있다.

3. 단일 상태

리덕스에서는 모든 글로벌 상태를 하나의 커다란 상태 객체에 넣어서 사용하는 것이 필수이다. (매번 Context를 새로 만드는 수고로움을 덜 수 있다.)

📌 리덕스를 언제 쓸까?

  1. 프로젝트 규모가 클 때 사용한다.
  2. 비동기 작업을 자주 할 때 사용한다.
  3. 리덕스가 편리할 경우에 사용한다.

✍️ 단순히 글로벌 상태 관리를 위한 것이라면 Context API를 활용하는 것만으로도 충분할 수 있음에 유의하자.




리덕스의 규칙

1) 단일 스토어

하나의 애플리케이션에서는 단 한개의 스토어를 만들어서 사용한다.

  • 여러개의 스토어를 사용하는 것은 가능하지만 권장되지 않는다.

단일 스토어 사용 준비하기

1) import redux
2) 액션 정의
3) 액션을 사용하는 리듀서를 만들기
4) 리듀서들을 합치기
5) 합쳐진 최종 리듀서를 인자로, 단일 스토어를 만들기

스토어 리액트 컴포넌트에서 사용하기

1) import react-redux
2) connect 함수를 이용해서 컴포넌트에 연결하기



2) 읽기전용 상태

상태는 읽기전용이다. 리액트에서 상태를 업데이트 할 때와 마찬가지로 기존의 상태는 건들이지 않고 새로운 상태를 생성하여 업데이트 해준다.

  • 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 검사(shallow equality) 를 하기 때문이다.
    이를 통해 객체의 변화를 감지할 때 깊은 비교가 아닌 얕은 비교를 함으로써 좋은 성능을 유지할 수 있다.



3) 순수한 함수 리듀서

동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 한다.

  • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
  • 이전의 상태는 건들이지 않고 변화를 일으킨 새로운 상태 객체를 만들어서 반환한다.
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야 한다.

+) 순수하지 않은 작업들은 리듀서 함수의 바깥에서 처리해줘야 하는데 이런 것들을 위해 리덕스 미들웨어를 사용한다.







리덕스 키워드

Action

  • 액션은 객체(Object) 이다.
  • type은 필수 프로퍼티이며 보통 문자열이다. 그 외의 값들은 임의로 넣어줄 수 있다.
  • store의 상태를 변경하는 용도로 사용된다.
  • 액션객체를 리터럴로 사용하는 것이 아니라 액션을 만들어내는 함수를 만들어서 사용한다. → 실수방지

1) Type 프로퍼티만 가지고있는 액션
{type: 'TEST'} → payload가 없는 액션

2) 다른 프로퍼티가 있는 액션
{type: 'TEST', params:'hi'} → payload가 있는 액션

{
  type: "TEST", // type 필드는 필수적이다.
  data: {
    id: 0,
    text: "리덕스 연습!"
  }
}

액션 생성자 (Action Creator : 액션 생성 함수)

function 액션생성자(...args) {return 액션;}

  • 액션을 만드는 함수이다. 단순히 파라미터를 받아와서 액션 객체 형태로 만든다.
  • 추후 컴포넌트에서 쉽게 액션을 발생시키기 위해 만든다. 따라서 보통 함수 앞에 export 키워드를 붙여서 불러와 사용한다.
  • 액션 생성함수를 사용하는 것은 필수는 아니다. 액션을 발생 시킬 때마다 직접 액션 객체를 작성할 수도 있다.

리덕스에서 액션이 하는 일

  • 액션 생성자를 통해 액션을 만든다.
  • 만들어낸 액션 객체를 리덕스 스토어에 보낸다.
  • 리덕스 스토어가 액션 객체를 받으면 스토어의 상태 값이 변경된다.
  • 변경된 상태 값에 의해 상태를 이용하고 있는 컴포넌트가 변경된다.

액션 준비하기

  • 액션의 타입을 정의하여 변수로 빼는 단계
    - 액션의 타입이 문자열이므로 실수유발 가능성이 크므로 변수를 활용하는 것이 좋다. (필수는X)
  • 액션 객체를 만들어 함수를 만드는 단계
    - 하나의 액션 객체를 만들기 위해 하나의 함수를 만든다. 액션의 타입은 미리 정의한 타입 변수로부터 가져와서 사용한다.
// 타입
const ADD_TODO = 'ADD_TODO';

// ADD_TODO 타입을 이용하는 액션생성함수
export function addTodo(todo) {
  return {
    type: ADD_TODO,
    todo,
  };
}

// 화살표 함수로도 만들 수 있다.
export const addTodo = (todo) => ({
  type: ADD_TODO,
  todo
})



Reducer

  • 액션을 주면 그 액션이 적용된 결과를 만들어 준다. 즉, 변화를 일으키는 함수이다.
  • 현재의 상태와 전달 받은 액션을 참고하여 새로운 상태를 만들어서 반환한다.
  • 함수이다.
    - Pure Function
    : 같은 인풋을 받으면 같은 결과를 내는 함수
    - Immutable
    : 오리지널 state와 바뀐 state가 별도의 객체로 만들어져야 한다. (리덕스가 인지하는 방식과 관련)
    function 리듀서(previousState, action){
      // 상태 업데이트 로직
        return newState
    }
    • action을 받아서 state를 리턴하는 구조
    • 인자로 들어오는 previousState와 리턴되는 newState는 다른 참조를 가지도록 해야한다.
  • 여러개의 리듀서를 만들고 이를 합쳐서 루트 리듀서(Root Reducer)를 만들 수 있다.

ex.1 )

import { ADD_TODO } from './actions';

const initialState = [];

function todoApp(previousState = initialState, action) {
  if (action.type === ADD_TODO) {
    return [...previousState, action.todo]; // 1) 
  }
  return previousState;
}

1) Immutable 하게 데이터를 업데이트 하기
❗️만약 previousState.push(''); 와 같이 작성한다면 객체는 변경하지만 객체 레퍼런스는 변경되지 않으므로 리덕스에서 이부분을 바뀌었다고 판단하지 못한다. (Immutable하지 않은 코드)


ex.2 )

// 카운터를 위한 리듀서 예시
function counter(state, action) {
  switch (action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state - 1;
    default: // 1)
      return state;
  }
}

1) 리덕스의 리듀서에는 default 값으로 기존 State를 반환하도록 작성한다.



Store

  • 리덕스에서는 한 애플리케이션당 하나의 스토어를 만든다. 스토어 안에는 현재의 앱 상태, 리듀서, 내장 함수들이 있다.

Store 만드는 함수

const store = createStore(reducer)

  • const store = createStore(리듀서)
  • createStore의 인자
    1) reducer : reducer 함수,
    2) preloadedState : initialState를 넣을 수 있다. 넣지 않으면 각각 리듀서에서 최초 값으로 undefined가 들어오므로 그쪽에서 initialState를 설정해 줄 수도 있다.
    3) enhancer

✍️ store 출력해보기

console.log(store);


1. store.getState()

  • 현재 store의 state를 가져오는 함수
console.log(store.getState()); // 현재 state 상태 출력


2.store.dispatch()

  • 디스패치는 액션을 발생 시키는 내장함수로, 액션은 파라미터로 전달한다.
  • store.dispatch(액션), store.dispatch(액션생성자())
    : action을 인자로 넣어서 store의 함수를 변경시키는 함수
    → 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태를 만들어 준다.
store.dispatch(addTodo('coding'));
console.log(store.getState()); // 현재 state 상태 출력


3. subscribe

  • subscribe 함수는 함수 형태의 값을 파라미터로 받아온다. 특정 함수를 전달해주면 액션이 디스패치 되었을 때 마다 전달해준 함수가 호출된다.
  • store에 변경이 생겼을 때 함수를 실행시킨다.
  • return은 unsubscribe함수이다. 추후 unsubscribe() 하면 제거된다.
store.subscribe(() => {
  console.log(store.getState()); // 2,3,4)
});

console.log(store.getState()); // 1)
store.dispatch(addTodo('coding'));
store.dispatch(addTodo('sleep'));
store.dispatch(addTodo('eat'));
console.log(store.getState()); // 5)

// 1) []
// 2) ['coding']
// 3) ['coding', 'sleep']
// 4) ['coding', 'sleep', 'eat']
// 5) ['coding', 'sleep', 'eat']

✍️ 보통 리액트에서 리덕스를 사용할 때에는 subscribe 내장함수 대신 react-redux 라이브러리에서 제공하는 connect 함수 또는 useSelector Hook을 사용해서 리덕스 스토어의 상태에 구독을 한다.


4. store.replaceReducer(다른리듀서)

  • 원래 가지고 있던 리듀서를 다른 리듀서로 바꾸는 기능(실무에서 잘 쓰이지는 않음)



combineReducers

  • reducers 안의 여러 복잡한 로직이 앱의 사이즈가 커질수록 더 복잡해 질 수 있다.
  • 한 프로젝트에 여러개의 리듀서가 있을 때에는 이를 한 리듀서로 합쳐서 사용한다. 합쳐진 리듀서는 루트 리듀서라고 부른다.
  • 리듀서를 합치는 작업은 리덕스에 내장되어 있는 CombineReducers 함수를 사용한다.

▼ combineReducers 활용 전 코드

import { ADD_TODO, COMPLETE_TODO, SHOW_ALL, SHOW_COMPLETE } from './actions';

const initialState = { todos: [], filter: 'ALL' };

export function todoApp(previousState = initialState, action) {
  if (action.type === ADD_TODO) { // 1)
    return {
      ...previousState,
      todos: [...previousState.todos, { text: action.text, done: false }],
    };
  }

  if (action.type === COMPLETE_TODO) { // 2)
    return {
      ...previousState,
      todos: previousState.todos.map((todo, index) => {
        if (index === action.index) {
          return { ...todo, done: true };
        }
        return todo;
      }),
    };
  }

  if (action.type === SHOW_COMPLETE) { // 3)
    return {
      ...previousState,
      filter: 'COMPLETE',
    };
  }

  if (action.type === SHOW_ALL) { // 4)
    return {
      ...previousState,
      filter: 'ALL',
    };
  }

  return previousState;
}
  • 1)과 2)의 경우 filter에는 영향을 주지 않고 todos에만 관여한다.
  • 3)과 4)의 경우 todos에는 영향을 주지 않고 filter에만 영향을 준다.
  • 1)과 2)를 묶고, 3)과 4)를 묶어서 combineReducer를 활용하자.

📌 reducers 폴더에 아래와 같은 폴더 구조를 작성
-reducer.js
-filter.js
-todos.js

▼ combineReducers 활용 & 폴더 분리

// reducer.js
import { combineReducers } from 'redux';
import todos from './todos';
import filter from './filter';

const reducer = combineReducers({ // 이 부분을 reducer 대신 rootReducer라고 쓰는 듯 하다.🧐
  todos,
  filter,
});

export default reducer;
// filter.js
import { SHOW_ALL, SHOW_COMPLETE } from '../actions';
const initialState = 'ALL';

export default function filter(previousState = initialState, action) {
  if (action.type === SHOW_COMPLETE) {
    return 'COMPLETE';
  }

  if (action.type === SHOW_ALL) {
    return 'ALL';
  }

  return previousState;
}
// todos.js
import { ADD_TODO, COMPLETE_TODO } from '../actions';
const initialState = [];

export default function todos(previousState = initialState, action) {
  if (action.type === ADD_TODO) {
    return [...previousState, { text: action.text, done: false }];
  }

  if (action.type === COMPLETE_TODO) {
    return previousState.map((todo, index) => {
      if (index === action.index) {
        return { ...todo, done: true };
      }
      return todo;
    });
  }

  return previousState;
}




reference)
redux-공식문서
pc-redux
vlpt-redux

profile
기억보단 기록을 ✨

0개의 댓글