본문 바로가기
내마음대로만들어보자/React

[복습]React 주요 개념정리 - 리덕스를 통한 상태 관리

by 소농민! 2021. 9. 26.
728x90

1. 상태관리의 필요성

현재는 App.js에 리스트 항목 배열을 두고 props로 넘겨주고 있고 추가하기버튼도 App.js에 있는 상황이다.

 

만일 추가하기버튼과 텍스트영역을 AddListItem 컴포넌트로 분리하고싶은경우 어떻게해야될까!

단순히 파일을 만들고 코드를 분리한다면 문제점이 발생한다.

→ 자식 컴포넌트는 부모 컴포넌트의 state를 마음대로 수정할 수 없다.

     (데이터는 단방향으로만 흐리기때문에)

 

이러한 부분들을 리덕스를 통해 해결을 할 수 있다.

리덕스는 여러 컴포넌트가 동일한 상태를 보고 있을때 유용하게 사용할 수 있다. 또한 데이터를 관리하는 로직을 빼면 컴포넌트는 오직 뷰만 관리하면 되기때문에 유지보수에도 용이하다.

 

2. 상태관리 흐름

1. 리덕스 Store를 Component에 연결한다.

2. Component에서 상태 변화가 필요할때 Action을 부른다.

3. Reducer를 통해 새로운 상태 값을 만들고 새 상태값을 Store에 저장한다.

4. Component는 새로운 상태 값을 받아온다. props를 통해 받아오기때문에 다시 랜더링 된다. 

 

3. 리덕스 사용해보기

 

리덕스를 사용하기 앞서 기본 덕스구조에 대해 참고할만한 사이트가 있다.

https://github.com/erikras/ducks-modular-redux

 

GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

A proposal for bundling reducers, action types and actions when using Redux - GitHub - erikras/ducks-modular-redux: A proposal for bundling reducers, action types and actions when using Redux

github.com

이런 기본 형태는 외우기보다 자주 사용하다보면은 자연히 익혀지도록 자주보고 사용해보는게 좋을 것 같다.

 

보통 리덕스를 사용할때는 모양새대로 action, actionCreator, reducer를 분리해서 끼리끼리 작성해서 사용한다.

(위의 예시에 나와있는 덕스구조에서는 모양새로 묶는것이 아닌 기능대로 묶어서 사용한다.)

 

- 폴더 만들기

src 폴더 아래에 redux 폴더를 생성 후 그안에 modules 라는 폴더를 만들어주자.

이제 modules라는 폴더에 bucket.js 파일을 만들어 여기에 본격적으로 리덕스에 대한 코드를 작성해보자.

 

아직은 폴더,파일을 관리하는 부분에 대해서는 배운대로 따라해보고 내가 보다 관리하기 편한 방식이 있다면 

바꿔나가는게 좋을 것 같다. 처음부터 이런 규칙없이 막 생성하는경우 복잡하고 꼬이는경우가 생길 수 있다.

 

- 리덕스 모듈 만들기

1) Action

  → 여기에서는 버킷리스트를 가져오는 것과 생성하는것 2가지의 상태 변화가 필요하다.

  const LOAD = 'bucket/LOAD';

  const CREATE = 'bucket/CREATE';

 

2) initialState

  → 초기 상태 값을 만들어주자.

  const initialState = {

       list : ["EPL관람","습관고치기","공부목표달성"]

   }

 

3) Action Creator

  → 액션생성함수를 작성한다.

  export const loadBucket = (bucket) => {

         return {type: LOAD, bucket};

  }

 

  export const createBucket = (bucket) => {

         return {type: CREATE, bucket};

  }

 

4) Reducer

  → 이제 리듀서를 작성하면 되는데 우선은 버킷리스트를 가져오는것과 생성하는 것 2가지를 고려해서 작성하면된다.

      load 할때는 설정한 초기값을 보여주면되고 create 할때는 새로받아온 값과 초기값을 더해서 보여주면된다.

 

  export default function reducer(state = initialState, action = {}) {

       switch (action.type) {

            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 파일을 만들어 스토어를 만들어보자.

 

import { createStore, combineReducers } from "redux";

import bucket from './modules/bucket';
import { createBrowserHistory } from "history";

 

//브라우저 히스토리를 만들어준다.
export const history = createBrowserHistory();

 

//root 리듀서를 만들어준다. 
// 나중에 리듀서를 여러개 만들게될경우 여기에 추가하면된다. 

const rootReducer = combineReducers({ bucket });

 

//스토어를 만든다. 
const store = createStore(rootReducer);

export default store;

 

 

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

 

- Store 연결하기

   store를 만들었으니 연결하기 위해 index.js에 필요한 내용을 추가해보자.

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

 

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

   1) 리덕스 모듈과 connect 함수를 불러온다.

   2) 상태값을 가져오는 함수와 액션생성함수를 불러온다.

   3) connect로 컴포넌트와 스토어를 엮어 준다.

   4) 콘솔에 this.props를 찍어봐서 스토어에 있는 값이 잘 나오는지 확인해준다.

   5) this.state에 있는 list를 지우고 스토어에 있는 값으로 바꿔준다.

   6) setState를 this.props.create로  바꿔준다. 

 

* 적용 예시

import React from "react";

import { withRouter } from "react-router";

import { Route, Switch } from "react-router-dom";

 

// import [컴포넌트 명] from [컴포넌트가 있는 파일경로];

import BucketList from "./BucketList";

import styled from "styled-components";

import Detail from "./Detail";

import NotFound from "./NotFound";

import Progress from "./Progress";

import Spinner from "./Spinner";

 

// 리덕스 스토어와 연결하기 위해 connect라는 친구를 호출한다. 

import {connect} from 'react-redux';

// 리덕스 모듈에서 (bucket 모듈에서) 액션 생성 함수 두개를 가져온다. 

import {loadBucket, createBucket, loadBucketFB, addBucketFB} from './redux/modules/bucket';

import { firestore } from "./firebase";

 

// 이 함수는 스토어가 가진 상태값을 props로 받아오기 위한 함수

const mapStateToProps = (state) => ({

bucket_list: state.bucket.list,

is_loaded: state.bucket.is_loaded,

});

 

// 이 함수는 값을 변화시키기 위한 액션 생성 함수를 props로 받아오기 위한 함수

const mapDispatchToProps = (dispatch) => ({

load: () => {

dispatch(loadBucketFB());

},

create: (new_item) => {

console.log(new_item);

dispatch(addBucketFB(new_item));

}

});

 

// 클래스형 컴포넌트 시작 

class App extends React.Component {

constructor(props) {

super(props);

// App 컴포넌트의 state를 정의

this.state = {

 

};

// ref는 이렇게 선언한다. 

this.text = React.createRef();

}

 

componentDidMount() {

this.props.load();

// const bucket = firestore.collection("buckets");

 

// bucket.doc("bucket_item").set({text: "EPL보러가기", compeleted: false});

 

// bucket.doc("bucket_item1").get().then((doc) => {

// if(doc.exists) {

// console.log(doc);

// console.log(doc.data());

// console.log(doc.id);

// }

// console.log(doc.exists);

// });

 

// bucket.get().then(docs => {

// let bucket_data = [];

 

// docs.forEach((doc) => {

// if(doc.exists){

// bucket_data = [...bucket_data, {id: doc.id, ...doc.data()}]

// }

// });

 

// console.log(bucket_data);

// });

 

// bucket.add({text:"리액트 마스터", compeleted: false}).then((docRef) => {

// console.log(docRef);

// console.log(docRef.id);

// });

 

// bucket.doc("bucket_item2").update({text: "세계일주"});

 

// bucket.doc("bucket_item2").delete().then(docRef => {

// console.log("삭제완료!!")

// });

}

 

addBucketList = () => {

const new_item = this.text.current.value;

this.props.create(new_item);

};

 

// 랜더 함수 안에 리액트 엘리먼트를 넣어줍니다!

render() {

return (

<div className="App">

{!this.props.is_loaded? (<Spinner/>) :(

<React.Fragment>

<Container>

<Title>내 버킷리스트</Title>

<Progress/>

<Line />

{/* 컴포넌트를 넣어줍니다. */}

{/* <컴포넌트 명 [props 명]={넘겨줄 것(리스트, 문자열, 숫자, ...)}/> */}

{/* Route 쓰는 법 2가지를 모두 써봅시다! */}

<Switch>

<Route

path="/"

exact

render={(props) => (

<BucketList

list={this.props.bucket_list}

history={this.props.history}

/>

)}

/>

<Route path="/detail/:index" component={Detail} />

<Route

render={(props) => <NotFound history={this.props.history} />}

/>

</Switch>

</Container>

{/* 인풋박스와 추가하기 버튼을 넣어줬어요. */}

<Input>

<input type="text" ref={this.text} />

<button onClick={this.addBucketList}>추가하기</button>

</Input>

<button onClick={() => {

window.scrollTo({top:0, left:0, behavior: "smooth"});

}}>위로가기</button>

</React.Fragment>

)}

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

display: flex;

align-items: center;

justify-content: space-between;

 

& > * {

padding: 5px;

}

 

& input {

border-radius: 5px;

margin-right: 10px;

width: 70%;

&:focus{ outline: 2px solid #59d393;}

}

 

& button {

width: 25%;

border-radius: 5px;

color: #fff;

border: 1px solid #59d393;

background-color: #59d393;

}

`;

 

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;

`;

// withRouter 적용

// connect로 묶어준다. 

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

 

- 함수형 컴포넌트에 리덕스 데이터 사용하기

리덕스도 리액트 처럼 훅이 있고 훅을 사용해서 액션생성함수도 불러오고 스토어에 저장된 값도 가져올 수 있다.

 

1) BucketList.js에 useSeletor() 적용하기

 

...

// redux hook 을 불러온다. 
import {useDispatch, useSelector} from 'react-redux';

 

const BucketList = (props) => {

//버킷리스트를 리덕스 훅으로 가져오는 방법 

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

 

console.log(bucket_list);

 

return (

   ...

 

2) 몇번째 상세페이지에 와있는지 알기위해 URL 파라미터를 적용해주자.

App.js

...

<Switch>

    <Route 

        path="/" 
        exact
        render={(props) => <BucketList history={this.props.history} />}

        />

    <Route

       path="/detail/:index"

       render={(props) => <Detail history={this.props.history} />} />

    <Route
       render={(props) => <NotFound history={this.props.history} />}

    />

</Switch>

...

 

BucketList.js

...

   {bucket_list.map((list, index) => {

       return (

           <ItemStyle

               className="list_item"

               key={index}

               onClick={() => {

// 배열의 몇번째 항목을 눌렀는지에 대해  url파라미터로 넘겨준다. 

                   props.history.push("/detail"+index);

                   }}

               >

                   {list}

                   </ItemStyle>

               );

})}

    </ListStyle>

);

};

...

 

 

3) 상세페이지에서 버킷리스트를 띄워보자.

Detail.js

import React from "react";

 

// redux hook을 불러온다.
import { useDispatch, useSelector } from "react-redux";

 

const Detail = (props) => {

// 스토어 상태값 가져오기

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

// url파라미터 인덱스 가져오기 
       let bucket_index = parseInt(props.match.params.index);

 

       return <h1>{bucket_list[bucket_index]}</h1>; };

 

export default Detail;