React에서 Redux를 사용해보기

Grace·2021년 8월 26일
0

react

목록 보기
5/6
post-thumbnail

개요

점점 어플리케이션에 요구되는 기능들이 늘어나게 되면서, 자바스크립트 코드가 많은 상태를 관리해야 하는 필요성이 늘어나고 있다. 그렇기에 Flux의 흐름을 따라 상태변화가 일어나는 시점에 제약을 두어 상태변화를 예측 가능하게 만들었다.

  • 3가지 원칙
    1. 어플리케이션의 모든 state는 하나의 store 안에 하나의 객체트리 구조로 저장된다.
    2. 상태를 변화시키는 유일한 방법은 액션 객체를 '전달'하는 방법뿐이다.
      (상태는 읽기 전용)
    3. 액션을 통해 변화하는 상태트리를 지정하기 위해 순수 리듀서를 작성해야 한다. (순수함수로 작성)
  • Flux와의 관계

    Redux를 Flux의 흐름을 따르고 있다고 말할 수 있지만, 분명한 차이점을 알아두어야 한다.
  • Redux에는 dispatcher가 아닌 순수함수에 의존한다. 따라서 더 간단하게 관리가 가능하다.
  • Redux는 reducer를 통해 사용자가 결코 데이터의 상태를 변경하지 않는다고 가정하고 있다.
    ⇒ 이를 위해서는 Babel에 구현되어 있는 object spread 문법이나 Immutable을 사용할 수 있다.

데이터 흐름

엄격한 단방향 데이터 흐름을 따른다 = Flux의 흐름을 따름

  • 생명주기

    1. 사용자가 store.dispatch(action)를 호출
      앱 내의 어디서든 호출 가능

    2. Redux store가 지정된 reducer 함수를 호출

      같은 입력으로 몇번의 호출에도 항상 같은 출력값이 나와야 함

    3. root reducer가 각 reducer의 출력값을 합쳐서 하나의 상태트리로 만듦

    4. Redux store가 root reducer에 의해 반환된 상태트리를 저장

기초 사용법

1. Action / Action생성자

어플리케이션에서 store로 보내는 데이터 묶음으로, store의 유일한 정보원이다.

  • Action의 조건
    • Action은 자바스크립트 객체로, 어떤 형태의 Action이 실행될지 나타내는 type 속성을 가져야 한다.
      = 어떤 일이 일어난다는 사실만을 기술
    • type은 일반적으로 문자열 상수(대문자)로 정의되며, 띄어쓰기에는 '_'를 사용한다.
    • type외에 액션 객체의 구조는 자유롭게 생성 가능하다.
    • 각 Action에는 가능한 작은 데이터를 전달하는 것이 좋다.
const ADD_TODO = 'ADD_TODO';

    {
    	type : "ADD_TODO",
    	text: "Hello Redux"
    }
  • Action 생성자의 조건

    • Action을 만드는 함수로, 단지 액션을 반환하는 역할을 한다.

    • 리덕스를 사용할 때 필수적으로 사용해야 하는 것은 아니지만, 나중에 컴포넌트에서 액션을 발생시킬 때 더 쉽게 사용할 수 있도록 해준다.

  // 기존 함수형 - 다른 component에서 사용해야 하기 때문에 export 해주기
  export function addTodo(text) {
    return {
  	type: ADD_TODO,
  	text
    }
  }

  // 화살표함수형 (더 간단히 사용 가능)
  export const addTodo = (text) => ({
    type: ADD_TODO,
    text
  })

2. Reducer

어플리케이션의 상태가 어떻게 바뀌는지 정의해주는, 이전상태와 액션을 받아서 다음상태를 반환해주는 순수함수

  • 절대로 하지 말아야 할 것 ⇒ 계산만 가능!!
    • 인수 변경(mutate)
    • side-effect 일으키기 ⇒ API 호출이나 라우팅 전환
    • 순수하지 않은 함수의 호출 ⇒ 예를들면, Date.now( )나 Math.random( )
  • 작성법

    1. 초기 상태 정의

      Redux는 처음에 reducer를 undefined의 상태로 호출하기 때문에, 초기상태를 정의해주어야 한다.

  const initialState = {
  	visibilityFilter: SHOW_ALL,
  	todos: [],
  };

  function todoApp(state, action) {
  	if(typeof state === "undefined") {
  		return initialState
  	}
  	return state;
  }

  // 위의 todoApp reducer를 더 간단하게 작성하면,
  function todoApp(state = initialState, action) {
  	return state;
  }
  1. 상태변경 로직 구성

    • 조건
      - state값을 직접 변경하지 않고, 복사본을 만들어서 사용해야 한다.
      - default case에 대해서는(알 수 없는 action에 대한) 이전의 state값을 반환한다.

      visibiliyFilter와 다른 액션들의 로직까지 구현해보자면,

  function todoApp(state = initialState, action) {
    switch(action.type) {
  	case SET_VISIVILITY_FILTER : 
  	  return Object.assign({}, state, {
  		visibilityFilter : action.filter 
  	  });
  	case ADD_TODO : 
  	  return Object.assign({}, state, {
  		todos : [
  		  ...state.todos, {
  			text: action.text,
  			completed: false
  		}]
  	  });
  	case COMPLETED_TODO :
  	  return Object.assign({}, state, {
  		todos : [
  		  ...state.todos.slice(0, action.index),
  		  Object.assign({}, state.todos[action.index], {
  		   completed: true,
  		  }),
  		...state.todos.slice(action.index + 1)
  		]
  	  });
  	default :
 	 return state;
    }
  }

  // Object.assign()대신 object spread 문법 사용하면
  function todoApp(state = initialState, action) {
    switch(action.type) {
  	case SET_VISIBILITY_FILTER : 
  	  return {
  	    ...state,
  	    visibilityFilter : action.filter
  	  }
  	...
    }
  }
  • 리듀서를 독립적인 수정이 일어나는 것을 기준으로 쪼개어 하나의 객체로 조합하는 함수로 만들 수 있다.
    // 초기상태는 직접 넣어주기
    function todos(state = [], action) {
    	switch(action.type) {
    		case ADD_TODO :
    			return [
    				...state, {
    		      text: action.text,
    		      completed: false
    		    }
    			];
    	  case COMPLETE_TODO:
    	    return [
    	      ...state.slice(0, action.index),
    	      Object.assign({}, state[action.index], {
    	        completed: true
    	      }),
    	      ...state.slice(action.index + 1)
    	    ];
    	  default:
    	    return state;
      }
    }

    function visibilityFilter(state = SHOW_ALL, action) {
      switch (action.type) {
    	  case SET_VISIBILITY_FILTER:
    	    return action.filter;
    	  default:
    	    return state;
      }
    }

    export default function todoApp(state = {}, action) {
    	return {
        visibilityFilter: visibilityFilter(state.visibilityFilter, action),
        todos: todos(state.todos, action)
      };
    }

여기서 combineReducers() 라는 유틸리티를 통해 독립된 reducer들을 합쳐줄 수 있다.
아래의 코드는 위의 todoApp()과 동일한 기능을 하는 코드이다.

    import {combineReducers} from 'redux';

    const todoApp = combineReducers({
    	todos,
    	visibilityFilter
    });

    export default todoApp;

3. Store

'무엇이' 일어날지 정의한 Action, 이 Action에 따라 상태를 '어떻게' 변경시킬지 정의한 Reducer를 가져오는 객체인 Store , 단 하나만 존재할 수 있다.

  • 하는 일

    • 어플리케이션의 state 저장
    • getState()를 통해 상태에 접근
    • dispatch(action)을 통한 상태의 변경
    • subscribe(listener)를 통한 리스너 등록
  • 생성법

    여러개의 Reducer를 합쳐주는 combineReducers()를 통해 합쳐진 것을 가져와서 createStore()에 넘기기

    import {createStore} from 'redux';
    import todoApp from './reducers';

    const store = createStore(todoApp);

Redux module 만들기

  • src/module 파일에 필요한 작업의 모듈 만들어주기
    -> 액션 타입 / 액션 생성 함수 / 초기상태 선언 / 리듀서 선언
  • 여러개의 리듀서를 사용해야 할 경우에는, src/module/index.ts 를 만들고 rootReducer 로 리듀서들을 합쳐서 사용하기
    -> combineReducers함수를 사용해서 합쳐주기
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

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

export default rootReducer;
  • 제일 부모 컴포넌트에서 createStore를 통해 리덕스 스토어 생성
    -> 특히 react에서는 Provider의 props로 store를 넣어서 App 컴포넌트를 감싸게 되면 렌더링하는 모든 컴포넌트가 리덕스 스토어에 접근할 수 있게 된다.

서버에서 데이터를 주고받는 state값이 생기게 되면 좀 더 복잡해지지만,
기본적인 내용은 이렇다.
그동안 redux를 사용해보면서 큰그림으로만 정리되어 있는 느낌이 강했는데,
이번에 이렇게 정리를 하면서 좀 더 구체적인 그림이 그려진 것 같다.
능숙하게 다룰 수 있을때까지 연습하기!

📌 참고하였습니다 :)
수정이 필요한 부분이 있다면 댓글로 알려주세요!

profile
쉽게 사는건 재미가 없더군요, 새로 시작합니다🤓

0개의 댓글