ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redux | 리액트 상탯값 리덕스로 관리하기
    JavaScript/Redux 2020. 5. 15. 09:40

    리액트 상탯값 리덕스로 관리하기

    리덕스는 리액트뿐만 아니라 자바스크립트를 사용하는 모든 곳에서 사용 가능하지만 리액트와 궁합이 잘 맞음

    • 리액트 컴포넌트의 상탯값과 마찬가지로 리덕스의 상탯값도 불변 객체
    • 상탯값이 불변 객체이면 값의 변경 여부를 빠르게 확인 가능
      → 리액트 성능 향상의 한 가지 요인
    • 리액트에서 리덕스를 사용할 때는 react-redux 패키지가 많이 사용됨

    react-redux 패키지 없이 직접 구현하기

    앞에서 작성한 친구 목록/타임라인 코드를 기반으로 작성

    스토어 분리하기

    스토어 객체를 원하는 곳에서 가져다 쓸 수 있도록 별도의 파일로 분리
    common 폴더 밑에 store.js 파일 생성 후 아래 코드 입력

    import { createStore, combineReducers } from "redux";
    import timelineReducer from "../timeline/state";
    import friendReducer from "../friend/state";
    
    const reducer = combineReducers({
      timeline: timelineReducer,
      friend: friendReducer,
    });
    
    const store = createStore(reducer);
    export default store;
    • timeline 리듀서와 friend 리듀서를 합쳐서 스토어를 만든 뒤 외부에서 사용할 수 있도록 내보냄

    타임라인 프레젠테이션 컴포넌트 작성

    프레젠테이션 컴포넌트UI 효과를 위한 상탯값을 제외하고는 상탯값을 갖지 않는 컴포넌트를 말함

    timeline 폴더 밑에 component 폴더 생성 후 TimelineList.js 파일 작성

    import React from "react";
    
    function TimelineList({ timelines }) {
      return (
        <ul>
          {timelines.map((timeline) => (
            <li key={timeline.id}>{timeline.desc}</li>
          ))}
        </ul>
      );
    }
    
    export default TimelineList;
    • 타임라인 배열을 받아서 화면에 그리는 프레젠테이션 컴포넌트

    타임라인 컨테이너 컴포넌트 작성

    상탯값과 비즈니스 로직을 갖는 컴포넌트컨테이너 컴포넌트라고 함

    timeline 폴더 밑에 container 폴더 생성 후 TimelineMain.js 파일 작성

    import React, { Component } from "react";
    import store from "../../common/store";
    import { getNextTimeline } from "../../common/mockData";
    import { addTimeline } from "../state";
    import TimelineList from "../component/TimelineList";
    
    class TimelineMain extends Component {
      componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.forceUpdate());
      }
    
      componentWillUnmout() {
        this.unsubscribe();
      }
    
      onAdd = () => {
        const timeline = getNextTimeline();
        store.dispatch(addTimeline(timeline));
      };
    
      render() {
        console.log("TimelineMain render");
        const timelines = store.getState().timeline.timelines;
        return (
          <div>
            <button onClick={this.onAdd}>타임라인 추가</button>
            <TimelineList timelines={timelines} />
          </div>
        );
      }
    }
    
    export default TimelineMain;
    • 스토어 객체를 가져옴
    • getNextTimeline 함수를 이용해서 필요할 때마다 타임라인 데이터를 가져오도록 할 예정
    • timeline/state.js 에서 정의한 데이터 추가를 위한 액션 생성자 함수를 가져옴
    • 액션이 처리될 때마다 화면을 다시 그리기 위해 subscribe 메서드 사용
    • 컴포넌트 인스턴스의 forceUpdate 메서드를 호출하면 해당 컴포넌트를 무조건 렌더링함
    • 컴포넌트가 언마운트될 때 subscribe 메서드에 등록한 이벤트 처리 함수를 해제
    • 타임라인 추가 버튼을 누르면 타임라인을 추가하는 액션 발생

    친구 목록 프레젠테이션 컴포넌트 작성

    friend 폴더 밑에 component 폴더 생성 후 FriendList.js 파일 작성

    import React from "react";
    
    export default function FriendList({ friends }) {
      return (
        <ul>
          {friends.map((friend) => (
            <li key={friend.id}>{friend.name}</li>
          ))}
        </ul>
      );
    }
    • 친구 목록 배열을 받아서 화면에 그리는 프레젠테이션 컴포넌트

    친구 목록 컨테이너 컴포넌트 작성

    container 폴더 생성 후 FriendMain.js 파일 작성

    import React, { Component } from "react";
    import store from "../../common/store";
    import { getNextFriend } from "../../common/mockData";
    import { addFriend } from "../state";
    import FriendList from "../component/FriendList";
    
    export default class FriendMain extends Component {
      componentDidMount() {
        this.unsubscribe = store.subscribe(() => this.forceUpdate());
      }
    
      componentWillUnmount() {
        this.unsubscribe();
      }
    
      onAdd = () => {
        const friend = getNextFriend();
        store.dispatch(addFriend(friend));
      };
    
      render() {
        console.log("FriendMain render");
        const friends = store.getState().friend.friends;
        return (
          <div>
            <button onClick={this.onAdd}>친구 추가</button>
            <FriendList friends={friends} />
          </div>
        );
      }
    }
    • 타임라인 컨테이너와 동일한 구조로 작성

    더미 데이터 생성

    서버 역할을 대체할 더미 데이터 파일과 데이터를 가져오는 함수 작성

    common 폴더 밑에 mockData.js 파일 생성 후 getNextFriend, getNextTimeline 함수 구현

    const friends = [
      { name: "쯔위", age: 15 },
      { name: "수지", age: 20 },
      { name: "아이유", age: 25 },
      { name: "손나은", age: 30 },
    ];
    
    const timelines = [
      { desc: "점심이 맛있었다", likes: 0 },
      { desc: "나는 멋지다", likes: 10 },
      { desc: "호텔에 놀러 갔다", likes: 20 },
      { desc: "비싼 핸드폰을 샀다", likes: 30 },
    ];
    
    function makeDataGenerator(items) {
      let itemIndex = 0;
      return function getNextData() {
        const item = items[itemIndex % items.length];
        itemIndex += 1;
        return { ...item, id: itemIndex };
      };
    }
    
    export const getNextFriend = makeDataGenerator(friends);
    export const getNextTimeline = makeDataGenerator(timelines);
    • 친구 목록과 타임라인 데이터를 생성할 때 사용할 기본 데이터 작성
    • 친구 목록과 타임라인 데이터를 생성하는 로직이 같기 때문에 makeDataGenerator 함수 하나로 작성
    • getNextData 함수는 items, itemIndex 변수를 기억하는 클로저이며 중복되지 않는 id 값을 넣어서 반환

    index.js 수정

    지금까지 작성한 컴포넌트를 렌더링하기 위해 src/index.js 파일 수정

    import React from "react";
    import ReactDOM from "react-dom";
    import TimelineMain from "./timeline/container/TimelineMain";
    import FriendMain from "./friend/container/FriendMain";
    
    ReactDOM.render(
      <div>
        <FriendMain />
        <TimelineMain />
      </div>,
      document.getElementById("root")
    );
    • npm start 명령어로 프로젝트 실행 후 렌더링된 버튼을 클릭하면 데이터가 리덕스의 상탯값에 추가됨
    • 화면은 정상적으로 렌더링되지만 타임라인 추가 버튼을 누를 때도 FriendMain 컴포넌트의 render 메서드가 호출됨
    • 각각의 render 메서드는 그에 맞는 데이터가 변경될 때만 호출되도록 하는 것이 좋음

    FriendMain 개선하기

    불필요하게 render 메서드가 호출되지 않도록 friend/container/FriendMain.js 코드 개선

    // ...
    class FriendMain extends PureComponent { // PureComponent 상속
      // state 정의
      state = {
        // 리덕스 상탯값으로부터 초기 상탯값을 가져옴
        friends: store.getState().friend.friends,
      };
    
      componentDidMount() {
        this.unsubscribe = store.subscribe(() =>
          // setState 메서드 사용
          this.setState({ friends: store.getState().friend.friends })
        );
      }
    // ...
    • 상탯값이 변경되는 경우에만 render 메서드가 호출되도록 PureComponent 상속
    • state로 컴포넌트 상태값 정의
    • componentDidMount 함수 내부에서 forceUpdate 메서드를 setState 메서드로 변경
      → setState 메서드를 호출하면 shouldComponentUpdate 생명 주기 메서드가 호출되고,
      상탯값이 변경되지 않으면 render 메서드가 호출되지 않음
    • 이제 타임라인 추가 버튼을 눌러도 FriendMain 컴포넌트의 render 메서드는 호출되지 않음

    react-redux 패키지 사용하기

    위에서 작성한 코드를 기반으로 react-redux 패키지 사용


    설치

    $ npm i react-redux

    Provider 컴포넌트 사용

    Provider 컴포넌트는 react-redux에서 제공하는 컴포넌트로,
    Provider 컴포넌트 하위에 있는 컴포넌트는 리덕스의 상탯값이 변경되면 자동으로 렌더 함수가 호출되도록 할 수 있음

    index.js 파일을 다음과 같이 수정

    // ...
    import store from "./common/store";
    import { Provider } from "react-redux";
    
    ReactDOM.render(
      <Provider store={store}>
        <div>
          <FriendMain />
          <TimelineMain />
        </div>
      </Provider>,
      document.getElementById("root")
    );
    
    • 스토어 객체를 Provider 컴포넌트의 속성값으로 전달
    • Provider 컴포넌트는 전달받은 스토어 객체의 subscribe 메서드를 호출해서 액션 처리가 끝날 때마다 알림을 받음
    • 그 다음 컨텍스트 API를 사용해서 리덕스의 상탯값을 하위 컴포넌트로 전달

    FriendMain 컴포넌트 리팩터링

    FriendMain 컴포넌트가 react-redux를 사용하도록 FreindMain.js 파일을 다음과 같이 수정

    // ...
    import { connect } from "react-redux";
    
    class FriendMain extends Component {
      onAdd = () => {
        const friend = getNextFriend();
    
        // mapDispatchToProps 함수로부터 전달받은
        // addFriend 함수를 호출해서 리덕스의 상탯값 변경
        this.props.addFriend(friend);
      };
    
      render() {
        console.log("FriendMain render");
    
        // mapStateToProps 함수로 전달받은 friends 데이터 사용
        const { friends } = this.props;
        return (
          <div>
            <button onClick={this.onAdd}>친구 추가</button>
            <FriendList friends={friends} />
          </div>
        );
      }
    }
    
    const mapStateToProps = (state) => {
      return { friends: state.friend.friends };
    };
    
    const mapDispatchToProps = (dispatch) => {
      return {
        addFriend: (friend) => {
          dispatch(addFriend(friend));
        },
      };
    };
    
    export default connect(mapStateToProps, mapDispatchToProps)(FriendMain);
    • connect: 컴포넌트가 리덕스 상탯값 변경에 반응하기 위해서는 react-redux에서 제공하는 connect 함수를 사용해야 함
      → connect 함수 호출 시 고차 컴포넌트가 생성되고 해당 컴포넌트에 직접 만든 컴포넌트를 전달(여기서는 FriendMain)
    • mapStateToProps: 리덕스 상탯값을 기반으로 컴포넌트에서 사용할 데이터를 속성값으로 전달
    • mapDispatchToProps: 리덕스의 상탯값을 변경하는 함수를 컴포넌트의 속성값으로 전달
    • 프로젝트 실행 후 타임라인 추가 버튼을 클릭해도 FriendMain 컴포넌트의 render 메서드는 호출되지 않음
      connect 함수로 생성한 고차 컴포넌트는 입력된 컴포넌트로 전달하는 속성값에 변화가 없다면 입력된 컴포넌트를 다시 렌더링하지 않음

    mapDispatchToProps 함수 없이 액션 생성자 함수 전달

    mapDispatchToProps 함수를 단순히 액션 생성자 함수와 dispatch 메서드를 연결하는 목적으로 사용한다면 다음과 같이 간편하게 작성 가능

    import * as actions from '../state';
    // ...
    export default connect(mapStateToProps, actions)(FriendMain);
    
    // mapDispatchToProps 함수로 액션 함수를 전달하는 예
    const mapDispatchToProps = dispatch => {
        return {
            addFriend: friend => {
                dispatch(addFriend(friend));
            },
            removeFriend: friend => {
                dispatch(removeFriend(friend));
            },
            editFriend: friend => {
                dispatch(editFriend(friend));
            },
        };
    };
    • 모든 액션 생성자 함수를 actions 객체로 가져옴
    • export default 키워드를 이용해서 내보낸 리듀서 함수가 default라는 이름으로 같이 넘어오지만 큰 문제는 되지 않음
    • connect 함수의 두 번째 인자로 객체를 전달하면 그 객체를 액션 생성자 함수를 모아 놓은 객체로 인식
    • 매개변수의 개수가 많아도 잘 동작하며, 단순하게 dispatch 함수를 연결하는 경우에는 액션 생성자 함수를 모아 놓은 객체를 전달하는 것이 편함

    리액트에서 리덕스로 상탯값을 관리하는 방법에 대해 알아보았습니다.

     

    참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]

    댓글

Designed by Tistory.