[React] 상태관리와 전역상태관리 라이브러리

·2022년 11월 21일
3

React

목록 보기
11/21

📌 상태

📍 상태 란?

상태란 컴포넌트 내부에서 관리되며 어플리케이션의 렌더에 영향을 미치는 플레인 자바스크립트 객체이다. 변화하는 데이터 라고도 한다.

상태들은 일관적이여야 한다. 즉, 서로 다른 컴포넌트에서 동일한 상태를 다룬다면 그 출처가 같아야 한다.
예를 들어, 인스타 피드에 공유된 포스트들을 보여주는 컴포넌트가 있고 그 포스트들으 개수를 나타내주는 컴포넌트가 있을 경우 두 가지 컴포넌트 모두 '내가 올린 포스트'라는 데이터를 가져와 사용한다.
만약 새로운 포스트를 올렸을 때 피드에 보여지는 포스트는 11개, 그 개수룰 나타내는 컴포넌트는 10개를 나타낸다면 혼란을 가져다준다.

따라서 일관성 즉, 데이터의 무결성은 정말 중요하다.

📍 상태의 종류

📝 지역 상태 (local state)

지역 상태는 특정 컴포넌트 안에서만 관리되는 상태를 뜻한다.
다른 컴포넌트들과 데이터를 공유하지 않는다.
예를 들면, input, selectbox 등에서 사용자의 입력값을 받는 경우가 있다.

📝 컴포넌트 간 상태 (cross component state)

컴포넌트 간 상태는 여러가지 컴포넌트에서 관리되는 상태를 나타낸다.
다수의 컴포넌트에서 쓰이고, 또 영향을 미치는 상태를 뜻한다.
예를 들면, 프로젝트 곳곳에서 쓰이는 모달이 있다.

보통 상위 컴포넌트에서 하위 컴포넌트로 props를 넘겨 해당 컴포넌트까지 전달되도록 하는 props drilling 방식을 필요로 한다.

📝 전역 상태 (global state)

전역 상태는 프로젝트 전체에 영향을 끼치는 상태이다.

이 또한 props drilling 방식을 활용해서 부모에서 자식으로 데이터를 전달한다.

📌 상태관리의 필요성

서로 다른 두 컴포넌트에 같은 데이터가 필요한 경우 각 컴포넌트가 부모 자식 관계로 되어 있지 않은 이상, 각 컴포넌트 간의 직접적인 데이터 전달이 어렵다.
데이터를 부모 컴포넌트로 보내고 다시 그 데이터가 필요한 컴포넌트로 전달해야 하는데, 이러한 props drilling이 많아지면 props를 추적하기 어려워진다.
따라서 각 어플리케이션에 알맞은 상태관리 툴을 선택해 상태를 잘 관리하는 것이 중요하다.

📌 상태관리 툴

📍 Context API

📝 Context API 란?

Context API는 React 컴포넌트 트리 안에서 전역 상태를 공유할 수 있도록 만들어진 방법이다.
Context API는 종속성을 주입하기 위한 도구이기 때문에 전역 상태관리 툴이라기엔 다소 애매한 면이 있다.
Context API는 이미 존재하는 상태를 다른 컴포넌트들과 쉽게 공유할 수 있게 해주는 역할을 한다.

📝 Context API 구성

✏️ Context

전역 상태를 저장하는 곳이다.
Context 내부에 Provider와 Consumer가 정의되어 있고, Consumer는 Context를 통해 상태에 접근이 가능하다.

✏️ Provider

전역 상태를 제공하는 역할을 한다.
Context에 상태를 제공해서 다른 컴포넌트가 상태에 접근할 수 있도록 도와준다.
제공된 상태에 접근하기 위해서는 Provider 하위에 컴포넌트가 포함되어 있어야 한다.
따라서 모든 컴포넌트에 접근 가능하도록 Root component (index.js / app.js) 에서 Provider를 정의한다.

✏️ Consumer

제공받은 전역 상태를 받아서 사용하는 역할을 한다.
Context는 Consumer 사이에 있는 첫 객체를 Context에 인자로 전달하기 때문에 빈 객체 작성 후 JSX를 작성해야 한다.

📝 Context API 사용법

import {createContext} from 'react';

const MyContext = createContext();

createContext 함수를 불러와서 Context를 만든다.
기본 값을 설정하고 싶은 경우 createContext 함수 안에 인자로 기본 값을 넣어주면 된다.

function App() {
  return (
    <MyContext.Provider value="Hello World">
    	<GrandParend />
    </MyContext.Provider>
  );
}

Context 객체 안에 Provider라는 컴포넌트가 있다.
컴포넌트 간에 공유하고자 하는 값을 value라는 props로 설정하면 자식 컴포넌트들에서 해당 값에 접근할 수 있다.

import {useContext} from 'react';

function Message() {
  const value = useContext(MyContext);
  return <div>Received: {value}</div>;
}

useContext를 사용하여 Context에 넣은 값에 바로 접근할 수 있다.

📍 Redux

📝 Redux 란?

Redux는 전역 상태관리를 위한 도구로, 자바스크립트 앱을 위한 에측 가능한 상태 컨테이너이다.

  • Single source of truth
    동일한 데이터는 항상 같은 곳에서 가지고 온다. 즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미이다.
  • State is read-only
    액션이라는 객체를 통해서만 상태를 변경할 수 있다.
  • Changes are made with pure functions
    변경은 순수 함수로만 가능하다.
    Store - Action - Reducer

📝 Redux 구성

✏️ Store

Store는 상태가 관리되는 오직 하나의 공간이다.
컴포넌트와는 별개로 스토어라는 공간이 있어, 그 스토어 안에 앱에서 필요한 상태를 담는다.
컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.

✏️ Action

Action은 앱에서 스토어에 운반할 데이터를 말한다.
Action은 자바스크립트 객체 형식으로 되어 있다.

✏️ Reducer

Action을 Reducer에 전달해야 Store에 저장할 수 있다.
Reducer가 Action을 보고 Store의 상태를 업데이트 한다.
Action을 Reducer에 전달하기 위해서는 dispatch() 메소드를 사용해야 한다.

  • Action 객체가 dispatch( ) 메소드에 전달된다.
  • dispatch(액션)를 통해 Reducer를 호출한다.
  • Reducer는 새로운 store를 생성한다.

Redux는 flux패턴을 따른다.
Redux의 데이터 흐름은 동일하게 단방향으로, view(컴포넌트)에서 Dispatch(store에서 주는 state를 바꾸는 함수)라는 함수를 통해 action(dispatch 함수 이름)이 발동되고 reducer에 정의된 로직에 따라 store의 state가 변화하고 그 state를 쓰는 view(컴포넌트)가 변하는 흐름을 따른다.

📝 Redux 사용법

[ reducers / index.js ]

import {combineReducers} from 'redux';
import counter from './counter';

// 여러 reducer를 사용할 경우 reducer를 하나로 묶어주는 메소드이다.
// store에 저장되는 reducer는 오직 한 개이다.
const rootReducer = combineReducers({
  counter
});

export default rootReducer;

rootReducer를 정의한다.

[ reducers / counter.js ]

// reducer가 많아지면 action 상수가 중복될 수 있으니 action이름 앞에 파일 이름을 넣는다.
export const INCRESE = "COUNT/INCRESE";

export const increaseCount = count => ({ type : INCRESE, count});

const initalState = {
  count: 0
};

const counter = (state = initalState, action) => {
  switch (action.type) {
    case INCRESE:
      return {
        ...state,
        count: action.count
      };
    
    // default를 쓰지 않으면 맨 처음 state에 count값이 undefined가 나오므로 default문을 꼭 넣어야 한다.
    default :
      return state;
  }
};

세부 reducer를 정의한다.

[ index.js ]

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import App from './App';
import rootReducer from './reducers';

// 위에서 만든 reducer를 스토어 만들 때 넣어준다.
const store = createStore(rootReducer)

ReactDOM.render(
  // 만든 store를 앱 상위에 넣어준다.
  <Provider store = {store}>
  	<App />
  </Provider>
  document.getElementById('root'),
);

app에 store 넣고, 만든 reducer를 반영한다.

import {useSelector, useDispatch} from 'react-redux';
import {increaseCount} from 'reducers/count';

const dispatch = useDispatch();

const {count} = useSelector(state => state.counter);

const increase = () => {
  // store에 있는 state 바꾸는 함수 실행
  dispatch(increaseCount());
};

const Counter = () => {
  return (
    <div>
    	{count}
    	<button onClick={increase}>증가</button>
    </div>
  );
};

export default Counter;

store에서 useDispatch, useSelector로 state와 함수를 가져와서 컴포넌트에서 redux 사용

📍 MobX

📝 MobX 란?

MobX란 react에서 상태 관리를 위해 사용되는 라이브러리로, 객체지향적인 느낌으로 프로그래밍을 한다.
Redux에 비해 사용되는 방식이 간단하고 복잡하지 않다.

📝 MobX 구성

Action 함수가 실행되어 state 값에 변화가 발생하면 렌더링 같은 side effects들이 실행되어 그에 따른 결과들이 화면에 보이게 된다.

✏️ Actions

Observable state에 저장되어 있는 데이터들을 변환시키는 액션 함수

✏️ Observable state

관찰되고 있는 데이터 값들이 저장되어 있는 장소

✏️ Compute values

Observable state에 저장되어 있는 데이터가 변화되는 것을 알아채면 렌더링 같은 side effects trigger를 전달

✏️ Side effects

렌더링 같은 side effect가 실행되고, 실행된 side effects들은 다시 액션 함수가 실행되도록 이벤트 전달

📝 MobX 사용법

[ src / modules / numberStore.jsx ]

import { observable } from 'mobx';

const NumberStore = observable({
  // state
  num: 0,
  
  // action
  increaseAction(num) {
    this.num = this.num + num;
  },
  
  decreaseAction(num) {
    this.num = this.num - num;
  }
});

export default NumberStore;

state와 action 함수가 담겨 있는 observable 파일 생성

[ src / modules / indexStore.jsx ]

import NumberStore from './numberStore';

const indexStore = () => ({
  NumberStore,
  // store1,
  // store2
  // ...
});

export default indexStore;

사용되는 observable 파일들을 묶어주는 파일 생성

[ App.jsx ]

import * as React from 'react';
import { useObserver } from 'mobx-react';
import indexStore from './modules/indexStore';

const App = () => {
  const { NumberStore } = indexStore();
  
  const onClickIncrease = () => {
    NumberStore.increaseAction(3);
  }
  
  const onClickDecrease = () => {
    NumberStore.decreaseAction(2);
  }
  
  return useObserver(() => (
    <div>
    	<p>현재 값: {NumberStore.num}</p>
        <button onClick={onClickIncrease}>증가</button>
        <button onClick={onClickDecrease}>감소</button>
	</div>
  ))
}

export default App;

useObserver를 사용하여 관찰되고 있는 대상을 가져다 사용

📍 Recoil

📝 Recoil 이란?

Recoil은 페이스북에서 만든 새로운 React를 위한 상태 관리 라이브러리이다.
Recoil을 사용하면 atoms (공유 상태) 에서 selectors (순수 함수) 를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다.

📝 Recoil 구성

✏️ Atoms

atom은 상태의 단위로, 값이 업데이트되면 값을 구독한 컴포넌트는 다시 렌더링된다.
Atoms는 atom 함수를 사용해 생성할 수 있다.
컴포넌트에서 atom을 읽고 쓰려면 useRecoilState라는 훅을 사용해야 한다.

✏️ Selectors

Selectors는 Atoms나 다른 Selectors를 입력으로 받아들이는 순수 함수이다.
상위의 Atoms 또는 Selectors가 업데이트 되면 하위의 selector 함수도 다시 실행된다.

최소한의 상태 집합만 Atoms에 저장하고, 다른 모든 파생되는 데이터는 Selectors를 통해 계산함으로써 쓸모 없는 상태의 보존을 방지할 수 있다.

📝 Recoil 사용법

[ RecoilRoot ]

import {RecoilRoot} from 'recoil';

function AppRoot() {
  return (
    <RecoilRoot>
    	<ComponentThatUsesRecoil />
    </RecoilRoot>
    );
}

RecoilRoot는 Redux의 Provider와 비슷한 역할을 하여 여러 개의 RecoilRoot가 공존할 수 있다.
컴포넌트에서 Recoil과 연동할 때에는 해당 컴포넌트와 가장 가까이에 있는 RecoilRoot를 사용한다.

[ atom ]

const todoListState = atom({
  key: 'todoListState',
  default: [],
});

const [todoList, setTodoList] = useRecoilState(todoListState);

const todoList = useRecoilValue(todoListState);

const setTodoList = useSetRecoilState(todoListState);

atom은 recoil에서 상태를 정의하는 방법으로, 상태를 정의할 때는 고유값인 key를 설정하고, 기본값(default)을 설정하면 된다.

정의한 atom은 useRecoilValue, useSetRecoilState, useRecoilState 훅으로 사용할 수 있다.

[ 읽기 전용 단방향 흐름의 selector]

import { atom, selector } from 'recoil';

type Job = { content: string; isDone: boolean };

const jobListState = atom<Job[]>({
  key: 'jobListState',
  default: [],
});

const completedJobsSelector = selector({
  key: 'completedJobsSelector',
  get: ({ get }) => {
    const jobList = get(jobListState);
    const completed = jobList.filter((v) => v.isDone);
    return completed;
  },
});

작업 목록에서 완료된 작업만 추려내고 싶을 때 사용

[ 편집이 가능한 양방향 흐름의 selector]

const mphState = atom({
  key: 'mphState',
  default: 0,
});

export const kphState = selector<number>({
  key: 'kphState',
  get: ({ get }) => {
    const mph = get(mphState);
    return mph * 1.609;
  },
  set: ({ set }, newValue) => {
    if (typeof newValue === 'number') {
      set(mphState, newValue / 1.609); // 킬로미터를 마일로 변환한다
    }
  },
});

const KPHInput: React.FC<Props> = () => {
  const setKph = useSetRecoilState(kphState);
  return (
    <input type="number" onChange={(e) => setKph(parseInt(e.target.value))} />
  );
};

속도 정보를 관리해야 할 때, 원본 데이터는 마일(m/h)인데 화면 상에는 킬로미터(km/h)로 표시해야 하는 경우가 있다.
selector에 set 속성을 추가하면 쓰기가 가능해진다.
컴포넌트에서 useSetRecoilState를 사용하여 kphState의 setter함수를 만들면 k/h 값을 넘겨 mphState 상태를 업데이트할 수 있다.

<참고 : https://mingule.tistory.com/74
https://velog.io/@velopert/react-context-tutorial
https://hanamon.kr/redux%EB%9E%80-%EB%A6%AC%EB%8D%95%EC%8A%A4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC/
https://kyounghwan01.github.io/blog/React/redux/redux-basic/#%E1%84%87%E1%85%A9%E1%84%8B%E1%85%AA%E1%86%AB%E1%84%8C%E1%85%A5%E1%86%B7
https://jforj.tistory.com/154
https://velog.io/@brb1111/Recoil-%EC%9D%B4%EB%9E%80
https://blog.rhostem.com/posts/2021-11-24-recoil-writable-selector >

profile
개발을 개발새발 열심히➰🐶

0개의 댓글