본문 바로가기

Redux

[Redux] 🏃‍♂️ 리액트와 리덕스 함께 사용하기 (feat. ant-design) (2)

이전 글에 이어서 이번 글에서는 노트 앱의 컨테이너와 컴포넌트를 구성해보겠습니다. 폴더 구성은 컨테이너와 컴포넌트 두 가지로 나눌 예정인데, 결국 컨테이너도 마찬가지로 컴포넌트입니다. 하지만, 명칭을 나누는 이유는 상태 값의 변화를 담당하고 불러와야 하는 역할을 하는 똑똑한 컴포넌트를 컨테이너라 부르며, 유저에게 보이는 view 만을 구성하면 되는 컴포넌트를 멍청한 컴포넌트라 부릅니다. 이를 구분하여 파일을 구성하면 컴포넌트를 관리하고 구성하는데 훨씬 수월해지는 효과를 가져옵니다. 더 자세한 내용은 아래 링크를 참고하시면 좋을 것 같습니다.

 

 

Functional vs Class-Components in React

In this article I want to show you the differences between functional and class components in React and when you should choose which one.

medium.com

 

 

Stateful and Stateless Components in React - Programming with Mosh

Stateful and Stateless Components -- what are they, how do you write each, and when should you make a component stateful or stateless?

programmingwithmosh.com

9. 멍청한 컴포넌트 구성 (noteLayout.note.js)

src/components/contentNoteLayout/noteLayout.note.js 파일을 새롭게 생성하겠습니다. 노트를 담당하는 컴포넌트입니다. UI는 이전에 설치했던 antd가 책임져 줍니다. framework로 구성된 UI를 사용하는 것은 커스터마이즈하기엔 불편하지만, 간단한 앱을 만들 때 UI를 구성하기 좋습니다.

 

import React from "react";
import { Card, Button, Typography } from "antd";
import { DeleteOutlined } from "@ant-design/icons";

const { Paragraph } = Typography;

const note = (props) => {
  return (
    <Card
      hoverable="true"
      key={props.id}
      id={props.id}
      title={<h3>{props.title}</h3>}
      extra={
        <Button danger type="ghost" onClick={() => props.onRemove(props.id)}>
          {<DeleteOutlined />}
        </Button>
      }
    >
      <Paragraph>{props.content}</Paragraph>
    </Card>
  );
};

export default note;

 

10. 멍청한 컴포넌트 구성 (noteLayout.addForm.js)

다음으로는 제목과 내용을 입력받고, 플러스 버튼을 통해 note를 추가할 수 있는 addForm을 담당하는 컴포넌트를 만들어 보겠습니다.

src/components/contentNoteLayout/noteLayout.addForm.js 파일을 생성하고 아래 코드를 추가하겠습니다.

 

import React from "react";
import { Card, Button, Input } from "antd";
import { PlusOutlined } from "@ant-design/icons";

const { TextArea } = Input;

const addForm = (props) => {
  return (
    <Card
      key="addForm"
      id="addForm"
      title={
        <Input
          required
          value={props.inputTitle}
          placeholder="제목을 입력하세요."
          style={{ width: "60%" }}
          onChange={props.onChangeTitle}
        ></Input>
      }
      extra={
        <Button type="primary" onClick={props.onSubmit}>
          {<PlusOutlined />}
        </Button>
      }
    >
      <TextArea
        placeholder="내용을 입력하세요."
        rows={3}
        onChange={props.onChangeContent}
        value={props.inputContent}
      ></TextArea>
    </Card>
  );
};

export default addForm;

 

11. 멍청한 컴포넌트 구성 (noteLayout.js)

note와 addForm을 묶어서 하나의 layout으로 구성해줄 컴포넌트입니다. 처음부터 하나의 컴포넌트로 구성해서 작성해도 문제는 없지만, 컴포넌트를 최대한 잘게 나누어 구성하는 습관을 갖기 위해서 둘로 나누었고, 그것을 묶어줄 컴포넌트까지 만들어 주었습니다. 

src/components/contentNoteLayout/noteLayout.js

 

import React from "react";
import Note from "./noteLayout.note";
import AddForm from "./noteLayout.addForm";
import { Layout } from "antd";

const noteLayout = ({
  inputTitle,
  inputContent,
  noteList,
  onChangeTitle,
  onChangeContent,
  onSubmit,
  onRemove,
}) => {
  const notes = noteList.map((n) => (
    <Note
      key={n.id}
      title={n.title}
      content={n.content}
      id={n.id}
      onRemove={() => onRemove(n.id)}
    />
  ));

  return (
    <Layout>
      <AddForm
        inputTitle={inputTitle}
        inputContent={inputContent}
        onChangeTitle={onChangeTitle}
        onChangeContent={onChangeContent}
        onSubmit={onSubmit}
      ></AddForm>
      <div>{notes}</div>
    </Layout>
  );
};

export default noteLayout;

 

12. 똑똑한 컴포넌트(Container) 구성 (noteListContainer.js)

12) 1. 모듈 import

이제 드디어 store와 연결시켜 상태값들을 불러와 관리하는 컨테이너를 생성해볼 텐데, 복잡하니 코드를 나누어서 하나하나 추가 해나가 보도록 하겠습니다.

src/containers/noteListContainer.js

 

import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as noteActions from "../store/modules/note";
//import note, addnote;
import NoteLayout from "../components/contentNoteLayout/noteLayout";

 

  • React와 component를 불러옵니다.
  • store와 container를 연결할 수 있게 해주는 react-redux의 connect를 불러옵니다.
  • 모듈에서 정의한 액션생성자들을 묶어주는 redux의 bindActionCreators를 불러옵니다.
  • 모듈에서 정의한 액션생성자들을 noteActions이라는 이름으로 *(모두) 불러옵니다.
  • view역할을 하는 멍청한 컴포넌트 noteLayout을 불러옵니다.

12) 2. Container의 큰 틀 구성

 

아래와 같이 Container의 큰 틀을 잡아줍니다. 

 

class NoteListContainer extends Component {
  render(){
    return(

    );
  }
}

//props값으로 불러올 store의 state값
const mapStateToProps = ({ note }) => ({
  inputTitle: note.inputTitle,
  inputContent: note.inputContent,
  noteList: note.noteList,
});

//props로 넣어줄 액션 생성함수
const mapDispatchToProps = (dispatch) => ({
  NoteActions: bindActionCreators(noteActions, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(NoteListContainer);

 

  • mapStateToProps : Containers에서 사용할 props을 정의해줍니다. 쉽게 설명하자면, store라는 부모 객체에 있는 state들을 자식 객체에서 사용하기 위해 props로 받듯이, 마찬가지로 { note } 모듈에 있는 state들을 props로 받아 사용하겠다는 것입니다.
  • mapDispatchToProps : props로 넣어줄 액션 생성함수를 정의해줍니다. dispatch는 액션이 발생했다고 리듀서에게 알리는 함수라고 생각하시면 됩니다. 위에서 noteActions들을 note 모듈에서 모두 불러왔고, 그것을 NoteActions이란 이름으로 Container에서 사용하겠다는 것입니다. NoteActions이 호출되면 dispatch는 리듀서에게 알려 적절한 상태 변화를 이르킬 것입니다.

큰 틀을 먼저 구성하다보니 이해가 어려울 수 있습니다. 이제 함수를 하나하나 정의해보면 이해가 더 쉽게 될 거라 생각이 듭니다!

 

12) 3. 제목입력란 onChange 함수 정의 

드디어, props로 불러온 액션 함수를 사용해보겠습니다. mapDispatchToProps를 통해 note모듈에서 정의한 액션 함수들을 NoteActions이라는 이름으로 props에 넣어주었습니다. 그리고 아래와 같이 this.props를 구조 분해 할당을 통해 NoteActions을 가져와 note모듈에 정의돼있던 changeInputTitle을 호출할 수 있는 것입니다. 

 

class NoteListContainer extends Component {
  
  handleChangeTitle = (e) => {                        //추가 시작
    const { NoteActions } = this.props;
    NoteActions.changeInputTitle(e.target.value);
  };                                                  //추가 끝

  render(){
    return(

    );
  }
}

//props값으로 불러올 store의 state값
const mapStateToProps = ({ note }) => ({
  inputTitle: note.inputTitle,
  inputContent: note.inputContent,
  noteList: note.noteList,
});

//props로 넣어줄 액션 생성함수
const mapDispatchToProps = (dispatch) => ({
  NoteActions: bindActionCreators(noteActions, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(NoteListContainer);

 

12) 4. 추가적인 함수 정의

onChangeTitle과 마찬가지로, onChangeContent 함수를 정의해주었고, 등록이벤트, 삭제 이벤트를 아래와 같이 정의해주었습니다. 

 

//inputContent 변경 이벤트
  handleChangeContent = (e) => {
    const { NoteActions } = this.props;
    NoteActions.changeInputContent(e.target.value);
  };

  //등록 이벤트
  handleSubmit = (e) => {
    e.preventDefault();
    const { NoteActions, inputTitle, inputContent } = this.props;
    NoteActions.addNote(inputTitle, inputContent);
    NoteActions.changeInputTitle("");
    NoteActions.changeInputContent("");
  };

  //삭제 이벤트
  handleRemove = (id) => {
    const { NoteActions } = this.props;
    NoteActions.removeNote(id);
  };

 

12) 5. 랜더링

마지막으로 멍청한 컴포넌트인 NoteLayout에 porps값을 전달해주면 Container 완성입니다.

 

render() {
    const { inputTitle, inputContent, noteList } = this.props;
    return (
      <NoteLayout
        inputTitle={inputTitle}
        inputContent={inputContent}
        noteList={noteList}
        onChangeTitle={this.handleChangeTitle}
        onChangeContent={this.handleChangeContent}
        onSubmit={this.handleSubmit}
        onRemove={this.handleRemove}
      />
    );
  }

 

13. app.js 수정하기

최종적으로 app.js에는 열심히 만든 Container를 넣어주기만 하면 됩니다.

 

import React, { Component } from "react";
import "./App.css";
import "antd/dist/antd.css";

import NoteListContainer from "./containers/noteListContainer";

class App extends Component {
  render() {
    return (
      <div>
        <NoteListContainer />
      </div>
    );
  }
}

export default App;

 

14. 스토어 만들기

스토어는 무조건 하나! 딱 한번 생성해주면 됩니다. src/index.js 파일을 아래와 같이 수정하겠습니다.

 

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "react-redux";

import { createStore } from "redux";
import rootReducer from "./store/modules";

const devTools =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);

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

serviceWorker.unregister();

 

  • react-redux의 Provider를 불러옵니다.
  • redux의 createStore를 불러옵니다. 
  • combineReducers를 통해 모듈을 결합한 루트리듀서를 불러옵니다.
  • devtool은 크롬 extension입니다. redux의 상태, 상태변화 등에 대한 로그를 쉽게 확인할 수 있는 굉장히 유용한 툴입니다. 아래 링크에서 설치할 수 있습니다.

 

 

Redux DevTools

Redux DevTools for debugging application's state changes.

chrome.google.com

 

15. 앱 실행

~$ npm start

앱이 잘 작동됩니다! 잘 작동되지 않는다면, 아래 최종코드나 github를 참고하셔서 완성해보시길 바랍니다. 하지만, 코드를 모두 복붙 하는 것은 지양하시길 바랍니다. 코드 한줄한줄 모두 핵심적인 코드라 이해하는 것이 더 중요합니다!

 

최종 코드

src/containers/noteListContainer.js

import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import * as noteActions from "../store/modules/note";
//import note, addnote;
import NoteLayout from "../components/contentNoteLayout/noteLayout";

class NoteListContainer extends Component {
  //inputTitle 변경 이벤트
  handleChangeTitle = (e) => {
    const { NoteActions } = this.props;
    NoteActions.changeInputTitle(e.target.value);
  };

  //inputContent 변경 이벤트
  handleChangeContent = (e) => {
    const { NoteActions } = this.props;
    NoteActions.changeInputContent(e.target.value);
  };

  //등록 이벤트
  handleSubmit = (e) => {
    e.preventDefault();
    const { NoteActions, inputTitle, inputContent } = this.props;
    NoteActions.addNote(inputTitle, inputContent);
    NoteActions.changeInputTitle("");
    NoteActions.changeInputContent("");
  };

  //삭제 이벤트
  handleRemove = (id) => {
    const { NoteActions } = this.props;
    NoteActions.removeNote(id);
  };

  render() {
    const { inputTitle, inputContent, noteList } = this.props;
    return (
      <NoteLayout
        inputTitle={inputTitle}
        inputContent={inputContent}
        noteList={noteList}
        onChangeTitle={this.handleChangeTitle}
        onChangeContent={this.handleChangeContent}
        onSubmit={this.handleSubmit}
        onRemove={this.handleRemove}
      />
    );
  }
}

//props값으로 불러올 store의 state값
const mapStateToProps = ({ note }) => ({
  inputTitle: note.inputTitle,
  inputContent: note.inputContent,
  noteList: note.noteList,
});

//props로 넣어줄 액션 생성함수
const mapDispatchToProps = (dispatch) => ({
  NoteActions: bindActionCreators(noteActions, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(NoteListContainer);

 

src/app.js

import React, { Component } from "react";
import "./App.css";
import "antd/dist/antd.css";

import NoteListContainer from "./containers/noteListContainer";

class App extends Component {
  render() {
    return (
      <div>
        <NoteListContainer />
      </div>
    );
  }
}

export default App;

 

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "react-redux";

import { createStore } from "redux";
import rootReducer from "./store/modules";

const devTools =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);
console.log(store.getState() + "스토어");

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

serviceWorker.unregister();