리덕스에 대해 알아보자

세바님·2023년 5월 26일
0
post-thumbnail

글을 시작하며

나는 그동안 리액트를 쓰면서, 리덕스에 대해 '아직은 필요 없어 보인다' 라는 이유로 공부를 미루곤 무시하곤 했다.
그러나 마침 교내 동아리에서 프론트 스터디를 하게 되었고, 이번 주제가 바로 리덕스 였다.
그래서 리덕스를 공부할 겸 이 글을 쓰게 되었다.

리덕스란?

리덕스 공식 문서를 보면, 리덕스를 "자바스크립트 앱을 위한 예측 가능한 상태 컨테이너" 라고 설명한다.
말 그대로이다. 리덕스에서는 상태(state)를 생성하고, 여러 함수들을 이용해 상태를 변경, 참조 한다.
또한, 상태를 비롯한 유사한 성질로 인해 리액트와 함께 쓰이는 경우가 많다.

흠... 아직까지는 그리 어렵지 않아 보인다. 리덕스에 대해 더 파고들어 보기 전에, 리덕스를 왜 쓰는지 알아보자.

리덕스를 왜 쓸까?

리덕스의 필요성을 알아보기 위해 리액트로 예시를 들어보겠다.
다음과 같은 구조를 가인 페이지가 있다고 해 보자.

이렇게 간단한 구조를 가졌거나, 렌더링이 많이 일어나지 않는 경우라면 리덕스가 딱히 필요하지 않다.
그러나, 구조가 훨씬 복잡해진다면 어떨까?

만약 이렇게 보다 복잡한 구조의 페이지에서 부모 컴포넌트에서 최하위 자식 컴포넌트로 상태를 전달하려면, 부모 컴포넌트부터 최하위 자식 컴포넌트까지 props 를 이용해 상태를 전달해야 하며, 상태가 바뀔 때마다 다른 자식 컴포넌트까지 필요하지 않은 렌더링이 되는 상황이 연출된다.

리덕스는 이런 문제를 해결하기에 적합하다.
왜냐하면 상태를 스토어를 통해 컴포넌트 구조 내부가 아닌 외부에서 하기 때문이다!

얼마나 편한가! 이제부터 사실상 props 따위는 쓸 필요가 없어진 것이다!

리덕스의 주요 개념

이제 리덕스를 왜 쓰는지 알게 되었다! 이번에는 리덕스의 주요 개념, 그리고 어떻게 사용하는지에 대해 알아보자.

액션 (Action)

액션은 상태에 변화가 필요할 때 발생한다. 액션은 객체 형태이며, 이때 type 이라는 요소는 필수적이다. 이때 type 은 일반적으로 상수를 이용한다.
또한 액션 내에 요소를 추가 할 때, 원하는 대로 설정할 수 있다.

// type 선언은 상수로
const ADD = "ADD";
const DELETE = "DELETE";
const DEFAULT = "DEFAULT";

// 액션은 객체형태로 선언
{
  // 필수
  type: ADD, 
    // 선택
    data: {  
      title: "와! 리덕스!",
      main: "겁.나.어.렵.습.니.다."
    }
}

또한 리덕스에서 관리 할 상태의 기본값을 따로 정의할 수 있다.

const initState = {
  input: "",
  remain: 0
};

액션 생성함수 (Action Creator)

액션 생성 함수는 말 그대로 액션을 생성하는 함수이다. 매개변수를 받고, 이를 액션 형태로 만들어 준다.

const addSome = (data) => {
  return {
    type: ADD,
    data
  }
}
const deleteOne = () => {
  return {
    type: DELETE
  }
}

// arrow function 무슨 형태든 사용 가능
const thisAlsoWorks = () => ({ type: DEFAULT });
const thisAlsoWorksToo = data => ({ type: DEFAULT, data });

리듀서 (Reducer)

리듀서는 상태와 액션 두가지를 매개변수로 받는다. 그리고 이를 토대로 새로운 상태를 만들어 값을 반환한다.

const reducer = (state = initState, action) => {
	switch(action.type) {
      case ADD:
        return {
          // 이전값 그대로 넘겨주기
          ...state,
          remain: state.remain + action.data 
        }
      case DELETE:
        return {
          ...state,
          remain: state.remain - 1
        }
      // type이 case에 없다면 실행 
      default:
        return state;
    }
}

스토어 (Store)

스토어는 리덕스에서 가장 중요한 개념이라고 할 수 있다. 스토어 안에는 상태(state)와 리듀서, 그리고 그 외에 여러 내장함수가 있다.

스토어는 createStore 함수로 만들 수 있고, createStore 함수는 리듀서 함수를 매개변수로 가진다.

import { createStore } from 'redux';

const store = createStore(reducer);

또한, getState() 함수로 스토어 안에 있는 상태의 값을 조회할 수도 있다.

const state = store.getState();

console.log(state.remain); // 0

디스패치 (Dispatch)

디스패치는 스토어의 내장함수중 하나로, 액션을 발생시킨다.
매개변수로 액션 생성함수를 받으며, 함수를 호출하면 스토어는 리듀서 함수를 실행시킨다.

console.log(state.remain); // 0

store.dispatch(addSome(5));

console.log(state.remain); // 5 

구독 (Subscribe)

구독 또한 스토어의 내장함수중 하나이다. 매개변수로 함수를 받으며, 액션이 디스패치될 때 마다 매개변수로 전달해준 함수가 호출된다.

const listenState = () => {
  console.log(store.getState()); // 스토어에서 객체를 콘솔에 출력
}

store.subscribe(listenState);

store.dispatch(addSome(5));
// 구독 함수 실행 -> 
// 액션 객체가 콘솔창에 나옴!

이렇게 리덕스의 기본적인 개념에 대해 알아 보았다! 이번에는 이 개념들을 올바르게 사용할 수 있도록 하는 리덕스의 규칙을 알아보자.

리덕스의 3원칙

리덕스에는 다음과 같은 중요한 세 가지 원칙이 있다.

단일 스토어

스토어는 무조건 한 애플리케이션 안에 하나만 존재해야 한다.
여러 개의 스토어를 사용을 할 수는 있으나, 상태 관리가 상당히 복잡해진다고 한다. 웬만하면 피하도록 하자.

상태는 읽기 전용

상태는 불변(읽기 전용) 데이터이며, 상태를 변화시키려면 액션을 전달해야 한다.
이는 상태를 변화시키는 의도를 정확하게 표현할 수 있고, 상태 변경에 대한 추적이 용이해지게 된다.

리듀서는 순수한 함수 형태로

먼저, 순수한 함수란 무엇일까?
순수한 함수란 동일한 입력을 받았을 때 언제나 동일한 출력을 내는 함수를 말한다.
또한 리듀서는 이전 상태와 액션을 받아 다음 상태를 반환하는 함수이지, 상태 자체를 변경하지 않는다. 대신 새로운 상태 객체를 생성해 반환하는 것이다.

리덕스와 리액트로 투두리스트 만들기

이번에는 직접 해보면서 그동안 배운 것들을 적용해보자!
그 전에, 리덕스를 리액트 환경에서 보다 쓰기 쉽게 만들어주는 react-redux를 사용하였다.

먼저 스토어를 만들어 보자.

store.js

액션 타입

먼저 액션의 type을 만들어 준다.

const ADD = "ADD";
const DELETE = "DELETE";

액션 생성 함수

그 다음, 액션 생성 함수를 작성한다.

export const addTodo = (text) => {
	return {
		type: ADD,
		text,
	};
};

export const deleteTodo = (id) => {
	return {
		type: DELETE,
		id: parseInt(id),
	};
};

리듀서

앞에서 배운대로라면 초기값을 설정해야한다. 그러나 이번에는 따로 생성하지 않고 빈 배열로 지정을 해 주었다.

const reducer = (state = [], action) => {
	switch (action.type) {
		case ADD:
			return [{ text: action.text, id: Date.now() }, ...state];
		case DELETE:
			return state.filter((toDo) => toDo.id !== action.id);
		default:
			return state;
	}
};

스토어 생성

createStore 를 이용해 스토어를 만들어 준다!

import { createStore } from "redux";

{ ... }

const store = createStore(reducer);

export default store;

main.jsx

provider

import { ... } ;

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

위 코드에서 Provider 는 react-redux에서 제공된 것이다.
이 컴포넌트에 store를 주어야 앱이 돌아가게 된다.

App.jsx

connect

import { connect } from "react-redux";
import { addTodo } from "./store";

{ ... }

const mapStateToProps = (state) => {
	return { todoLists: state };
};

const mapDispatchToProps = (dispatch) => {
	return {
		addTodo: (text) => dispatch(addTodo(text)),
	};
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

앞에서 스토어를 만들었으니 이젠 스토어에서 상태를 꺼내 쓰거나 변경할 차례이다.
connect 함수는 컴포넌트와 스토어를 연결해주는 역할을 한다고 보면 된다.
mapStateToProps , mapDispatchToProps 함수를 매개변수로 받으며, 두 함수는 각각 스토어 안의 상태를 넘겨주는 역할을, 디스패치를 리듀서에게 보내는 역할을 한다.
그 뒤에 있는 괄호 안에는 현재 사용중인 컴포넌트의 이름을 적어주면 된다.

Todo.jsx

import { connect } from "react-redux";
import { deleteTodo } from "../store";

{ ... }

const mapDispatchToProps = (dispatch, ownProps) => {
	return {
		handleClick: () => dispatch(deleteTodo(ownProps.id)),
	};
};

export default connect(null, mapDispatchToProps)(ToDo);

이번엔 투두리스트에서 각각의 리스트를 책임질 Todo 컴포넌트에 connect 를 추가해 줄 시간이다!
App.jsx 와 다른 점은 mapStateToProps 함수가 없다는 점인데, 이때는 connect 함수에 null을 주면 된다.

드디어 어찌저찌 투두리스트를 만들어 내게 되었다! 와!

글을 마치며

드디어!! 리덕스에 대한 전반적인 공부를 마치게 되었다.
생각했던 것보다 공부도 많이 해야 했고, 주요 개념을 봤을 땐 '생각보다 쉬운 것 같은데?' 했다가 막상 직접 해 보니 너무 헷갈리고 어려웠다.
그래도 이전보다 한 발 앞으로 나아간 것 같아 기쁘다!!

profile
아아

0개의 댓글