리덕스 기초

Posted by yunki kim on July 17, 2021

  리덕스는 리액트에서 가장 많이 사용하는 상태 관리 라이브러리이며 전역 상태를 관리할 때 효율적이다. 리덕스를 사용하면 다음과 같은 장점이 있다

    1. 상태 업데이트 관련 로직을 다른 파일로 분리시켜 더 효율적인 관리가 가능하다

    2. 컴포넌트끼리 상태를 공유해야 할때 여러 컴포넌트를 거치지 않아도 된다.

    3. 미들웨어를 지원한다.

용어, 개념 정리

  액션

    상태에 어떤 변화가 필요하면 액션이 발생한다. 액션은 하나의 객체로 표현되며 액션의 이름 정도 되는 type필드를 반드시 가지고 있어야 한다.

1
2
3
4
5
6
7
{
    type: 'ADD_TODO',
    data: {
        id: 1,
        text: 'reduxs',
    }
}
cs

  액션 생성 함수(action creator)

    말 그대로 액션 객체를 만들어 준다. 필요할때 마다 매번 객체를 만든다면 번거롭고 필요한 정보를 놓칠 수 있기 때문에 액션 생성 함수가 필요하다.

1
2
3
4
5
6
function addTodo(data){
    return {
        type: 'ADD_TODO',
        data,
    }
}
cs

  reducer

    변화를 일으키는 함수이다. 액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아 온다. 그 후 받아온 두 값을 사용해 새로운 상태를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const initialState = {
    counter: 1
}
 
function reducer(state = initialState, action){
    switch(action.type) {
        case INCREMENT: 
            return {
                counter: state.counter + 1
            };
        default:
            return state;
    }
}
cs

  store

    프로젝트에서 리덕스를 사용하기 위해서는 스토어가 있어야 한다. 스토어는 한개의 프로젝트 당 하나만 가질 수 있으며 스토어 안에는 현재 애플리케이션 상태와 리듀서, 필요한 내장함수가 포함 된다.

  dispatch

    스토어의 내장 함수이다. 이는 액션을 발생시킨다. 액션객체를 파라미더로 받고 이 함수가 호출되면 스토어는 리듀서 함수를 실행애 새로운 상태를 만든다.

  subscribe

    subscribe는 스토어 내장 함수 중 하나이며 함수 안에 리스너 함수를 파라미터로 넣어서 호출하면 이 리스너 함수가 액션이 dispatch되어 상태가 업데이트될 때마다 호출된다.     

1
2
3
4
5
const listener = () => {
    console.log('state updated');
}
const unsubscribe = store.subscribe(listener);
unsubscribe();
cs

 

리덕스는 리액트에 종속적이지 않다

  리덕스는 비록 리액트에서 사용하기 위해 만들어진 라이브러리이지만 다른 UI 라이브러리, 프레임워크에서 사용이 가능하다. 따라서 다음과 같이 바닐라에서도 리덕스를 사용할 수 있다. 바닐라에서 리덕스를 돌리기 위해서는 parcel-bundler를 글로벌로 설치 하고, redux를 설치해야 한다. 그 후 parcel index.html을 입력하면 서버가 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="./index.css">
    <title>Title</title>
</head>
<body>
    <div class="toggle"></div>
    <hr>
    <h1>0</h1>
    <button id="increase">+1</button>
    <button id="decrease">-1</button>
    <script src="./index.js"></script>
</body>
</html>
cs
1
2
3
4
5
6
7
8
9
10
11
.toggle {
    border: 2px solid black;
    width: 64px;
    height: 64px;
    border-radius: 32px;
    box-sizing: border-box;
}
 
.toggle.active {
    background: yellow;
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import {createStore} from "redux";
 
const divToggle = document.querySelector('.toggle');
const counter = document.querySelector('h1');
const btnIncrease = document.querySelector('#increase');
const btnDecrease = document.querySelector('#decrease');
 
//프로젝트의 상태에 변화를 일으키는 것이 액션이다.
//액션의 이름은 대문자로 작성하고 고유해야 한다.
const TOGGLE_SWITCH = 'TOGGLE_SWITCH';
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
 
//위의 액션 이름을 사용해 만든 액션 객체, 액션 객체는 반드시 type 값을 가져야 한다.
const toggleSwitch = () => ({type: TOGGLE_SWITCH});
const increase = (difference) => ({type: INCREASE, difference});
const decrease = () => ({type: DECREASE});
 
//초기값
const initialState = {
  toggle: false,
  counter: 0
};
 
//reducer 함수는 변화를 일으킨다.
//리듀서 함수가 맨처음에 호출되면 undefined이고 이떄문에 기본설정을 한다.
//리듀서 내부에서는 불변성을 유지해야 한다.
function reducer(state = initialState, action) {
  switch(action.type) {
    case TOGGLE_SWITCH:
      return {
        ...state,//불변성 유지
        toggle : !state.toggle,
      };
    case INCREASE:
      console.log(typeof(action.difference));
      return {
        ...state,
        counter: state.counter + action.difference,
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1,
      }
    default:
      return state;
  }
}
 
//store를 만든다
const store = createStore(reducer);
 
//상태가 업데이트 될때 마다 호출된다.
//리액트가 아니기 때문에 이미 html을 사용해 만들어진 UI의 속성을 상태에 따라 변경해 준다.
const render = () => {
  const state = store.getState();//현재 상태를 불러온다
  if(state.toggle) {
    divToggle.classList.add('active');
  }
  else {
    divToggle.classList.remove('active');
  }
  counter.innerText = state.counter;
}
 
render();
 
//스토어의 상태가 바뀔때 마다 render가 호출되도록 한다.
//subscribe의 인자로 함수 형태의 값을 전달하면
//이 함수는 추후 액션이 발생해 상태가 없데이트 될 떄 호출된다
//리액트에서는 subscribe하는 작업을 상태 조회 과정에서 react-redux 라이브러리가
//대신 해준다.
store.subscribe(render);
 
//액션을 발생시키는 것을 dispatch라 하며 인자로 액션 객체를 넣어 준다.
divToggle.onclick = () => {
  store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
  store.dispatch(increase(1));
}
btnDecrease.onclick = () => {
  store.dispatch(decrease());
}
cs

리덱스의 세가지 규칙

  리덕스를 사용할때는 다음과 같은 3개의 규칙을 준수해야 한다.

    1. 단일 스토어

      하나의 애플리케이션 안에는 하나의 스토어가 들어 간다. 하나의 애플리케이션에서 두개 이상의 스토어를 사용할 수 는 있지만 상태관리가 복잡해 질 수있다.

    2. 읽기 전용 상태

      리덕스는 읽기 전용이다. 따라서 상태를 업데이트 할때에는 불변성을 지켜야 한다. 리덕스는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사(shallow equality)를 하기 때문에 객체의 변활르 감지할 때 객체의 깊숙한 변화를 감지할 때 객체의 깊숙한 안쪽 까지 비교하는 것이 아닌 겉핥기 식으로 비교해 좋은 성능을 유지할 수 있다.

    3. 리듀서는 순수 함수이다

      변화를 일으키는 리듀서 함수는 순수 함수 여야 한다. 순사 함수는 다음과 같은 조건을 만족한다

        - 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.

        - 파라미터 외의 값에는 의존하면 안된다.

        - 이전 상태는 건드리지 않고 변화를 준 새로운 상태를 반환한다

        - 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.

      위의 조건 때문에 리듀서 함수 내에서는 현재 시간 가져오기, 랜덤 값 생성, 네트워크 요청 등을 할 수 없다. 이런 동작은 리듀서 함수 외부, 예를 들어 액션을 만드는 과정, 리덕스 미들웨어에서 처리하며 비동기 작업은 미들웨어를 통해 관리한다.

 

리덕스의 흐름

  액션 타입, 액션 생성 함수 작성 -> 리듀서 작성 -> 스토어 생성.