본문 바로가기

온라인 강의(유데미, 인프런 등)/React 완벽 가이드(유데미)

[react] redux를 사용하기 싫을 때 전역 데이터 관리 방법 - react custom hook으로 바꾸기(feat. context API)

반응형

Redux를 사용하고 싶지 않을 때?!

  1. 리액트만 존재하는 세상에서 살고 싶을 때
  2. 부가 라이브러리를 설치하기 싫을 때
  3. 리덕스를 사용하지 않고 전역 state를 관리하고 싶을 때

 

해결 방안 1 ) context API

장점: 사용하기 간단한 편. 리덕스를 몰라도 됨.

단점: 고빈도 업데이트에는 효율적이지 않음(테마나 인증에는 적합). context에 변경 사항이 있을 시 useContext를 사용하는 모든 컴포넌트가 다시 빌드되고 리렌더링 됨(직접적으로 영향받았는지 상관 안 하고) -> 자주 바뀌는 state의 관리에 최적화되어있지 않음.

https://bbeeyaks-moment.tistory.com/entry/react-context-API-useContext

 

[react] context API

context API란? props drilling을 사용하기 싫을 때, 즉 컴포넌트에 상관없이 전역적으로 데이터를 이용하고 싶을 때 사용한다. context API 사용 시 전역 데이터를 Context에 저장한 후, 데이터가 필요한 컴

bbeeyaks-moment.tistory.com

 

해결 방안 2 ) custom hook store

리덕스 같은 custom store를 만든다. 

1. hooks-store 폴더 만든 후 store.js 파일 생성

import { useState, useEffect } from 'react';

// 전역적으로 선언되어 커스텀 훅을 사용하는 파일이 모두 같은 데이터 사용할 수 있도록 함
// -> 로직 + 데이터를 공유
let globalState = {};// 전역적으로 관리되는 state
// state의 변경사항을 듣고 따라가는 함수 모음
// (호출 했을 때 훅을 사용하는 모든 컴포넌트 global state에 따라 리렌더링 될 수 있도록)
let listeners = []; 
let actions = {}; // 액션들 모음 객체

// 커스텀 훅을 사용하는 컴포넌트 모두 리렌더링 될 수 있도록 useState 사용
export const useStore = () => {
  // [1] => useState가 반환하는 두번째 값(setState)에만 관심이 있다는 의미
  const setState = useState(globalState)[1];

  const dispatch = (actionIdentifier, payload) => {
    const newState = actions[actionIdentifier](globalState, payload);
    // state merge
    globalState = { ...globalState, ...newState };
    // state update
    for (const listener of listeners) {
      // setState 호출
      listener(globalState);
    }
  };
 
  // 훅을 사용하는 컴포넌트가 업데이트 될 때마다 하단 로직 실행됨
  useEffect(() => {
    // 커스텀 훅을 사용하는 모든 컴포넌트가 각각의 setState를 가짐
    listeners.push(setState);

    // clean-up 함수: listeners 제거(un-mount되면 listeners를 비워줌)
    return () => {
      listeners = listeners.filter(li => li !== setState);
    };
  }, [setState]);

  return [globalState, dispatch];
};

// store 초기화 => 구체적인 store의 slice를 만들어주기 위해
// (리덕스에서 여러 개의 reducers로 했던 것 처럼) 
export const initStore = (userActions, initialState) => {
  if (initialState) {
    globalState = { ...globalState, ...initialState };
  }
  actions = { ...actions, ...userActions };
};

2. 콘크리트 store 만들기

//hooks-store/products-store.js

import { initStore } from './store';

const configureStore = () => {
  // 액션 설정
  const actions = {
    TOGGLE_FAV: (curState, productId) => {
      const prodIndex = curState.products.findIndex(p => p.id === productId);
      const newFavStatus = !curState.products[prodIndex].isFavorite;
      const updatedProducts = [...curState.products];
      updatedProducts[prodIndex] = {
        ...curState.products[prodIndex],
        isFavorite: newFavStatus
      };
      return { products: updatedProducts };
    }
  };
  
  // 이 globalState의 슬라이스에 actions와 초기 state 전달
  initStore(actions, {
    products: [
      {
        id: 'p1',
        title: 'Red Scarf',
        description: 'A pretty red scarf.',
        isFavorite: false
      },
      {
        id: 'p2',
        title: 'Blue T-Shirt',
        description: 'A pretty blue t-shirt.',
        isFavorite: false
      },
      {
        id: 'p3',
        title: 'Green Trousers',
        description: 'A pair of lightly green trousers.',
        isFavorite: false
      },
      {
        id: 'p4',
        title: 'Orange Hat',
        description: 'Street style! An orange hat.',
        isFavorite: false
      }
    ]
  });
};

export default configureStore;

3. custom store 사용하기

// index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';

import './index.css';
import App from './App';
import configureProductsStore from './hooks-store/products-store';

// store 초기화
configureProductsStore();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
// containers/Products.js

import React, { useContext } from 'react';

import ProductItem from '../components/Products/ProductItem';
import { useStore } from '../hooks-store/store';
import './Products.css';

const Products = props => {
  // global store data에 접근
  const state = useStore()[0];
  return (
    <ul className="products-list">
      {state.products.map(prod => (
        <ProductItem
          key={prod.id}
          id={prod.id}
          title={prod.title}
          description={prod.description}
          isFav={prod.isFavorite}
        />
      ))}
    </ul>
  );
};

export default Products;
// ProductItem.js

import React from 'react';

import Card from '../UI/Card';
import { useStore } from '../../hooks-store/store';
import './ProductItem.css';

const ProductItem = props => {
  const dispatch = useStore()[1];

  const toggleFavHandler = () => {
    dispatch('TOGGLE_FAV', props.id);
  };

  return (
    <Card style={{ marginBottom: '1rem' }}>
      <div className="product-item">
        <h2 className={props.isFav ? 'is-fav' : ''}>{props.title}</h2>
        <p>{props.description}</p>
        <button
          className={!props.isFav ? 'button-outline' : ''}
          onClick={toggleFavHandler}
        >
          {props.isFav ? 'Un-Favorite' : 'Favorite'}
        </button>
      </div>
    </Card>
  );
};

export default ProductItem;

4. custom hook store 최적화하기

import { useState, useEffect } from 'react';

let globalState = {};
let listeners = [];
let actions = {};

// 액션을 dispatch하려고 custom hook이 사용되어도(store의 변화에 관심이 없어도) 리렌더링되기 때문에 
// 불필요한 리렌더링 방지를 위해 변수 하나 추가하여 최적화해줌
export const useStore = (shouldListen = true) => {
  const setState = useState(globalState)[1];

  const dispatch = (actionIdentifier, payload) => {
    const newState = actions[actionIdentifier](globalState, payload);
    globalState = { ...globalState, ...newState };

    for (const listener of listeners) {
      listener(globalState);
    }
  };

  useEffect(() => {
    if (shouldListen) {
      listeners.push(setState);
    }

    return () => {
      if (shouldListen) {
        listeners = listeners.filter(li => li !== setState);
      }
    };
  }, [setState, shouldListen]);

  return [globalState, dispatch];
};

export const initStore = (userActions, initialState) => {
  if (initialState) {
    globalState = { ...globalState, ...initialState };
  }
  actions = { ...actions, ...userActions };
};
//ProductItem.js

import React from 'react';

import Card from '../UI/Card';
import { useStore } from '../../hooks-store/store';
import './ProductItem.css';

const ProductItem = React.memo(props => {
  console.log('RENDERING');
  const dispatch = useStore(false)[1];

  const toggleFavHandler = () => {
    // toggleFav(props.id);
    dispatch('TOGGLE_FAV', props.id);
  };

  return (
    <Card style={{ marginBottom: '1rem' }}>
      <div className="product-item">
        <h2 className={props.isFav ? 'is-fav' : ''}>{props.title}</h2>
        <p>{props.description}</p>
        <button
          className={!props.isFav ? 'button-outline' : ''}
          onClick={toggleFavHandler}
        >
          {props.isFav ? 'Un-Favorite' : 'Favorite'}
        </button>
      </div>
    </Card>
  );
});

export default ProductItem;

 

오늘은 리덕스를 사용하고 싶지 않을 때 전역 데이터를 관리하는 해결 방안(context API, custom hook store)에 대해 배워보았다.

custom hook store를 사용하는 방법은 useAsync처럼 redux의 store 개념을 추상화 해준다는 개념인 것 같다.. 직접 사용해 본 것이 아니라 개념만 이해되는 수준이지만..! 상태관리 라이브러리를 더 많이 사용할 것 같긴 하지만, 개념은 잘 알아두고 있어야겠다. 성능을 최적화해 준다고도 하니! :) 

반응형