카테고리 없음

React 상태 관리 라이브러리(Redux, Recoil) 소개

물통꿀꿀이 2020. 7. 5. 19:00

React에서 중요한 것 중 하나는 역시나 State(상태) 관리이다. 

https://reactjs.org/docs/state-and-lifecycle.html

 

State and Lifecycle – React

A JavaScript library for building user interfaces

reactjs.org

컴포넌트에 의해 관리되는 상태는 단일 컴포넌트에 의해 다루느냐, 그렇지 않으면 Context와 같이 부모, 자식 간의 통신으로 사용하느냐에 따라 난이도가 달라진다.

특히 부모, 자식 간에 사용하기 위해서는 React에서 제공해주는 Props, Context와 조합하면 만들 수는 있지만.. 시간 낭비이고 현 시점에 등장한 라이브러리를 사용하면 쉽게(?) 사용 할 수 있다. 

 

해당 글에서는 먼저 단일 컴포넌트에 의한 상태 관리 방법에 대해 잠시 살펴보고, Redux와 Recoil에 대해 다루어보도록 하겠다.

(Mobx는 아직 학습을 못해서... 추후...)

※ 코드는 Typescript로 제공합니다.

 

단일 컴포넌트 중, 클래스 형태로 다룰 땐,

import React from "react";

interface State {
    counter: number
}

class ClassState extends React.Component {
    state: State = {counter: 0}

    handleClick = () => {
        this.setState({
            counter: this.state.counter + 1
        })
    }


    render() {
        return (
            <div>
                <button onClick={this.handleClick}>click</button>
                {this.state.counter}
            </div>
        )
    }
}

export default ClassState;

컴포넌트에서 지원해주는 setState를 사용하면 된다.

 

또한 함수 형태로 다룰 땐,

import React, {useState} from "react";

interface State {
    counter: number
}

const HookState: React.FC = () => {
    const [state, setState] = useState<State>({counter: 0})

    const handleClick = () => {
        setState({
            counter: state.counter + 1
        })
    }

    return (
        <div>
            <button onClick={handleClick}>click</button>
            {state.counter}
        </div>
    )
}

export default HookState;

ReactHook을 사용하면, 클래스 컴포넌트보다 더 쉽게 상태 값을 관리할 수 있다.

 

여기서 잠시!!
클래스 컴포넌트와 함수 컴포넌트의 차이점을 자세히 알고 싶으면 아래 페이지를 참조

https://overreacted.io/ko/how-are-function-components-different-from-classes/

 

함수형 컴포넌트와 클래스, 어떤 차이가 존재할까?

전혀 다른 '포켓몬'이라고 할 수 있다.

overreacted.io

간단히 보면, 성능은 둘째 치고 상태 관리 부분에서 오작동이 날 수 있다는 것이다.
React는 상태가 변경될 때마다 rendering이 된다. 클래스 컴포넌트는 이벤트 작업이 보류 될 때 가장 마지막에 요청을 바탕으로 작업을 수행하지만, 함수형 컴포넌트는 처음에 들어온 요청을 바탕으로 (즉, 함수형 컴포넌트 인스턴스가 생성되었을 때) 작업을 수행한다. 그러므로 함수형 컴포넌트가 이벤트에 의한 작업을 할 때, 잘못된 값을 사용 할 일이 없다.

그런데 함수형 컴포넌트가 클래스형 컴포넌트의 모든 역할을 다 지원하지는 못하는 것 같다.
물론 대부분은 함수형 컴포넌트를 사용하겠지만.. 지원하지 못하는 부분에 대해서는 아직 클래스형 컴포넌트를 사용해야 할 것 같다.

이처럼 단일 컴포넌트에서는 상태값을 매우 쉽게 관리 할 수 있다.

 

그러면 본격적으로 Redux, Recoil에 대해 알아보자.

https://redux.js.org/

 

Redux - A predictable state container for JavaScript apps. | Redux

A predictable state container for JavaScript apps.

redux.js.org

리덕스(Redux)는 React를 접할 때, 거의 필수적으로 알야하는 라이브러리로 알려져있다. 

그런데 리덕스의 개념은 간단하다. 하지만 개념은 개념일 뿐이고.. 코드로 녹일땐 장난아니게 헷갈리고 그 덕에 코드로 이해하는데 시간이 얼추 걸린다. (아마 이것 때문에 Learning Curve가 높아보이는 것 같다.)

 

위의 상태 관리를 리덕스로 녹여보면 아래와 같다.

export default combineReducers({
    reduxState
})
const COUNTER = "REDUX/COUNTER"

export const counterActions = {
    handleClick: () => ({type: COUNTER}),
}

type AddCounterAction = ReturnType<typeof counterActions.handleClick>;
type Actions = AddCounterAction;


export type CounterState = Readonly<{
    counter: number
}>;

const initialState: CounterState = {
    counter: 0
};


export default function (state: CounterState = initialState, action: Actions): CounterState {
    const {type} = action
    switch (type) {
        case COUNTER: {
            return {
                ...state,
                counter: state.counter + 1
            }
        }
        default:
            return state
    }
}
interface ReduxState {
    reduxState: {
        counter: number
    }
}

function mapStateToProps(state: ReduxState) {
    return {
        counter: state.reduxState.counter
    }
}

function mapDispatchToProps(dispatch: Dispatch) {
    return {
        CounterActions: bindActionCreators(counterActions, dispatch)
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(ReduxState);
const store = createStore(reducers);
export default store;
import React from "react";
import {counterActions} from "./modules/reduxState"


interface CounterContainerProps {
    counter: number
    CounterActions: typeof counterActions;
}


const ReduxState = ({counter, CounterActions}: CounterContainerProps) => {
    return (
        <div>
            <button onClick={CounterActions.handleClick}>click</button>
            {counter}
        </div>
    )
}

export default ReduxState;
<Provider store={store}>
	<div>
		<ReduxStateContainer/>
	</div>
</Provider>

우리가 원하는건 Counter 값 1 올리는 것 뿐인데, 이를 위해 코드로 적용해야 할 부분이 상당히 많다.

(Action을 Dispatch에 태워 Reducer로 넘겨주는 것인데..왜...)

 

하지만 한번 작업해 놓으면 거의(?) 불변의 코드이기는 하지만 기본이 되는 코드 양으로 눈이 빠질수 있다...^^;;;

 

그럼 가장 최근에 나온 Recoil을 확인해보자.

https://recoiljs.org/

 

Recoil

A state management library for React.

recoiljs.org

아직 버전 1도 되지 않은 불안정한 라이브러리이긴 하지만 Facebook에서 공식 라이브러리를 만드는 만큼 더 발전할 것이라 기대하고 있다.

먼저 코드를 확인해보면..

interface State {
    counter: number
}

const counterState = atom<State>({
    key: 'counterState',
    default: {
        counter: 0
    },
});


const NRecoilState = () => {
    const [state, setState] = useRecoilState<State>(counterState)

    const handleClick = () => {
        setState({
                counter: state.counter + 1
            }
        )
    }

    return (
        <div>
            <button onClick={handleClick}>click</button>
            {state.counter}
        </div>
    )
}

export default NRecoilState;
<RecoilRoot>
	<RecoilState />
</RecoilRoot>

굉장히 심플하다.!!! 리덕스처럼 눈 빠지는 코드도 없이 그냥 ReactHook에 녹여버렸다.

(물론 atom으로 기본 값은 설정해줘야 하지만 리덕스에 비하면 껌...)

코드도 직관적이라서 컴포넌트 간에 atom 및 recoilstate 만 적절히 사용한다면 리덕스보다 굉장히 효과적일 것 같다.

 

이렇게 Redux와 Recoil를 간단한 코드로 확인해 보았다. 간단히 Recoil이 좋아보일 수는 있으나 Redux는 오랜시간 다져놓은 라이브러리이기 때문에 미들웨어와 같이 관련 라이브러리가 많이 존재한다. 그렇기 떄문에 누가 확실히 좋다라고는 단언하기는 어려울 것 같다. 

하지만 Simple is Best 이기에...자연스럽게 마음이 가는건 Recoil (+ FB의 공식 라이브러리로 발전하고 있기때문에.!!!)

 

판단은 여러분의 몫 입니다.. ^^