[Velopert리액트6]리덕스

Yongrok·2021년 12월 29일
0

React_Velopert

목록 보기
7/9

사용교재 : 벨로퍼트와 함께하는 모던 리액트
범위 : 6.1 ~ 6.5
노션 용량문제로 Migration

리덕스

리덕스

리액트에서 가장 사용률이 높은 상태관리 라이브러리

vs Context API

리덕스 ≒ Context API + useReducer
→ 리덕스도 리듀서, 액션 개념 有

  1. 미들웨어

리덕스는 미들웨어를 사용하여 액션객체가 리듀서에서 처리되기 전, 원하는 작업 수행

  • 조건에 따른 액션 무시
  • 액션을 콘솔에서 출력 or 서버에서 로깅
  • 액션이 디스패치 됐을 때, 이를 수정해서 리듀서에게 전달
  • 특정 액션이 발생했을 때, 다른 액션 발생 or 함수 실행

+) 주로 비동기 작업 처리에 사용

  1. 유용한 함수와 Hooks

Context의 Provider , 커스텀 Hook 과 비슷한 기능이 내재

connect 함수 : 리덕스 상태, 액션 생성 함수를 컴포넌트의 props 로 받을 수 있음
useSelector useDispatch useStore 등 과 함께 쓰면 상태조회, 액션 디스패치 가능
→ 실제 상태가 바뀔 때만 리렌더링
↔ Context API는 상태가 바뀌면 Provider 내부 모든 컴포넌트 리렌더링

  1. 하나의 커다란 상태

context는 기능별로 따로 만드는 것이 일반적
↔ 리덕스는 하나의 커다란 상태를 전역적으로 다룸

사용 추천

  • 프로젝트 규모 大
  • 비동기 多
  • 익숙함 O

1. 키워드

액션

상태에 변화가 필요할 때 발생시키는 객체

{
  type: "TOGGLE_VALUE", // 필수
	text: "나머지는 값은 개발자 마음대로"
}

액션 생성 함수

액션을 만드는 함수. 파라미터를 받아서 액션 객체 형태로 만듦.
→ 컴포넌트에서 쉽게 액션을 발생시키기 위함 (필수는 아님)

export function changeInput (text) {
  return {
    type: "ADD_TODO",
    text
  };
}

export const changeInput = text => ({
  type: "CHANGE_INPUT",
  text
});

리듀서

변화를 일으키는 함수. 상태와 액션을 파라미터로 받음

function counter(state, action) {
  switch (action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state - 1;
    default:
      return state;
  }
}

(!) useReducer와 차이 : defaultstate를 반환. useRedcer는 에러 처리

리덕스는 여러개의 서브 리듀서와 이를 합친 루트 리듀서의 형태로도 만들 수 있다.

스토어

현재 앱의 상태, 리듀서가 내제. 몇가지 내장 함수도 존재.

디스패치

⬆️스토어의 내장 함수. 액션을 발생시킴 dispatch(액션)

구독

⬆️스토어의 내장 함수. 함수 형태의 값을 파라미터로 받아옴.
→ 액션이 디스패치될 때마다 전달해준 함수 호출

2. 리덕스의 3가지 규칙

1. 1앱 1스토어

여러 개의 스토어가 가능은 하지만, 권장 X
→ 특정 업데이트가 너무 빈번함 or 특정 부분 완전 분리하기 위해 사용
→ 개발 도구 사용 불가

2. 상태는 읽기 전용

불변성 유지를 위해
→ shallow equality 검사를 하기 때문
→ 내부 데이터 변경을 감지
→ 얕은 비교로 퍼포먼스 up

3. 변화를 일으키는 함수, 리듀서는 반드시 순수 함수

(?) 순수함수

  • 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받음
  • 이전 상태는 노터치. 변화를 일으킨 새로운 상태 객체를 만들어서 반환 → 불변성
  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값 반환

3. 리덕스 사용 준비

$ yarn add redux

import { createStore } from 'redux';

// 리덕스에서 관리 할 상태 정의
const initialState = {
  counter: 0,
  text: '',
  list: []
};

// 액션 타입 정의. 주로 대문자로 작성합니다.
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';

// 액션 생성함수 정의. 주로 camelCase 로 작성합니다.
function increase() {
  return {
    type: INCREASE // 액션 객체에는 type 값이 필수입니다.
  };
}

const decrease = () => ({
  type: DECREASE
});

const changeText = text => ({
  type: CHANGE_TEXT,
  text // 액션안에는 type 외에 추가적인 필드를 마음대로 넣을 수 있습니다.
});

const addToList = item => ({
  type: ADD_TO_LIST,
  item
});

// 리듀서 만들기
// 액션 생성함수로 만든 객체들을 새로운 상태를 만드는 함수 (불변성 필수)
function reducer(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        ...state,
        counter: state.counter + 1
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1
      };
    case CHANGE_TEXT:
      return {
        ...state,
        text: action.text
      };
    case ADD_TO_LIST:
      return {
        ...state,
        list: state.list.concat(action.item)
      };
    default:
      return state;
  }
}

// 스토어 만들기
const store = createStore(reducer);

console.log(store.getState()); 
// getState() : 현재 store 상태를 조회하는 store 내장함수

// 스토어안에 들어있는 상태가 바뀔 때 마다 호출되는 listener 함수
const listener = () => {
  const state = store.getState();
  console.log(state);
};

const unsubscribe = store.subscribe(listener);
// 구독을 해제하고 싶을 때는 unsubscribe() 를 호출하면 됩니다.

4. 리덕스 모듈

(?) 리덕스 모듈

  • 액션 타입
  • 액션 생성 함수
  • 리듀서

가 포함된 JS파일
→ 각각 다른 파일에 분리가능(액션 / 리듀서)
↔ 하나로 뭉쳐서 쓰는 스타일 : Ducks 패턴

counter 모듈

/* 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
export const setDiff = diff => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

/* 초기 상태 선언 */
const initialState = {
  number: 0,
  diff: 1
};

/* 리듀서 선언 */
// 리듀서는 export default 로 내보내주세요.
export default function counter(state = initialState, action) {
  switch (action.type) {
    case SET_DIFF:
      return {
        ...state,
        diff: action.diff
      };
    case INCREASE:
      return {
        ...state,
        number: state.number + state.diff
      };
    case DECREASE:
      return {
        ...state,
        number: state.number - state.diff
      };
    default:
      return state;
  }
}

todos 모듈

/* 액션 타입 선언 */
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';

/* 액션 생성함수 선언 */
let nextId = 1; // todo 데이터에서 사용 할 고유 id
export const addTodo = text => ({
  type: ADD_TODO,
  todo: {
    id: nextId++, // 새 항목을 추가하고 nextId 값에 1을 더해줍니다.
    text
  }
});
export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  id
});

/* 초기 상태 선언 */
// 리듀서의 초기 상태는 꼭 객체타입일 필요 없습니다. 원시 타입도 상관 없습니다.
const initialState = [
  /* 우리는 다음과 같이 구성된 객체를 이 배열 안에 넣을 것입니다.
  {
    id: 1,
    text: '예시',
    done: false
  }
  */
];

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return state.concat(action.todo);
    case TOGGLE_TODO:
      return state.map(
        todo =>
          todo.id === action.id // id 가 일치하면
            ? { ...todo, done: !todo.done } // done 값을 반전시키고
            : todo // 아니라면 그대로 둠
      );
    default:
      return state;
  }
}

루트 리듀서

한 프로젝트의 여러개의 리듀서를 합친 것

combineReducer 내장 함수 사용

import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
  todos
});

export default rootReducer;

// index.js
const store = createStore(rootReducer); // 스토어를 만듭니다.

리액트 프로젝트에 리덕스 적용하기

react-redux 라이브러리 사용

index.js 에서 Provider 컴포넌트로 App 컴포넌트 감싸기

import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

5. 카운터 구현

프레젠테이셔널 컴포넌트

리덕스 스토어에 직접 접근X
→ 필요한 값, 함수를 props 로만 받아와서 사용하는 컴포넌트

function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
  const onChange = e => {
    onSetDiff(parseInt(e.target.value, 10));
  };
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <input type="number" value={diff} min="1" onChange={onChange} />
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
}

export default Counter;

컨테이너 컴포넌트

리덕스 스토어의 상태 조회, 액션 디스패치를 할 수 있는 컴포넌트

프리젠테이셔널 컴포넌트를 사용 ( HTML 태그 사용X )

import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';

function CounterContainer() {
  // useSelector : 리덕스 스토어의 상태를 조회하는 Hook
  // state 값 === store.getState()의 반환값
  const { number, diff } = useSelector(state => ({
    number: state.counter.number,
    diff: state.counter.diff
  }));

  // useDispatch: 리덕스 스토어의 dispatch를 함수에서 사용하는 Hook 입니다.
  const dispatch = useDispatch();

  // 각 액션들을 디스패치하는 함수들
  const onIncrease = () => dispatch(increase());
  const onDecrease = () => dispatch(decrease());
  const onSetDiff = diff => dispatch(setDiff(diff));

  return (
    <Counter
      // 상태
      number={number}
      diff={diff}
			// 액션 디스패치 함수
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onSetDiff={onSetDiff}
    />
  );
}

export default CounterContainer;

// App.js
import CounterContainer from './containers/CounterContainer';

function App() {
  return (
    <div>
      <CounterContainer />
    </div>
  );
}

export default App;

프리젠테이셔널 / 컨테이너 컴포넌트 구분 짓는 것이 전통적인 대세
→ 꼭 따를 필요는 없음

useSelector

useSelector 로 리덕스 스토어의 상태를 조회하면, 상태가 바뀌지 않았을 때는 리렌더링 X
→ 해당 값이 바뀌었을 때만 컴포넌트 리렌더링

// 리렌더링X
const { number, diff } = useSelector(state => ({
  number: state.counter.number,
  diff: state.counter.diff
})); 
// 리렌더링
const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff); 

shallowEqul

useSelector 의 두번째 인자(equalityFn)로 react-redux함수인 shallowEqual 전달

equalityFn?: (left: any, right: any) => boolean
→ 이전 값과 다음 값을 비교
→ true 리렌더링X, false 리렌더링

connect 함수

컨테이너 컴포넌트를 만드는 방법 중 하나. (HOC)
→ 잘 안 쓰임. 클래스 컴포넌트에서 쓰임 (훅을 못 써서)
useSelector , useDispatch 가 더 쉬움

HOC(Higher-Order Component)

컴포넌트를 특정 함수로 감싸서 특정 값 또는 함수를 props로 받아와서 사용가능하게 하는 패턴

리액트 컴포넌트 개발 패턴 중 하나
→ 로직 재활용에 유리
→ 특정 함수, 값을 props 로 받아서 사용할 때 쓰임
→ hook을 쓰기 전에 자주 사용
→ recompose 라이브러리 사용 추천

connect(mapStateToProps, mapDispatchToProps)

  • mapStateToProps(state) : 컴포넌트에 props로 넣어줄 리덕스 스토어 상태에 관련된 함수
    state : 현재 상태
  • mapDispatchToProps(dispatch) : 컴포넌트에 props로 넣어줄 액션을 디스패치하는 함수들에 관련된 함수
  • mapDispatchToProps(_, ownProps)ownProps
    컨테이너 컴포넌트를 렌더링 할때 직접 넣어주는 props를 가리킴. 생략 가능
  • connect(_, _, mergeProps, options)
    • mergeProps : 컴포넌트가 실제로 전달 받을 props 를 정의. 생략가능
    • options : 컨테이너 컴포넌트가 어떻게 동작할 지에 대한 옵션. 생략가능

0개의 댓글