리덕스를 통한 리액트 상태관리

상태관리! 왜 필요할까?

리덕스는 여러 컴포넌트가 동일한 상태를 보고 있을 때 굉장히 유용합니다!
또, 데이터를 관리하는 로직을 컴포넌트에서 빼면, 컴포넌트는 정말 뷰만 관리할 수 있잖아요!
코드가 깔끔해질테니, 유지보수에도 아주 좋겠죠. 🙂

상태관리 흐름을 알아보자!

[상태관리 흐름도]
딱 4가지만 알면 됩니다! Store, Action, Reducer, 그리고 Component!
아주 큰 흐름만 잘 파악해도 굳굳!

  • (1) 리덕스 Store를 Component에 연결한다.
  • (2) Component에서 상태 변화가 필요할 때 Action을 부른다.
  • (3) Reducer를 통해서 새로운 상태 값을 만들고,
  • (4) 새 상태값을 Store에 저장한다.
  • (5) Component는 새로운 상태값을 받아온다. (props를 통해 받아오니까, 다시 랜더링 되겠죠?)

리덕스 패키지 설치 & 공식문서 보기

리덕스는 아주 흔히 사용하는 상태관리 라이브러리입니다.
전역 상태관리를 편히 할 수 있게 해주는 고마운 친구죠!

리덕스 개념과 용어

리덕스는 데이터를 한 군데 몰아넣고, 여기저기에서 꺼내볼 수 있게 해주는 친구입니다.
아래 용어들은 리덕스의 기본 용어인데, 여러분이 키워드 삼기 좋은 용어들이에요. 앞으로 자주 볼 단어들이니 미리 친해집시다!

  • (1) State

    리덕스에서는 저장하고 있는 상태값("데이터"라고 생각하셔도 돼요!)를 state라고 불러요.
    딕셔너리 형태({[key]: value})형태로 보관합니다.

  • (2) Action

    상태에 변화가 필요할 때(=가지고 있는 데이터를 변경할 때) 발생하는 것입니다.

    // 액션은 객체예요. 이런 식으로 쓰여요. type은 이름같은 거예요! 저희가 정하는 임의의 문자열을 넣습니다.
    {type: 'CHANGE_STATE', data: {...}}
  • (3) ActionCreator

    액션 생성 함수라고도 부릅니다. 액션을 만들기 위해 사용합니다.

    //이름 그대로 함수예요!
    const changeState = (new_data) => {
    // 액션을 리턴합니다! (액션 생성 함수니까요. 제가 너무 당연한 이야기를 했나요? :))
    	return {
    		type: 'CHANGE_STATE',
    		data: new_data
    	}
    }
  • (4) Reducer

    리덕스에 저장된 상태(=데이터)를 변경하는 함수입니다.
    우리가 액션 생성 함수를 부르고 → 액션을 만들면 → 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서 → 새로운 데이터를 만들고 → 리턴해줍니다.

    // 기본 상태값을 임의로 정해줬어요.
    const initialState = {
    	name: 'mean0'
    }
    
    function reducer(state = initialState, action) {
    	switch(action.type){
    
    		// action의 타입마다 케이스문을 걸어주면, 
    		// 액션에 따라서 새로운 값을 돌려줍니다!
    		case CHANGE_STATE: 
    			return {name: 'mean1'};
    
    		default: 
    			return false;
    	}	
    }
  • (5) Store

    우리 프로젝트에 리덕스를 적용하기 위해 만드는 거예요!
    스토어에는 리듀서, 현재 애플리케이션 상태, 리덕스에서 값을 가져오고 액션을 호출하기 위한 몇 가지 내장 함수가 포함되어 있습니다.
    생김새는 딕셔너리 혹은 json처럼 생겼어요.
    내장함수를 어디서 보냐구요? → 공식문서에서요! 😉

  • (6) dispatch

    디스패치는 우리가 앞으로 정말 많이 쓸 스토어의 내장 함수예요!
    액션을 발생 시키는 역할을 합니다.

    // 실제로는 이것보다 코드가 길지만, 
    // 간단히 표현하자면 이런 식으로 우리가 발생시키고자 하는 액션을 파라미터로 넘겨서 사용합니다.
    dispatch(action); 

몰라도 되는, 하지만 알면 재미있는 이야기
리덕스는 사실, 리액트와 별도로 사용할 수 있는 친구입니다. 상태관리를 위해 다른 프론트엔드 프레임워크/라이브러리와 함께 쓸 수 있어요.

리덕스의 3가지 특징

  • (1) store는 1개만 쓴다!

    리덕스는 단일 스토어 규칙을 따릅니다. 한 프로젝트에 스토어는 하나만 씁니다.

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

    리액트에서도 state는 setState()나, useState() 훅을 써서만 변경 가능했죠!
    데이터가 마구잡이로 변하지 않도록 불변성을 유지해주기 위함입니다.
    불변성 뭐냐구요? 간단해요! 허락없이 데이터가 바뀌면 안된단 소리입니다!

    조금 더 그럴 듯하게 말하면, 리덕스에 저장된 데이터 = 상태 = state는 읽기 전용입니다.

    그런데... 액션으로 변경을 일으킨다면서요? 리듀서에서 변한다고 했잖아요?
    → 네, 그것도 맞아요. 조금 더 정확히 해볼까요!
    가지고 있던 값을 수정하지 않고, 새로운 값을 만들어서 상태를 갈아끼웁니다!
    즉, A에 +1을 할 때,
    A = A+1이 되는 게 아니고, A' = A+1이라고 새로운 값을 만들고 A를 A'로 바꾸죠.

  • (3) 어떤 요청이 와도 리듀서는 같은 동작을 해야한다!

    리듀서는 순수한 함수여야 한다는 말입니다.
    순수한 함수라는 건,

    • 파라미터 외의 값에 의존하지 않아야하고,
    • 이전 상태는 수정하지(=건드리지) 않는다. (변화를 준 새로운 객체를 return 해야합니다.)
    • 파라미터가 같으면, 항상 같은 값을 반환
    • 리듀서는 이전 상태와 액션을 파라미터로 받는다.

덕스(ducks) 구조

  • 보통 리덕스를 사용할 때는, 모양새대로 action, actionCreator, reducer를 분리해서 작성합니다.
    (액션은 액션끼리, 액션생성함수는 액션생성함수끼리, 리듀서는 리듀서끼리 작성합니다.)
  • 덕스 구조는 모양새로 묶는 대신 기능으로 묶어 작성합니다.
    (버킷리스트를 예로 들자면, 버킷리스트의 action, actionCreator, reducer를 한 파일에 넣는 거예요.)
  • 덕스 구조로 리덕스 모듈을 만들어볼거예요!

[외울 필요 없어요!]
덕스 구조를 잘 설명해 주는 사이트가 있습니다. 😉 헷갈리실 때 들어가서 읽어보시면 되고, 모듈을 새로 만들 때 복사해서 쓰셔도 좋습니다.
[사이트 바로가기→
](https://github.com/erikras/ducks-modular-redux)

리덕스 모듈 예제

```jsx
// widgets.js

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

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

// 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 };
}

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

첫번째 모듈 만들기

일단 폴더부터 만들어요!
src 폴더 아래에 redux라는 폴더를 만들고, 그 안에 modules라는 폴더를 만들어주세요.
modules 아래에 bucket.js라는 파일을 만들고 리덕스 모듈 예제를 붙여넣어주세요.
아래 과정을 하나하나 밟으며 버킷리스트 항목을 리덕스에서 관리하도록 고쳐봅시다!

  • (1) Action

    우리는 지금 버킷리스트를 가져오는 것, 생성하는 것 2가지 변화가 있죠?
    두 가지 액션을 만듭시다.

    // 액션 타입을 정해줍니다.
    const LOAD = "bucket/LOAD";
    const CREATE = "bucket/CREATE";
  • (2) initialState

    초기 상태값을 만들어줄거예요! 그러니까, 기본 값이죠.

    // 초기 상태값을 만들어줍니다.
    const initialState = {
      list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
    };
  • (3) Action Creactor

    액션 생성 함수를 작성합니다.

    // 액션 생성 함수예요.
    // 액션을 만들어줄 함수죠!
    export const loadBucket = (bucket) => {
      return { type: LOAD, bucket };
    };
    
    export const createBucket = (bucket) => {
      return { type: CREATE, bucket };
    };
  • (4) Reducer

    리듀서를 작성합니다.
    load할 땐, 가지고 있던 기본값을 그대로 뿌려주면 되겠죠?
    create할 땐, 새로 받아온 값을 가지고 있던 값에 더해서 리턴해주면 될거예요!
    (우리는 action으로 넘어오는 bucket이 text값인 걸 알고 있죠! 이미 추가해봤잖아요.)

    // 리듀서예요.
    // 실질적으로 store에 들어가 있는 데이터를 변경하는 곳이죠!
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        // do reducer stuff
        case "bucket/LOAD":
          return state;
    
        case "bucket/CREATE":
          const new_bucket_list = [...state.list, action.bucket];
          return { list: new_bucket_list };
    
        default:
          return state;
      }
    }
  • (5) Store

    redux 폴더 하위에 configStore.js 파일을 만들고 스토어를 만들어볼게요!

    //configStore.js
    import { createStore, combineReducers } from "redux";
    import bucket from "./modules/bucket";
    
    // root 리듀서를 만들어줍니다.
    // 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
    const rootReducer = combineReducers({ bucket });
    
    // 스토어를 만듭니다.
    const store = createStore(rootReducer);
    
    export default store;

Store 연결하기

store를 다 만들었으니 이젠 컴포넌트와 연결할 차례! (끝이 다와가요!)
index.js에서 필요한 작업을 해줄거예요.
스토어를 불러오고 → 우리 버킷리스트에 주입하면 끝!

1) index.js

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")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

컴포넌트에서 리덕스 데이터 사용하기

컴포넌트에서 리덕스 액션 사용하는 법

App 컴포넌트에 있는 state를 리덕스로 교체해볼까요?

  • (1) 컴포넌트에서 리덕스 데이터 사용하기

    • -1) 리덕스 훅

      리덕스도 훅이 있어요!
      상태, 즉, 데이터를 가져오는 것 하나, 상태를 업데이트할 수 있는 것 하나 🙂
      이렇게 두 가지를 정말 많이 쓴답니다!
      더 많은 훅이 궁금하다면? (훅 보러가기→)

      // useDispatch는 데이터를 업데이트할 때,
      // useSelector는 데이터를 가져올 때 씁니다.
      import {useDispatch, useSelector} from "react-redux";
    • -2) BucketList.js에서 redux 데이터 가져오기

      useSelector((**state**) ⇒ state.bucket)
      configStore.js에서 루트 리듀서를 만들었던 거 기억하시나요?
      앗, 바로 감이 오셨나요? 네, 맞아요! 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.

      ...
      // redux 훅 중, useSelector를 가져옵니다.
      import { useSelector } from "react-redux";
      
      const BucketList = (props) => {
        let history = useHistory();
        //   이 부분은 주석처리!
        //   console.log(props);
        //   const my_lists = props.list;
        // 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
        // 우리는 그 중, bucket 안에 들어있는 list를 가져옵니다.
        const my_lists = useSelector((state) => state.bucket.list);
        return (
          <ListStyle>
            {my_lists.map((list, index) => {
              return (
                <ItemStyle
                  className="list_item"
                  key={index}
                  onClick={() => {
                    history.push("/detail");
                  }}
                >
                  {list}
                </ItemStyle>
              );
            })}
          </ListStyle>
        );
      };
      ...
    • -3) App.js에서 redux 데이터 추가하기

      useSelector((**state**) ⇒ state.bucket)
      configStore.js에서 루트 리듀서를 만들었던 거 기억하시나요?
      앗, 바로 감이 오셨나요? 네, 맞아요! 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.

      • import 부터!

        // useDispatch를 가져와요!
        import {useDispatch} from "react-redux";
        // 액션생성함수도 가져오고요!
        import { createBucket } from "./redux/modules/bucket";
      • useDispatch 훅 쓰기

        const dispatch = useDispatch();
        
        const addBucketList = () => {
            // 스프레드 문법! 기억하고 계신가요? :)
            // 원본 배열 list에 새로운 요소를 추가해주었습니다.
            // 여긴 이제 주석처리!
            // setList([...list, text.current.value]);
        
            dispatch(createBucket(text.current.value));
          };
    • [코드스니펫] - App.js

      import React from "react";
      import styled from "styled-components";
      import { Route, Switch } from "react-router-dom";
      // useDispatch를 가져와요!
      import {useDispatch} from "react-redux";
      // 액션생성함수도 가져오고요!
      import { createBucket } from "./redux/modules/bucket";
      
      // BucketList 컴포넌트를 import 해옵니다.
      // import [컴포넌트 명] from [컴포넌트가 있는 파일경로];
      import BucketList from "./BucketList";
      import Detail from "./Detail";
      import NotFound from "./NotFound";
      
      function App() {
        
        const text = React.useRef(null);
        // useHistory 사용하는 것과 비슷하죠? :)
        const dispatch = useDispatch();
      
        const addBucketList = () => {
          // 스프레드 문법! 기억하고 계신가요? :)
          // 원본 배열 list에 새로운 요소를 추가해주었습니다.
          // 여긴 이제 주석처리!
          // setList([...list, text.current.value]);
      
          dispatch(createBucket(text.current.value));
        };
      
        return (
          <div className="App">
            <Container>
              <Title>내 버킷리스트</Title>
              <Line />
              {/* 컴포넌트를 넣어줍니다. */}
              {/* <컴포넌트 명 [props 명]={넘겨줄 것(리스트, 문자열, 숫자, ...)}/> */}
              <Switch>
                {/* <Route
                  path="/"
                  exact
                  render={(props) => <BucketList list={list} />}
                /> */}
                {/* 이제는 render를 사용해서 list를 넘겨줄 필요가 없죠! 버킷리스트가 리덕스에서 데이터를 알아서 가져갈거니까요! */}
                <Route exact path="/" component={BucketList} />
                <Route exact path="/detail" component={Detail} />
                <Route component={NotFound} />
              </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;
    • [코드스니펫] - BucketList.js

      // 리액트 패키지를 불러옵니다.
      import React from "react";
      import styled from "styled-components";
      
      import { useHistory } from "react-router-dom";
      // redux 훅 중, useSelector를 가져옵니다.
      import { useSelector } from "react-redux";
      
      const BucketList = (props) => {
        let history = useHistory();
        //   이 부분은 주석처리!
        //   console.log(props);
        //   const my_lists = props.list;
        // 여기에서 state는 리덕스 스토어가 가진 전체 데이터예요.
        // 우리는 그 중, bucket 안에 들어있는 list를 가져옵니다.
        const my_lists = useSelector((state) => state.bucket.list);
        return (
          <ListStyle>
            {my_lists.map((list, index) => {
              return (
                <ItemStyle
                  className="list_item"
                  key={index}
                  onClick={() => {
                    history.push("/detail");
                  }}
                >
                  {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;
      • (2) 상세페이지에서 버킷리스트 내용을 띄워보기
    • -1) 몇 번째 상세에 와있는 지 알기 위해, URL 파라미터를 적용하자

      // App.js
      <Route exact path="/detail/:index" component={Detail} />
      //BucketList.js
      ...
            {my_lists.map((list, index) => {
              return (
                <ItemStyle
                  className="list_item"
                  key={index}
                  onClick={() => {
                    history.push("/detail/"+index);
                  }}
                >
                  {list}
                </ItemStyle>
              );
            })}
      ...
    • -2) 상세페이지에서 버킷리스트 내용을 띄워보자

      //Detail.js
      // 리액트 패키지를 불러옵니다.
      import React from "react";
      // 라우터 훅을 불러옵니다.
      import {useParams} from "react-router-dom";
      // redux hook을 불러옵니다.
      import { useSelector } from "react-redux";
      
      const Detail = (props) => {
        // 스토어에서 상태값 가져오기
        const bucket_list = useSelector((state) => state.bucket.list);
        // url 파라미터에서 인덱스 가져오기
        const params = useParams();
        const bucket_index = params.index;
      
        return <h1>{bucket_list[bucket_index]}</h1>;
      };
      
      export default Detail;

      하아... 리덕스... 흐름은 이해하겠는데 직접사용하기가 너무 힘들다... 머리가 너무 아프다 진짜 할수있을까 싶을정도다.
      일딴 반복해서 계속 듣고 쓰고 있는데 익숙해지면 좋아질꺼라고 굳게 믿고 해본다...

profile
i'm groot

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN