티스토리 뷰

Redux - BookList 예제 - 1. 컨테이너 만들기

Why Redux?

  • 리액트에서는 스테이트에 접근하기 위해서 부모 컴포넌트로부터 자식 컴포넌트로 계속 props를 전달해야만 했다. 이 관계가 복잡해지고 멀어질 수록 스테이트가 어디서 어떻게 변화하고 있는지 파악하기 어려워지며 비효율적이다.
  • 그러나 리덕스를 사용하면 스테이트에 접근하고자 하는 컴포넌트가 부모 컴포넌트를 통하지 않고도 직접 스테이트에 접근할 수 있게 해준다. 따라서 리액트에 스테이트 관리자인 리덕스를 추가하여 사용하면, 다음과 같은 장점을 누릴 수 있다.
    • Predictable state updates - 데이터 플로우를 이해하기 쉽게 해준다
    • 순수 함수인 리듀서를 사용함으로써 로직을 테스트하기 쉽다.
    • (스토어라는 하나의 객체에 모든 스테이트를 저장하므로) 스테이트를 centralize함으로써 데이터 변경 사항을 로깅하거나 페이지를 새로 고침할 때 데이터를 지속하는 작업들이 용이해진다.

컨테이너란?

  • 리액트의 기존 컴포넌트와 구별되는 개념으로, 스테이트에 직접 접근이 가능한 컴포넌트를 말한다. 리액트에서는 스테이트에 접근하기 위해 부모 컴포넌트에게 값을 보내고 받아야 했지만 컨테이너로 만든 컴포넌트는 리덕스 스토어가 보관하고 있는 스테이트에 직접 접근한다.

 

북리스트 만들기 예제

  • 전체 북리스트가 존재하고(master view), 이 리스트 중 특정 아이템을 사용자가 선택하면 선택된 책의 정보를 자세히 보여주는(detail view) 간단한 구조의 리액트-리덕스 앱을 만들어 보자!
  •  

북리스트 생성을 위한 주요 파일 구조

  • reducer : reducer_books.js북리스트의 전체 데이터가 이렇게 있다고 가정하자. 이 함수는 항상 전체 책 객체의 배열을 반환한다.
  •  
  • export default function() { return [ {title: 'Javascript Design Patterns'}, {title: 'Effective Java'}, {title: 'Deep Learning with Python'}, {title: 'The Go Programming Language'}, {title: 'Ruby on Rails'}, {title: 'iOS12 Programming Fundamentals'}, ] }
  • reducer : reducer_active_book.js
    • action : index.js (액션 생성자)
    export function selectBook(book) {
        // selectBook is an ActionCreator, it needs to return an action.
        // an object with a type of property.
        return {
            type: 'BOOK_SELECTED',
            payload: book
        }    
    }
    
    유저가 클릭하면 그 선택한 북 객체가 디테일 뷰에 나타나야 한다. 이는 전체 스테이트에 activeBook이라는 프로퍼티가 있고, 그 값이 유저 클릭에 따라 변화해야한다는 의미이다. 그러면 컨테이너가 업데이트된 값을 가져와서 렌더링할 것이다.이렇게 생성된 액션 객체는 activeBook 리듀서를 통해 액션 타입이 BOOK_SELECTED일 때, payload값 즉 북 객체 그 자체를 컴포넌트에게 전달할 것이다.의 과정을 거치게 될 것이다.
  •  
  • 전체 플로우 : 유저가 리스트 아이템 클릭(스테이트에 activeBook을 업데이트 해달라는 요청과 같음 = 'BOOK_SELECTED' 액션 객체 생성을 요청) -> 액션 생성자를 통해 타입이 BOOK_SELECTED인 액션 생성-> 액션을 디스패치 -> 스토어가 전체 스테이트와 생성된 액션 넘겨주며 루트 리듀서 호출 -> 루트 리듀서는 자식 리듀서에게 각자가 담당하고 있는 스테이트 일부와 액션을 넘겨줌 -> 각 리듀서는 변화된 상태 값 반환 -> 루트 리듀서는 반환된 값을 모아서 새로운 스테이트를 만들어 스토어에 넘겨줌 -> 새로운 스테이트는 스토어에 저장되고 스토어가 컨테이너에게 새로우 스테이트가 업데이트되었다고 알려줌 -> 컨테이너는 새로운 스테이트에 접근하여 변화된 값을 받아오고 리렌더링함.
  • 이러한 스테이트 변화를 설명하는 액션 타입을 BOOK_SELECTED라고 하였으며, payload 프로퍼티의 값은 북 객체 그 자체이다. 이 액션 객체는 selectBook이라는 액션생성자 함수를 통해 만들어진다.
  • export default function(state = null, action) { switch(action.type) { case 'BOOK_SELECTED': return action.payload; } return state; }
  • reducer : index.js앱이 커지면 스테이트 변화를 담당하는 리듀서가 여러 개가 될 것이다. comebineReducers 함수는 헬퍼 함수로, 이렇게 여러 개가 된 리듀서를 스토어에 전달할 수 있도록 하나의 리듀서로 만든다.

  • // 루트 리듀서는 스토어에 저장 import { createStore } from 'redux'; import reducers from './reducers/index.js' const store = createStore(reducers) console.log(store.getState());
  • 이 하나의 루트 리듀서는 자식 리듀서를 모두 호출하고, 그 반환된 값을 모아서 새로운 스테이트를 만든다. (각 리듀서는 자신이 담당하는 상태 변화가 없는 경우에도 호출되며 이 경우 전과 같은 상태를 리턴한다.)
  • import { combineReducers } from 'redux'; import BooksReducer from './reducer_books'; import ActiveBook from './reducer_active_book'; const rootReducer = combineReducers({ books: BooksReducer, activeBook : ActiveBook }); export default rootReducer; // This would produce the following state object { books: [{title: 'Javascript Design Patterns'}, {title: 'Effective Java'} ... ] activeBook : {title: ... } }
  • containers : book_list.js
    • 리액트를 안다면 BookList는 단순한 컴포넌트처럼 보인다는 것을 알 수 있다. 그러나 한 가지 의아한 점은 BookList내에서 this.props로 접근할 수 있는 books와 selectBook() 이다. 리액트에서는 부모 컴포넌트에게서 전달받은 것을 this.props로 접근한다. 하지만 리덕스를 사용하면 부모 컴포넌트에게서만 값을 전달받을 수 있는 불편함을 해소할 수 있다. 즉 여기서 this.props는 부모 컴포넌트에게서 받은 것이 아니라 직접 리덕스 스토어에 접근하여 스테이트를 받아오는 것이다. 이는 mapStateToProps, mapDispatchToProps, connect에 의해 가능한 것이다.
    • 즉 리액트 컴포넌트처럼 보이는 BookList에서 필요한 스테이트에 접근하기 위해서, react-redux 라이브러리의 connect함수를 사용하여 둘을 연결하는 것이다. (The connect() function connects a React component to a Redux store.)
    • 이 connect 함수에 아규먼트로 들어간 mapStateToProps, mapDispatchToProps는 함수명이 말해주듯이 각 함수에서 반환된 값은 BookList 컴포넌트 내부에서 props로 접근할 수 있도록 만들어졌다.
    •  
  • import React, { Component } from 'react'; import { connect } from 'react-redux'; import { selectBook } from '../actions/index'; import { bindActionCreators } from 'redux'; class BookList extends Component { renderlist() { return this.props.books.map((book)=> { return ( <li key={book.title} // onclick -> selectBook 액션생성함수 호출 // 반환된 액션은 모든 리듀서에게 흘러들어감 // 모든 리듀서들의 리턴값을 모아서 하나의 새로운 스테이트를 만들고 // 이를 컴포넌트들이 참조하여 리렌더링함 onClick={()=>this.props.selectBook(book)} className="list-group-item"> {book.title} </li> ) }) } render() { return ( <ul className="list-group col-sm-4"> {this.renderlist()} </ul> ) } } function mapStateToProps(state) { // whatever is returned will show up as props // inside of BookList // this function will glue react and redux return { books: state.books }; } // Anything returned from this function will end up as props // on the BookList container function mapDispatchToProps(dispatch) { // Whenever selectBook is called, the result(actions) should be passed // to all of our reducers return bindActionCreators({ selectBook : selectBook }, dispatch) } // Promote BookList from a component to a container - it needs to know // about this new dispatch method, selectBook. Make it available // as a prop. export default connect(mapStateToProps, mapDispatchToProps)(BookList);
  • mapStateToProps()
    • mapStateToProps는 connect 함수의 첫번째 아규먼트가 된다.
    • 연결된 컴포넌트가 필요로 하는 데이터를 스토어로부터 빼내오기 위해 사용된다.
    • 이는 스테이트가 변화할 때 마다 호출된다.(=스토어를 subscribe한다) 만약 subscribe하고 싶지 않으면 connect함수의 첫번째 아규먼트로 null이나 undefined를 전달한다.
    • 전체 스테이트(store.getState()와 동일)를 아규먼트로 받아서 연결된 컴포넌트가 필요로 하는 데이터 객체를 반환해야만 한다.
    • 두번째 아규먼트는 선택적이다. 스토어에서 데이터 검색을 위해 자체적으로 id 같은 데이터가 필요할 경우 두 번째 아규먼트로 정의할 수 있다.
    • 반환된 데이터 객체는 컴포넌트에서 props로 접근이 가능하다. (그래서 이름이 mapStateToProps)
      function mapStateToProps(state) {
        return {
          a: 42,
          todos: state.todos,
          filter: state.visibilityFilter
        }
      }
      
      // component will receive: props.a, props.todos, and props.filter 
      
    • // It works same either way function mapStateToProps(state, ownProps?) { ... } const mapState = (state, ownProps?) => { ... }
  • mapDispatchToProps
    • connect함수의 두번째 아규먼트.
    • 함수로 작성될 경우 최대 2개의 패러미터를 가지고 하나의 패러미터만 가질 때는 dispatch가 그것이 된다.
    • dispatch함수는 스토어의 내장 함수인데, dispatch함수 내에 액션 객체를 pass해주면 스토어가 리듀서를 호출하면서 스테이트 업데이트가 진행된다.
    • mapDispatchToProps 내에서는 액션생성자로부터 액션을 만들어 이를 dispatch 까지 해야 한다...
      // 북리스트 예제
      function mapDispatchToProps(dispatch) {
          return bindActionCreators({ selectBook : selectBook }, dispatch)
      }
      
    • // example const mapDispatchToProps = dispatch => { return { // dispatching plain actions increment: () => dispatch({type: 'INCREMENT'}), decrement: () => dispatch({type: 'DECREMENT'}), reser: () => dispatch({type: 'RESET'}) } }
    • 예제 코드에서는 함수 내에 bindActionCreators 라는 함수를 썼는데, 이 함수는 각 액션 생성자에 스토어의 dispatch를 bind하는 역할을 하여 코드를 더 간단하게 해준다.
    • 첫번째 아규먼트로는 액션생성자를 값으로 갖는 객체를 pass하고 두번째 아규먼트로는 dispatch를 준다.
    • 여기서 리턴된 객체는 BookList 내에서 this.props로 접근가능하다.
  • containers : book_detail.js
    • 디테일뷰를 보여주는 컨테이너로 BookList에서 클릭 이벤트 발생 시 업데이트된 state에서 activeBook을 가져와서(mapStateToProps), 컴포넌트 내에서 this.props.book으로 접근하여 렌더링하는 구조임을 알 수 있다.
    • 여기까지가 컨테이너, 리듀서, 액션생성자끼리의 상호작용을 보여주는 코드였고 이 코드를 실제로 pass하여 렌더링하는 App.js, Index.js 파일의 코드는 다음에 리뷰...
  • import React, { Component } from 'react'; import { connect } from 'react-redux'; class BookDetail extends Component { render() { if(!this.props.book) { return <div>Select a book to get started</div> } return ( <div className="book-detail"> <h3>Details for : </h3> <div>{this.props.book.title}</div> </div> ) } } function mapSateToProps(state) { return { book: state.activeBook } } export default connect(mapSateToProps)(BookDetail);
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함