본문 바로가기
Development/Front-end

리액트 - 리덕스(Redux) 쉽게 이해하기 (with Typescript)

by 알 수 없는 사용자 2020. 3. 19.

안녕하세요. 휴몬랩 소프트웨어 개발자 진(JIN)입니다. 리액트를 이용해 개발하다 보면 컴포넌트(조각들)를 만들어 쪼개어 만들다 보면 많은 계층구조가 생기게 되고 state(상태)를 관리하는 게 참 쉽지가 않았습니다. 리덕스 관련 예제는 도큐먼트와 많은 훌륭한 분들이 잘 정리하셨겠지만 좀 풀어서 쉽게 리덕스에 대해 좀 쉽게 접근해보자! 해서 정리하게 되었습니다:)

Redux?

우리는 리액트의 state 관리하는 것과 UI와의 일관성 유지하는 게 중요하다는 것을 알고? 있습니다. (중요합니다ㅎ)
하지만 웹 애플리케이션은 훨씬 복잡해지고 많은 계층구조가 생기면(부모-자식-손자-증손자...) state관리는 정말 힘들어집니다.  

 각 계층은 자신이 가지는 기능을 해야하고 그 기능 수행을 위해 어떤 관계에서는 state를 가져와 사용하는 의존관계가 심화될 수 있습니다. 밑의 그림을 보면 서로서로 아주 격렬하게 의존관계가 생성됨을 알 수 있습니다...

 

출처 :  http://www.liberaldictionary.com/redux/

 

왼쪽 그림을 보면  빨간 화살표로 이동하는 값이 state(상태)인데 그림으로만 봐도 상당히 관계가 얽히고설켜있습니다. 이 상태들을 코드로 보면 매우 난잡하고 추가적인 컴포넌트를 중간에 넣는다면 매우 골치 아플 것 같습니다. 확장성이라는 개념이 거의 없다고 볼 수 있습니다.. 이러한 애플리케이션의 'state관리'라는 문제를 좀 해소시켜보자 해서 'Redux'라는 게 등장했습니다. 

 

 오른쪽 그림을 보면 리덕스는 애플리케이션의 state를 다루고 저장하는 마법을 애플리케이션에 부여하는 일만 신경 씁니다.

그리고 그것을 '스토어(store)'라는 단일 저장소에 저장합니다!

 

"애플리케이션 - 스토어"  간 직접적인 처리가 아닌 

"애플리케이션 - 액션 - 리듀서 - 스토어 - 애플리케이션" 이렇게 우회적으로 처리합니다

 

용어 설명

액션(Action) : 스토어에 새로운 state정보를 추가하거나 기존 state를 변경하는 일은 무엇이 변경된 지 기술하는 것입니다.

리듀서(Reducer) : 액션의 결과로 최종 state를 결정합니다.

 

우회적으로 처리를 하는 이유?

=> 확장성 때문입니다! 리덕스는 애플리케이션의 상태를 쉽게 저장하고 예측 가능하게 관리해줍니다

 

리덕스의 철학 3가지

1. 앱의 모든 state는 한 장소에 저장. 즉, state를 갱신하려고 여기저기 찾아다닐 필요가 없습니다

 

2. state는 오직 readable. 즉, 읽기 전용이고 액션을 통해서만 변경한다

    데이터 변경하는 유일한 방법이 액션이라는 뜻입니다 - 데이터의 불변성을 지켜줘야 하기 때문에 

 

3. 반드시 마지막 state가 저장되어야 합니다. state는 결코 직접 수정, 변형하지 않는다는 말입니다.

     따라서 리듀서를 이용해서만 마지막 state값을 지정해야합니다

 


액션 타입(action type) 

 state는 변하는 값이고, 이런 변화 때로 액티브한 웹을 만들 수 있게 되는 것이죠! 그 state의 변화를 액션으로 정의하고 사용하겠다는 것입니다. 우선 그 액션의 타입을 필수적으로 작성해줘야 합니다. 보통 아래와 같이 대문자와 스네이크 표기법으로 작성합니다. 저는  editor라는 공통된 작업에서 쓸 폴더를 만들어 액션 객체들을 여러 개 추가했으므로 editor/액션 타입 이렇게 지정했습니다!

const CHANGE_INPUT_EXAM = "editor/CHANGE_INPUT_EXAM";
const CHANGE_OUTPUT_EXAM = "editor/CHANGE_OUTPUT_EXAM";

 

 

액션 생성자 (return action)

export const setInputExam = (input: string) => ({
  type: CHANGE_INPUT_EXAM,
  payload: input
});

export const setOutputExam = (output: string) => ({
  type: CHANGE_OUTPUT_EXAM,
  payload: output
});

/* 액션의 리턴 타입 */
type ChangeExamples =
  | ReturnType<typeof setInputExam>
  | ReturnType<typeof setOutputExam>;
  
  
/* 상태 타입 &  초깃값 설정*/
interface ExamState {
  input: string;
  output: string;
}

const initialState: ExamState = {
  input: "입력 예시 작성.",
  output: "출력 예시 작성."
};

 

2개의 타입을 작성했는데 <input> 태그의 사용자 입력 값이 변할 때마다  state 값이 변하도록 하는 액션 생성자를 만들어보겠습니다.

이렇게 액션을 리턴하는 메서드를 리덕스 세계에서는 '액션 생성자'라고 지칭합니다.(액션을 생성하기 때문에) 

 

또한 Typescript를 이용하기 때문에  interface 나 type을 이용해서 상태의 타입을 작성해주고 그 타입으로 초기화를 해주시면 되겠습니다!


리듀서(Reducer)

 

이제 리듀서를 작성해봅시다

 액션(Action)이 하고자 하는 일을 정의한다면 리듀서(Reducer)는 그 일이 무슨 일인지와 새로운 state를 정의하는 방법을 구체적으로 다룹니다. 즉, 리듀서는 스토어(store)와 바깥세상의 중개자 정도로 생각하면 좋습니다

 

중개자의 일

1. store의 원래 state에 접근할 수 있게 해 준다

2. 현재 발생된 action을 조사할 수 있게 해 준다

3. store에 새로운 state를 저장할 수 있게 해 준다

 

리듀서를 작성해봅시다!  example이라는 이름의  reducer를 만들었습니다.

/* 리듀서 */
function example(state: ExamState = initialState, action: ChangeExamples) {
  switch (action.type) {
    
    case CHANGE_INPUT_EXAM:
      return { ...state, input: action.payload };
    
    case CHANGE_OUTPUT_EXAM:
      return { ...state, output: action.payload };
    
    default:
      return state;
  }
}

리듀서를 작성해 봤는데 리듀서 안에서 절! 대! 하지 말 사항들이 있습니다

  • 받은 인자의 변형
  • API 호출이나 라우팅 변경 등 같은 추가적인 기능 구현
  • Date.now(), Math.random() 같은 비순수 함수 호출

리듀서는 오직 주어진 인자로 다음 state를 산출해서 리턴합니다

추가 구현 x / api호출 x / 변형 x  - 오직 산출만 하는 순수한 함수여야 합니다

 

위의 코드에서도... state로  기존의 값을 복사 후 input값으로 대체한 후 새로운 객체를 리턴합니다. 새로 리턴하기 때문에 직접 수정하지 않는다는 원칙을 지켜준 것입니다. 이렇게. concat,. filter 같은 메서드들은 어떠한 처리를 해주고 새로운 배열을 리턴하므로  이러한 내장 함수들을 잘 숙지하고 익혀두시는 것도 좋겠습니다

 

import { combineReducers } from "redux";
import example from "./editor/example";

// import example2 form "./editor/example2";

const rootReducer = combineReducers({
  example,
  // example2
});

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

  

 리덕스는  RootRedux를 이용해서 하나로 묶어서 사용하면 관리하기에도 훨씬 수월합니다. 위에서 작성한 example을 불러와  redux 모듈의 combineReducer()의 객체 안에 하나씩 추가해주면 됩니다. example2라는 리덕스 모듈을 하나 더 만들었다면 다음과 같이 추가해주면 되겠습니다.

 


스토어(store)

 

action과 reducer를 작성했다면 이제 남은 건 action과 reducer를 스토어에 엮는 일입니다

그러려면 일단 스토어를 만듭시다!

 

import React from "react";
import ReactDOM from "react-dom";
import Routes from "./Routes";

import { createStore } from "redux";
import { Provider } from "react-redux";

/* store 생성 */
const store = createStore(rootReducer);

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

createStore  -  스토어를 생성하고 인자는 리듀서를 받습니다

이렇게 리덕스를 통해 앱의 state를 저장하는 과정의 한 사이클을 돌았습니다

즉, store가 있고 / reducer가 있으며 / reducer가 해야 할 일을 알려주는 action을 갖추게 되었습니다

 

모든 사항이 잘 작동하는지 확인을 위해 상태를 변화시켜봅시다. 현재  Hooks를 이용해 함수형으로 작성하는 것이 더 읽고 쓰기 좋은 코드라 생각하므로,  react-redux의 useSelector() ,  useDispatch()를 사용해보겠습니다!

 

useDispatch를 이용해 액션을  리듀서로 전달할 수 있습니다. 리듀서는  액션을 받아 동작하고 상태 값을 정의해줍니다

import React, {ChangeEvent} from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "modules";
  
  
  const dispatch = useDispatch();

  const onChangeInputExam = (e: ChangeEvent<HTMLTextAreaElement>) =>
    dispatch(setInputExam(e.target.value));

  const onChangeOutputExam = (e: ChangeEvent<HTMLTextAreaElement>) =>
    dispatch(setOutputExam(e.target.value));
    
    
 function InputBox(){
 	<>
    	<div>
        	<input type="text" onChange={onChangeInputExam} />
        </div>
     	<div>
        	<input type="text" onChange={onChangeOutputExam} />
        </div>
    </>
 }

 

 

useSelector를 이용해 state 값 가져와 컴포넌트에 반영할 수 있습니다.

import { useSelector } from "react-redux";
import { RootState } from "modules";
  
const { input, output } = useSelector((state: RootState) => ({
    input: state.example.input,
    output: state.example.output
}));

function View(){
	return(
    	<>
           <div>
        	{input}
           </div>
           
		   <div>
        	{output}
           </div>
        </>
    )
}

 

[정리]

 리덕스는 처음 접할 때는 액션은 뭐고 왜 이렇게까지 일을 복잡하게 해야 되는 거지?라는 생각을 했었습니다. 간단한 투두 리스트를 만들었을 땐 그런 생각을 잠시 했었습니다. 하지만 atomic design같이 컴포넌트들을 쪼개고 재사용하고 템플릿으로 만들어가면서 이곳저곳에서 state를 참조해야 했습니다. 트리 구조로 된 컴포넌트들에서 state를 이동시키려니 상당히 머리가 아프고 아 이래서 상태 관리가 정말 중요하다는 것을 깨달을 수 있었습니다. 리덕스 말고도 flux 아키텍처나 간단한 프로젝트는  Context API로도 상태를 관리할 수 있으니 프로젝트의 방향성 등을 잘 선택해서 효율적으로 상태를 관리할 수 있었으면 좋겠습니다!

 

[참고]

리액트 웹앱 제작 총론 2/e 

댓글