9일차

그루트·2021년 9월 23일
0

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

상태관리! 왜 필요할까?

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

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

[상태관리 흐름도]
딱 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개의 댓글