본문 바로가기
Front End/State management

[Redux] 리덕스의 흐름을 이해하기 위한 여정

by 옐 FE 2021. 12. 20.

바닐라 자바스크립트의 코드를 보고서 '어떤 내용인지 알겠다'정도의 감이 잡혔을 때 자바스크립트를 이용한 라이브러리 중 하나인 리액트를 배우기 시작했다. 유데미에 있는 리액트 강의 중 하나인 Modern React with Redux 라는 강의를 통해 리액트와 리덕스를 같이 공부하면 되겠다 생각했는데 리액트는 어찌어찌 내용의 흐름을 따라갈 수 있는 반면, 리액트에서 리덕스를 사용하는 강의로 넘어가는데 전혀 이해를 할 수 없었다. 리덕스의 진입장벽이 높다고 들었는데 이런 의미였던건가.

 

강사분도 리덕스에 대해 설명을 하면서 처음에는 이해하기 어려워도 쓰다보면 그 난이도가 그렇게 어렵게 느껴지지 않는다라고 언급했다. 상태관리를 양방향이 아닌 단방향으로 흐르게 하는 Flux를 바탕으로 설계된 것이 Redux라고 했는데, 그 단방향으로 흐르게끔 쓰는 코드가 왜 저렇게 쓰는거지? 라고 머릿속에 의문만 가득.

 

손으로 쓰면서 머릿속에 집어 넣으려고 하는 타입이라 이해가 너무 안돼서 필기를 하며 강의를 들었는데 비슷한 내용을 필기를 하며 세 번째 필기를 할 때쯤 조금씩 그 구조가 잡히기 시작했다. 그렇게 8월부터 시작한 강의를 3번 반복한 12월인 지금, 리덕스를 어떻게 쓰는지 그 흐름은 조금 이해할 수 있었다. 처음에는 코드를 따라 쓰면서도 전체적인 구조가 머릿속에 안 잡히니까 왜 코드를 이렇게 쓰는지 강의 내용을 따라갈 수 없었다. 그래서 다른 강의도 들어보고, 유뷰트에서 리덕스에 관한 설명도 찾아보고 하니까 이해가 되기 시작했다.

 

 

 

[메인 강의 외에 리덕스의 흐름을 이해하고자 찾아봤던 것 중에서 큰 도움이 되었던 것]

 

초보자를 위한 리덕스 101 – 노마드 코더 Nomad Coders

Vanilla JS, Redux, React

nomadcoders.co

 

 


 

🗃 제일 이해가 안되었던 건 리액트와 리덕스를 이어주기 위해 사용하는 connect

import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { fetchStream } from 'actions';

const StreamShow = ({ fetchStream, stream }) => {
  const { id } = useParams();
  const { title, description } = stream[id];

  useEffect(() => {
    fetchStream(id);
  }, [fetchStream, id]);

  return (
    <>
      <h2>{title}</h2>
      <p>{description}</p>
    </>
  );
};

const mapStateToProps = state => {
  return { stream: state.streams };
};

export default connect(mapStateToProps, { fetchStream })(StreamShow);

이번 3번째로 같은 강의를 들으면서 썼던 코드 중 StreamShow 컴포넌트의 코드. 영어로 된 강의를 들어서 이해를 잘못했던 것일수도 있지만, 왜 첫번째 인자에서 state를 가져오기 위한 mapStateToProps와 만든 action creator를 넣는지 알 수 없었다. 이제와서 코드를 다시 보자면 mapStateToProps리덕스에 store에 있는 state를 현 컴포넌트에서 사용하기 위해 쓰는 것이고, action creator 자리에는 dispatch, 즉 업데이트된 state를 전달하기 위한 함수가 들어가는 것.

 

export default connect(스토어에서 가져오는 state, state를 업데이트 시키기위한 함수)(현 컴포넌트)

// 스토어에서 가져오는 state = mapStateToProps(state, ownProps)
// state를 업데이트 시키기위한 함수 = mapDispatchToProps(dispatch, ownProps)

 

state의 상태를 변화시키기 위한 action creator를 import 하면서 직접적으로 현재의 컴포넌트에서 쓰지 않고 connect를 이용해서 현재의 컴포넌트에 props로 전달하는 이유는 connect를 통해서 받은 action creator를 써야 리덕스 스토어에 변화된 상태를 알려줄 수 있기 때문이다. 

 

 

 

🗃 Reducer에서 새롭게 생성된 state로만 업데이트 할 수 있음. 즉, action creator를 통해 상태를 변화시키면 state 그 자체를 변화시키는 게 아니라(not mutate) 이를 대체하는(replace) state를 리턴해야 한다.

// autoReducer.js
import { SIGN_IN, SIGN_OUT } from 'actions/types';
.
.
export const authReducer = (state = INIT_STATE, action) => {
  switch (action.type) {
    case SIGN_IN:
      return { ...state, isSignedIn: true, userId: action.payload };
    case SIGN_OUT:
      return { ...state, isSignedIn: false, userId: null };
    default:
      return state;
  }
};

switch문을 써서 action.type이 해당 케이스일 때 새로운 state(배열 혹은 객체)를 대체해서 리턴한다. 이 경우에는 새로운 객체 { }를 만들고 여기에 원래 있던 state를 spread로 늘여놓고 업데이트될 state의 값을 넣는다.

 

let hasChanged = false
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const actionType = action && action.type
        throw new Error(
          `When called with an action of type ${
            actionType ? `"${String(actionType)}"` : '(unknown type)'
          }, the slice reducer for key "${key}" returned undefined. ` +
            `To ignore an action, you must explicitly return the previous state. ` +
            `If you want this reducer to hold no value, you can return null instead of undefined.`
        )
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }

코드 출처 : https://github.com/reduxjs/redux

 

강의를 들을 때 강사분이 설명을 해주면서 보여주었던 Github에 있는 redux의 코드. 전체적인 코드를 이해할 필요는 없고 왜 reducer에서 대체할 수 있는 새로운 state를 리턴해야하는지에 대한 걸 알 수 있다.

 

.
.
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }

전에 가지고 있던 state의 키와 업데이트할 state의 키가 달라야 hasChanged = true 가 돼서 업데이트된 nextState를 리덕스 스토어에 전달하기 때문이다.

 

 


 

 

여전히 리덕스를 활용하는 방법을 다 아는 것도 아니지만 이 흐름을 이해하고서 코드를 썼다는 것만으로도 뿌듯하다. 왜냐하면 리덕스를 이해할 수 없어서 강의를 듣는 게 힘들고 진도가 너무 안나갔기 때문에. 그래서 redux보다 조금 더 직관적인 mobX를 사용해야할까, 아니면 리액트 개발팀에서 새롭게 만든 recoil을 사용해야할까 했었다. 그런데 상태관리툴이라는게 한 번은 어떤 식으로 사용되는 지 그 흐름을 이해해야 다른 것도 쉽게 접근할 수 있을거란 생각이 있어서 이런 저런 레퍼런스를 되게 많이 찾아봤다. 

 

이러한 여정 끝에 기능구현은 react, redux, react-hook-form, semantic ui를 이용하고 데이터를 받는 서버는 json server를 이용, heroku를 통해 배포해서 겉모습을 갖춘 Streamyy

배포했다가 현재(2022.5.6)는 내린 상태

 

동영상을 만들고 직접 스트리밍을 할 수 있게 코드를 더 써야하겠지만 이렇게 리덕스를 이용해서 CRUD 기능 구현을 한 것만으로도 왜 이렇게 뿌듯한지... 리덕스가 진짜 너무너무 이해가 안되다가 어느 순간 조금씩 이해가 되기 시작하면서 그 이해한 흐름을 바탕으로 코드를 짠 거라 더 뿌듯하다. 전에 했었던 클론 트위터를 리덕스도 쓰고 스타일 컴포넌트도 써서 조금 더 발전시켜야지.

'Front End > State management' 카테고리의 다른 글

[Redux] todoMVC에 Redux 적용하기  (0) 2022.02.14

댓글