본문 바로가기
React

[리액트] 리덕스 정리 (전역상태관리란?)

by 메이플 🍁 2022. 4. 20.

⚠️ 이 포스팅은 스파르타코딩클럽 리액트 기초반을 공부하고 정리한 블로그 포스팅입니다. ⚠️

 

포스팅에 해당하는 목차는 다음과 같습니다:

 

리액트 기초반

  • 3-7 리덕스를 통한 리액트 상태관리
  • 3-8 리덕스 살펴보기
  • 3-9 리덕스 써보기
  • 3-10 리덕스와 컴포넌트를 연결하자
  • 3-11 컴포넌트에서 리덕스 데이터 사용하기

 


 

리덕스란?

리덕스란 전역 상태 저장소다. 상태란 데이터를 의미하므로 리덕스는 전역적(global)으로 관리하는 데이터 저장소를 말한다. 리덕스를 사용하지 않는 리액트 프로젝트는 컴포넌트 내부에서 상태를 설정하고 업데이트해주는 지역적(local)인 방식으로 상태관리를 해주었다. 하지만 리덕스를 사용하면 상태를 저장해주는 데이터 저장소가 따로 생겨 컴포넌트 내부에서는 상태관리를 해주지 않아도 된다. 전역 상태관리를 해주는 리액트 도구로는 redux외에도 context API, recoil, react query가 있다.

 

리액트에서 전역 상태 관리가 필요한 경우

1. 자식 컴포넌트가 부모 컴포넌트의 state를 전달해줘야할때

리액트에서 데이터는 하나의 방향으로만 흘러야하는 단방향 데이터 흐름의 원칙을 지켜주어야 한다. 하나의 방향, 즉 데이터는 위에서 아래로, 부모에서 자식으로 넘겨줘야 한다. 자식 컴포넌트에 있는 state를 부모 컴포넌트가 사용할 수 없기 때문에 리덕스와 같은 전역 상태 저장소를 사용해 모든 state를 전역적으로 한곳에 저장해주고 필요한 경우 컴포넌트에서 참조해 컴포넌트의 트리 구조와 상관없이 자유롭게 state를 사용할 수 있다.

2. props drilling이 발생했을때

App.js ⊃ Bear.js ⊃ Cat.js ⊃ Dog.js ⊃ Elephant.js

props drilling이란 부모 컴포넌트에서 props를 전달하기 위해 props가 필요 없는 하위 컴포넌트에 값을 전달받는 상황을 의미한다. 예를 들어 App.js 부모 컴포넌트에 Bear.js, Cat.js, Dog.js, Elephant.js 라는 자식 컴포넌트들이 트리구조로 구성되어 있다고 가정해보자. Bear.js는 App.js의 자식 컴포넌트다. 이때 App.js가 Elephant.js에 props 값을 전달하기 위해서 Bear.js, Cat.js, Dog.js를 거쳐 가장 하위에 있는 Elephant.js에 props를 전달하게 된다. 이 과정에서 props가 필요 없는 하위 컴포넌트 Bear.js, Cat.js, Dog.js도 props를 가지게 된다. 이처럼 오로지 하위 컴포넌트에 전달하는 용도로 여러 자식 컴포넌트가 사용하지 않을 props를 지니게 되는 상황을 props drilling이라고 한다.

 

전역 상태 관리의 동작방식 (데이터 흐름도)

전역상태 관리의 동작방식

전역 상태 관리를 해주는 모든 라이브러리가 이러한 흐름을 가지고 데이터를 관리해준다:

1. 저장소에 전역적으로 관리해주는 데이터 name이 있다

2. 컴포넌트 Cat, Dog가 해당 데이터를 참조한다 (저장소에서 데이터를 참조하는 것을 "구독한다"라고도 표현한다)

3. Cat 컴포넌트가 데이터 name을 수정하기 위해 name 데이터를 바꿀 수 있는 어떤 함수를 호출해서 데이터를 수정해준다 (데이터를 직접 수정할 수가 없다)

4. name 데이터를 참조하고 있는 컴포넌트에게 수정된 name 데이터 값을 업데이트 해준다

 

리덕스의 동작방식

리덕스의 동작방식

1. 스토어에 전역적으로 관리해주는 상태 name이 있다

2. 데이터를 가져다 쓸 수 있도록 스토어와 컴포넌트를 연결시켜준다

2. 컴포넌트 Cat, Dog가 상태 name을 구독한다

3. 컴포넌트 Cat이 상태 name에 수정 요청을 한다

4. 데이터를 수정해주는 공간인 리듀서에서 액션을 디스패치한다 (데이터 수정)

5. 새 상태를 스토어에 저장한다

6. 스토어에서는 해당 데이터를 구독한 컴포넌트 Cat, Dog에게 구독한 스테이트가 변경되었다고 알려준다

7. 컴포넌트 Cat, Dog는 업데이트된 상태를 받아온다

8. 상태가 업데이트 되었으므로 컴포넌트가 리렌더링이 된다

9. 뷰단에서도 새로운 데이터가 나타난다

용어정리

  • 리덕스(redux): 스토어와 리듀서를 합친것
  • 스토어(store): 전역 데이터를 저장하는 곳
  • 리듀서(reducer): 스토어에 들어가 있는 데이터를 변경하는 곳
  • 액션을 디스패치하다: 데이터를 수정하는 작업
  • 데이터를 구독한다: 데이터를 참조하는 것

 

리덕스의 장점

1. 여러 컴포넌트가 동일한 상태를 보고있을때 유용하다

2. 저장소에서 데이터를 관리하게 되면 컴포넌트는 뷰에만 집중할 수 있다

3. state가 업데이트될때 해당 state를 구독한 컴포넌트만 리렌더링이 되기 때문에 프로젝트의 과부하를 막을 수 있다

4. 유지보수에 좋다

 

리덕스 개념과 용어

1. state

  • 리덕스에서 저장하고 있는 상태(=데이터)를 말한다
  • 딕셔너리 형태로 보관한다
{ key: value }

2. action

  • state를 업데이트할때 발생하는 객체
  • type에는 이름, data에는 state를 이렇게 바꾸겠다는 내용을 입력한다
  • 리덕스 스토어에서 action 객체를 받으면 state가 업데이트 된다
  • state가 수정을 요청해줄때 사용하는 함수 dispatch()의 파라미터로 넘겨줘 현재 state를 업데이트 해준다
{type: '이름', data: {state 업데이트 내용}}

3. action creator

  • 액션 생성 함수로 액션을 만들기 위해 사용하는 함수
  • 리턴된 액션은 dispatch() 함수의 파라미터로 전달된다
const changeState = (new_data) => {
  return {
    type: 'CHANGE_STATE',
    data: new_data
  }
}

4. reducer

  • dispatch() 함수가 수정을 요청하면 store에 저장된 state를 변경하는 함수
  • action creator에서 만든 액션 객체로 현재 state를 업데이트 해준다

리듀서 함수에서 기존 상태를 업데이트 하기 위해 필요한 두가지

1) 기존 상태

2) 액션 객체

// 초기상태
const initialState = {
  name: 'maple'
}

// 리듀서 함수에서 기존 상태를 업데이트 하기 위해 필요한 두가지
// 1) 기존 상태 2) 액션 객체
function reducer(state = initialState, action) {
  switch(action.type){
    // action의 타입마다 케이스문을 걸어줘 액션에 따라서 새로운 값을 리턴해준다
    case CHANGE_STATE: 
      return {name: 'honey'};

    default: 
      return false;
  }	
}

5. store

  • 전역 데이터를 저장하는 곳으로 딕셔너리 또는 json 형태
  • 여러 내장함수가 있다
    • getState: 컴포넌트가 상태를 참조할때 사용
    • dispatch(action): 상태를 업데이트할때 사용하는 함수

6. dispatch

  • store의 메소드로 구독하고 있는 상태 수정을 요청하는 함수
  • 발생시키고자 하는 액션을 파라미터로 넘겨 액션을 발생시키는 역할을 한다
dispatch(action);

 

리듀서 과정

1. 전역 저장소 store에 state가 있다

2. 컴포넌트에서 state를 구독한다

3. 컴포넌트에서 구독한 state를 바꿔주고 싶어 상태 수정을 요청한다

4. 상태 수정 요청 함수 dispatch가 실행한다

5. dispatch 함수는 action creator에서 만들어진 action 객체를 인자로 가진다

6. action에는 state를 이렇게 바꿔줘 라는 정보가 담겨져 있다

4. dispatch 함수가 action 객체를 매개변수로 가지고 실행되면 state를 업데이트해주는 reducer 함수가 실행된다

5. reducer 함수는 action 객체에 있는 정보(type, data)를 바탕으로 state를 업데이트 해준다

 

리덕스의 3가지 특징

1. store는 한개만 쓴다

  • 리덕스는 단일스토어 규칙을 따른다
  • 리덕스 라이브러리를 사용했을때 하나의 리액트 프로젝트에서 저장소는 하나만 있을 수 있다
  • 단 하나의 저장소내에서 리듀서는 여러개일 수 있다

2. store의 데이터(state)는 오직 action으로만 변경할 수 있다

  • state를 업데이트할때는 불변성을 지켜주어야 한다
  • 컴포넌트 내부에서 state가 있을때 state를 직접 수정하지 않고 해당 state를 수정할 수 있는 함수를 사용했던 것처럼 store의 state도 직접 수정하지 않고 action으로 state를 업데이트해야한다

3. 리듀서는 순수한 함수여야 한다

어떤 요청이 와도 리듀서는 같은 동작을 하는 순수한 함수여야 한다

순수한 함수란?

  • 같은 매개변수를 넣었을때 같은 값이 리턴되는 함수
  • 함수의 매개변수로 받아온 값 이외에는 아무것도 참조하지 않는 함수
  • 이전 상태는 수정하지 않고 업데이트된 새로운 객체를 리턴하는 함수
  • 리듀서는 이전 상태와 액션을 파라미터로 받아 업데이트된 새로운 객체를 리턴한다

 

리덕스에서 자주 사용하는 덕스구조

덕스(ducks) 구조란?

  • 보통 리덕스를 사용할 때는 모양새대로 action, action creator, reducer를 분리해서 작성한다 (액션은 액션끼리, 액션생성함수는 액션생성함수끼리, 리듀서는 리듀서끼리 작성)
  • 덕스 구조는 모양새로 묶는 대신 기능으로 묶어 작성한다 (버킷리스트 프로젝트가 있다면 버킷리스트의 action, action creator, reducer를 한 파일에 넣는 것)
  • 즉 한 파일에 관련기능을 모두 넣는것을 덕스구조라고 한다

덕스구조 예제

// widgets.js

// Action Types
const LOAD   = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';

// Action Creators
export function loadWidgets() {
  return { type: LOAD };
}

export function createWidget(widget) {
  return { type: CREATE, widget };
}

export function updateWidget(widget) {
  return { type: UPDATE, widget };
}

export function removeWidget(widget) {
  return { type: REMOVE, widget };
}

// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    // do reducer stuff
    default: return state;
  }
}

// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
  return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}

 

리덕스 적용하기

리덕스 공식문서 링크

영문 👉  https://redux.js.org/introduction/getting-started/

한국어 👉  https://ko.redux.js.org/introduction/getting-started/

 

1. 리덕스 설치하기

yarn add 커맨드로 두가지 라이브러리를 동시에 설치할 수도 있다

  • redux: 리덕스 패키지
  • react-redux: 리덕스를 리액트에서 편하게 쓸 수 있게 하는 패키지
yarn add redux react-redux

 

2. 모듈 만들기

src 폴더 안에 modules 라는 폴더를 만들고 모듈이름으로 컴포넌트 만들어주기

bucket.js 라는 모듈을 생성했다

 

2.1 덕스 구조를 사용해서 관련기능을 모두 같은 모듈에 생성시킨다

bucket.js 라는 모듈이 있다고 가정했을때 이 모듈과 관련있는 초기 상태, 액션 타입, 액션을 생성하는 action creator 함수, reducer 함수를 모두 같은 파일에 생성시킨다

// 모듈이름.js

// Initial State

// Action Type

// Action Creators

// Reducer

2.2 초기 상태 생성

리덕스에서 저장할 초기 상태(데이터)를 생성한다. 아래의 코드에서는 초기 상태값으로 list라는 배열을 생성했다.

// Initial State
const initialState = {
  list: ['영화관 가기', '매일 책읽기', '수영 배우기'],
};

2.3 액션 타입 생성

state를 업데이트할때 액션 타입에 따라 리턴값을 다르게 해준다. 즉 state를 구독한 컴포넌트에서 state 업데이트를 요청하면 reducer 함수에서 액션 타입에 따라 각기 다른 리턴값을 내보내준다. 아래의 코드에서는 CREATE라는 액션 타입을 생성했다.

// Action Type
const CREATE = 'bucket/CREATE';

2.4 action creator 함수 생성

state를 구독한 컴포넌트에서 state 업데이트를 요청할때 dispatch 함수를 호출한다. 이때 dispatch의 인자로 action creator 함수에서 리턴한 action 객체를 가진다. 아래 코드에서는 액션타입이 CREATE인 객체를 리턴해주는 createBucket이라는 함수를 선언했다.

// Action Creators
export function createBucket(bucket) {
  return { type: CREATE, bucket: bucket };
}

2.5 reducer 함수 생성

reducer는 state를 업데이트해주는 함수다. reducer 함수는 매개변수로 state와 dispatch에서 전달받은 action 객체를 가진다. 이 두가지 값으로 action 타입에 따라서 state를 업데이트 시켜준다. 아래 코드에서는 action type이 bucket/CREATE일때 새 배열의 요소를 추가해준다.

// Reducer
export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'bucket/CREATE': {
      const new_bucket_list = [...state.list, action.bucket];
      return { list: new_bucket_list };
    }

    default:
      return state;
  }
}

 

3. store 생성하기

전역 데이터를 저장할 store를 만든다. 아래의 이미지처럼 redux 폴더 안에 configStore.js 파일을 생성한다.

 

3.1 redux 라이브러리에서 createStore, combineReducers 함수 import하기

  • createStore: store를 만드는 함수
  • combineReducers: 여러개의 리듀서를 묶어주는 함수

리덕스로 전역 상태 관리를 할때는 단 하나의 store만 가질 수 있다. 프로젝트당 단 하나의 저장소만 가질 수 있지만 리듀서는 여러개 가질 수 있는데 이때 여러개의 리듀서를 묶어주는 함수를 combineReducers라고 하고 이 함수를 redux 라이브러리에서 호출해주어야한다.

import { createStore, combineReducers } from 'redux';

3.2 import한 리듀서를 combineReducers의 인자로 넣어주기

현재는 reducer가 하나밖에 없지만 이후에 추가적으로 리듀서를 더 생성할 수 있으므로 combineReducers 함수를 사용해서 리듀서를 묶어준다. 이때 아래의 코드에서 import 해준 bucket은 이전에 reducer 함수로 리턴해준 값을 의미한다.

import bucket from './modules/bucket';
const rootReducer = combineReducers({ bucket });

3.3 createStore 함수로 저장소를 생성한다

createStore 함수는 인자로 reducer 함수를 가진다. 프로젝트에 있는 모든 리듀서를 묶어준 값이 있는 rootReducer를 createStore함수의 인자로 전달해준다. 또한 해당 저장소를 export해 컴포넌트에서 state를 구독할 수 있게 한다.

const store = createStore(rootReducer);

export default store;

 

4. 리덕스와 컴포넌트 연결하기 (Provider)

리액트 컴포넌트가 리덕스에 저장된 state를 구독하기 위해 리덕스와 컴포넌트를 연결해주는 작업이 필요하다. react-redux 라이브러리에서 Provider 컴포넌트를 import해 가장 상위 컴포넌트인 App을 감싸주고 state가 저장된 store를 넘겨준다. 이 작업을 통해 App 컴포넌트를 포함한 하위에 있는 컴포넌트가 리덕스 저장소에 있는 state에 접근할 수 있게 된다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './redux/configStore';

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

reportWebVitals();

 

5. 컴포넌트에서 리덕스 데이터 사용하기 (useSelector)

1. 리덕스 스토어에서 state를 가지고 오는 리덕스 훅 useSelector를 import한다

import { useSelector } from 'react-redux';

2. useSelector() 함수에 인자로 화살표 함수를 넣어준다

useSelector(() => ());

3. 이때 state는 리덕스가 가지고 있는 모든 데이터를 의미하므로 useSelector(state => state)는 모든 데이터를 리턴해준다

useSelector(state => state);

4. state안에는 스토어에 저장된 { bucket: { list: ['영화관 가기', '매일 책읽기', '수영 배우기'] }; } 값이 들어 있다

console.log(useSelector(state => state));
// { bucket: { list: ['영화관 가기', '매일 책읽기', '수영 배우기'] }; }

5. 접근해야할 값은 안에 들어있는 배열이므로 useSelector(state => state.bucket.list);를 리턴한 후 my_list 변수에 담아준다

const my_lists = useSelector(state => state.bucket.list);

6. 받아온 값을 화면에 표시해주는 작업을 해준다

return (
<ListStyle>
  {my_lists.map((list, index) => {
    return (
      <ItemStyle
        className="list_item"
        key={index}
        onClick={() => {
          history.push('/detail/' + index);
        }}
      >
        {list}
      </ItemStyle>
    );
  })}
</ListStyle>
);

전체코드

import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
// 리덕스 스토어에서 state를 가지고 오는 리덕스 훅 useSelector를 import한다
import { useSelector } from 'react-redux';

const BucketList = props => {
 // useSelector()를 사용해 리덕스 스토어에 저장된 state에 접근한다
  const my_lists = useSelector(state => state.bucket.list);
  const history = useHistory();
  
  return (
    <ListStyle>
      {my_lists.map((list, index) => {
        return (
          <ItemStyle
            className="list_item"
            key={index}
            onClick={() => {
              history.push('/detail/' + index);
            }}
          >
            {list}
          </ItemStyle>
        );
      })}
    </ListStyle>
  );
};

const ListStyle = styled.div`
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
`;

const ItemStyle = styled.div`
  padding: 16px;
  margin: 8px;
  background-color: aliceblue;
`;

export default BucketList;

 

6. 리덕스 데이터 변경해주기 (useDispatch)

1. state를 업데이트시켜주는 리덕스 훅 useDispatch를 import한다

import { useDispatch } from 'react-redux';

2. 상수 dispatch에 usedispatch() 함수를 호출시켜 객체를 받는다

const dispatch = useDispatch();

3. useDispatch() 함수에서 리턴된 값을 가진 dispatch 상수는 state를 업데이트 시킬 수 있는 함수가 되었다

 dispatch();

4. dispatch 함수는 액션객체를 인자로 받는다

 dispatch( {type: '이름', data: {state 업데이트 내용}} );

5. bucket.js 모듈에서 만든 action creator 함수를 dispatch의 인자로 넣어주기 위해 사용할 action creator 함수를 import한다

import { createBucket } from './redux/modules/bucket';

6. dispatch에 import한 액션생성함수 createBucket을 인자로 전달해준다

dispatch(createBucket());

6. createBucket()함수는 기존 배열 데이터에 추가할 데이터를 인자로 가진다

 dispatch(createBucket(text.current.value));

전체코드

import React from 'react';
import styled from 'styled-components';
import { Route, Switch } from 'react-router-dom';
import BucketList from './BucketList';
import Detail from './Detail';
import NotFound from './NotFound';
// state를 업데이트시켜주는 리덕스 훅 useDispatch를 import한다
import { useDispatch } from 'react-redux';
// dispatch() 함수의 파라미터로 사용할 액션생성함수 import하기
import { createBucket } from './redux/modules/bucket';

function App() {
  const text = React.useRef(null);
  
  // useDispatch() 함수에서 리턴된 값을 가진 dispatch 상수는 state를 업데이트 시킬 수 있는 함수가 되었다
  const dispatch = useDispatch();

  const addBucketList = () => {
    // createBucket() 함수는 기존 배열 데이터에 추가할 데이터를 인자로 가진다 
    dispatch(createBucket(text.current.value));
  };

  return (
    <div className="App">
      <Container>
        <Title>내 버킷리스트</Title>
        <Line />
        <Switch>
          <Route path={'/'} exact>
            <BucketList />
          </Route>
          <Route path={'/detail/:index'}>
            <Detail />
          </Route>
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Container>
      {/* 리덕스에 있는 데이터를 업데이트하기 위해서 액션을 디스패치해줘야한다 */}
      <Input>
        <input type="text" ref={text} />
        <button onClick={addBucketList}>추가하기</button>
      </Input>
    </div>
  );
}

const Input = styled.div`
  max-width: 350px;
  min-height: 10vh;
  background-color: #fff;
  padding: 16px;
  margin: 20px auto;
  border-radius: 5px;
  border: 1px solid #ddd;
`;

const Container = styled.div`
  max-width: 350px;
  min-height: 60vh;
  background-color: #fff;
  padding: 16px;
  margin: 20px auto;
  border-radius: 5px;
  border: 1px solid #ddd;
`;

const Title = styled.h1`
  color: slateblue;
  text-align: center;
`;

const Line = styled.hr`
  margin: 16px 0px;
  border: 1px dotted #ddd;
`;

export default App;

 


 

Reference

댓글