[React] custom hook에서 useState를 사용할 때 주의할 점 (No rerender)

Ell!·2022년 3월 10일
0

react

목록 보기
25/28

구현 사항

목표 : 사이트 소개 툴팁 구현하기

dor.gg 사이트의 유저들에게 사이트 사용 방법을 설명하는 툴팁을 만드는 작업이었다.

기본적으로는 useState를 사용하되, 다음번에 유저가 들어왔을 때에는 툴팁이 뜨지 않도록 localStorage에 저장하는 방법으로 구현 중이었다.

다음은 개략적인 구현 코드이다.

// useTooltipBubble

const useTooltipBubble = () => {
  const currentUser = useContext(CurrentUserContext);
  const userIndex = currentUser?.user.id;

  // 소개 말풍선 이전의 clickable
  const [triggerIcon, setTriggerIcon] = useState(
    Object.keys(tooltips).reduce(
      (prev, curr) => ({ ...prev, [curr]: false }),
      {},
    ),
  );

  // 소개 말풍선 open
  const [bubbleOpen, setBubbleOpen] = useState(
    Object.keys(tooltips).reduce(
      (prev, curr) => ({ ...prev, [curr]: false }),
      {},
    ),
  );

  // 소개 말풍선 안 본거 담아둠.
  useEffect(() => {
    const siteIntroductionListForNewUsers = Object.keys(tooltips);
    const siteIntroductionListInStorage = localStorage.getItem(
      '1-site-introduction-list' + userIndex,
    );
    const siteIntroductionList = JSON.parse(siteIntroductionListInStorage);
    console.log('useEffect');

    if (siteIntroductionListInStorage) {
      console.log('사이트 접속한 유저');
      if (siteIntroductionList.length > 0) {
        // 전체 안 보기했을 때 대비
        // 안 본것만 true로 돌림.
        siteIntroductionList.forEach(item => {
          setTriggerIcon(prev => ({ ...prev, [item]: true }));
        });
      }
    } else {
      console.log('처음 방문');
      // 로그인한 유저만
      if (userIndex) {
        localStorage.setItem(
          '1-site-introduction-list' + userIndex,
          JSON.stringify(siteIntroductionListForNewUsers),
        );
        // 전체 true로 돌림
        siteIntroductionListForNewUsers.forEach(item => {
          setTriggerIcon(prev => ({ ...prev, [item]: true }));
        });
      }
    }
  }, []);

  // clickable 클릭해서 말풍선띄우기
  const handleOpenBubble = useCallback(target => {
    setTriggerIcon(prev => ({ ...prev, [target]: !prev[target] }));
    setBubbleOpen(prev => ({ ...prev, [target]: !prev[target] }));
  }, []);

  // 말풍선 닫기만 하기
  const handleCloseBubble = useCallback(target => {
    setTriggerIcon(prev => ({ ...prev, [target]: !prev[target] }));
    setBubbleOpen(prev => ({ ...prev, [target]: !prev[target] }));
  }, []);

  // 해당 말풍선 check (안보이게)
  const handleCheckBubble = useCallback(
    target => {
      const siteIntroductionList = JSON.parse(
        localStorage.getItem('1-site-introduction-list' + userIndex),
      );
      // 해당 trigger, bubble 꺼주기
      setTriggerIcon(prev => ({ ...prev, [target]: false }));
      setBubbleOpen(prev => ({ ...prev, [target]: false }));
      // 그것만 빼서 다시 로컬스토리지 저장
      const changedList = siteIntroductionList.filter(item => item !== target);
      localStorage.setItem(
        '1-site-introduction-list' + userIndex,
        JSON.stringify(changedList),
      );
    },
    [userIndex],
  );

  // 전체 말풍선 check
  const handleCheckAllBubble = useCallback(() => {
    // 로컬 스토리지에 저장
    localStorage.setItem(
      '1-site-introduction-list' + userIndex,
      JSON.stringify([]),
    );
    // trigger 다 꺼주고, bubble도 다 꺼줌
    setTriggerIcon(prev => ({
      ...Object.keys(prev).reduce((acc, key) => {
        return { ...acc, [key]: false };
      }, {}),
    }));
    setBubbleOpen(prev => ({
      ...Object.keys(prev).reduce((acc, key) => {
        return { ...acc, [key]: false };
      }, {}),
    }));
  }, [userIndex]);

  return {
    triggerIcon,
    bubbleOpen,
    handleOpenBubble,
    handleCloseBubble,
    handleCheckBubble,
    handleCheckAllBubble,
  };
};

export default useTooltipBubble;

bubble trigger와 bubble 이렇게 두개의 state를 통해서 조절했으며, useEffect를 통해서 로컬 스토리지에 저장된 값을 가져와서 useState에 담아주었다.

handleOpenBubblehandleCloseBubble함수를 사용해서 bubble을 열고 닫았으며,

handleCheckBubblehandleCheckkAllBubble함수를 사용해서 bubble을 닫고 다시 켜지지 않도록 로컬스토리지에 저장했다.

문제점

문제는 handleCheckAllBubble 함수를 작동하는데에서 발생했다. 분명 함수가 작동하고, 로컬 스토리지의 값도 변하는 것을 확인했는데, 다른 물음표들이 닫히지 않는 것이었다.

처음에는 로컬스토리지의 값이 다시 반영이 되어서 그런가하고 useEffect에 log를 찍어보고 했는데, 전혀 작동하지 않았다.

서칭을 해보다가 문제를 찾았는데, 이유는 custom hooks에서 각각의 state를 사용하던 것이었다.

생각해보면 당연한 문제였는데, useTooltipBubble을 각 useTooltipManager 컴포넌트에서 쓰고 있었고, 각 useTootipManager는 각 물음표가 필요한 곳에 하나씩 들어가있었다.

그렇게 되니 각 물음표에 10개의 key가 들어있는 각각의 state 가 들어가게 된 것이다.

즉, 상태 관리가 하나도 되지 않고 있던 것이었다.

위의 그림처럼 custom hook에 담아둔 useState를 통해 전역적인 상태 관리가 되리라고 생각했었으나, 실제로는 각각의 컴포넌트에 개별적인 useState가 생겨난 꼴이 되어버렸다.

해결 방법

상태 관리를 해주었다.

redux-persist를 사용할까 했으나 간단하게 context API를 사용해주었다.

// tooltips context

import React, { createContext, useState } from 'react';
import tooltips from 'lib/static/tooltips';

export const TooltipsContext = createContext(null);

export const TooltipsContextProvider = ({ children }) => {
  // 소개 말풍선 이전의 clickable
  const [triggerIcon, setTriggerIcon] = useState(
    Object.keys(tooltips).reduce(
      (prev, curr) => ({ ...prev, [curr]: false }),
      {},
    ),
  );

  // 소개 말풍선 open
  const [bubbleOpen, setBubbleOpen] = useState(
    Object.keys(tooltips).reduce(
      (prev, curr) => ({ ...prev, [curr]: false }),
      {},
    ),
  );

  return (
    <TooltipsContext.Provider
      value={{
        trigger: [triggerIcon, setTriggerIcon],
        bubble: [bubbleOpen, setBubbleOpen],
      }}
    >
      {children}
    </TooltipsContext.Provider>
  );
};

export default TooltipsContextProvider;

App에 provider를 씌어주고

맨위의 useTooltipsBibble.js에 다음 코드를 변경해주었다.

// state -> context


  const { trigger, bubble } = useContext(TooltipsContext);
  const [triggerIcon, setTriggerIcon] = trigger;
  const [bubbleOpen, setBubbleOpen] = bubble;

문제 해결!

참고

https://stackoverflow.com/questions/57130413/changes-to-state-issued-from-custom-hook-not-causing-re-render-even-though-added

profile
더 나은 서비스를 고민하는 프론트엔드 개발자.

0개의 댓글