개발

아니, 왜 컴포넌트 사이즈 얻어오는 hook이 이상해진거지? width와 height가 둘다 0이야

배우겠습니다 2023. 11. 20. 21:58

두괄식으로 결론

hooks는 죄가없다. 사람이 죄가 있다.

 

문제상황

1. component Watch는 props로 size:string을 받는다.

2. Watch는 랜더링시작시 size props에 따라 자체적으로 width와 height를 조절한다.

3. Watch자식컴포넌트에는 clip-path를 이용해 만든 다각형이 있다. 그 다각형은 컴포넌트의 width,height을 기준으로 모양을 형성한다.

4. 다각형이 이상하게 나온다!

 

hook코드

const useComponentSize = (): [React.RefObject<HTMLDivElement>, ComponentSize] => {
  const [size, setSize] = useState<ComponentSize>({ width: 0, height: 0 });
  const componentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleResize = () => {
      const { width, height } = componentRef.current?.getBoundingClientRect() ?? { width: 0, height: 0 };
      setSize({ width, height });
    };

    handleResize();

    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return [componentRef, size];
}

 

size state의 초기값은 너비,높이 둘다 0이다.

다각형이 이상하게 나온 이유는 width와 height로 0이 들어갔기 떄문이다.

 

문제 코드

useEffect(() => {
    if (size === "tiny") {
      setDefaultSize({ fontSize: "2rem", width: "8rem", height: "4rem" });
    } else if (size === "small") {
      setDefaultSize({ fontSize: "3rem", width: "12rem", height: "6rem" });
    } else if (size === "medium") {
      setDefaultSize({ fontSize: "5rem", width: "20rem", height: "10rem" });
    } else if (size === "big") {
      setDefaultSize({ fontSize: "8rem", width: "32rem", height: "16rem" });
    } else if (size === "huge") {
      setDefaultSize({ fontSize: "10rem", width: "40rem", height: "20rem" });
    }
    const minute = time.hour * 60 + time.minute;
    setPolygon(getPolygon(minute, componentSize.width, componentSize.height));
    setIsDay(minute >= 720);
  }, [time, componentSize, size]);

 

hooks는 문제없다.

useState가 꼬였다...

useState는 비동기적이다.

리액트는 업데이트들을 별도의 큐에 넣는다. 그리고 다음 랜더링 사이클 때 큐의 업데이트들을 처리한다. 상태 업데이트는 즉시 갱신되는 것이 아니다.= 배치 처리

이를 통해 불필요한 랜더링을 줄여 성능을 향상시킨다.

 

useComponentSize는 width와 height를 받는다.

useDefaultSIze는 width와 height를 변화시킨다.

그래서 꼬여버렸다 이런!

 

여러 해결법이 있다.

1. 필자는 size props를 주지않고도 사용자가 코드를 사용시 width와 height를 자유롭게 설정할 수 있도록 하였다. 

size props를 optional로 주지 못하게 한다.

 

2. hooks의 useEffect에 size를 의존배열로 넣으면 안돼?

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

이 메시지가 뜬다.(제대로 동작해보이긴 하지만 혹사당하고 있다.)

왜냐하면 무한루프가 되기 떄문이다.

상태가 갱신되면 useEffect가 실행된다. useEffect가 되면 상태가 갱신된다.

 

이것 역시도 해결방법이 있다. setState(prev=>func(prev))형식으로 이전상태값을 사용해 상태값이 변하지 않으면 랜더링을 막는다. 근데 이것도 어려워보인다.

 

3.코드가 조금 더러워지면 해결된다.

조건부 랜더링을 사용하자.

Watch.tsx

const Watch = ({
  containerStyle,
  dayStyle,
  nightStyle,
  time,
  size,
  timeFormat = formatHHMM,
  timeStyle,
}: proptype) => {
  const [isRender, setIsRender] = useState<boolean>(false);

  const [defaultSize, setDefaultSize] = useState<defaultSizeType>({});

  useEffect(() => {
    if (size === "tiny") {
      setDefaultSize({ fontSize: "2rem", width: "8rem", height: "4rem" });
    } else if (size === "small") {
      setDefaultSize({ fontSize: "3rem", width: "12rem", height: "6rem" });
    } else if (size === "medium") {
      setDefaultSize({ fontSize: "5rem", width: "20rem", height: "10rem" });
    } else if (size === "big") {
      setDefaultSize({ fontSize: "8rem", width: "32rem", height: "16rem" });
    } else if (size === "huge") {
      setDefaultSize({ fontSize: "10rem", width: "40rem", height: "20rem" });
    }
  }, [size]);

  return (
    <WatchContainer
      style={{ ...defaultSize, ...containerStyle }}
      setIsRender={setIsRender}
    >
      {isRender ? (
        <WatchChildren
          dayStyle={dayStyle}
          nightStyle={nightStyle}
          time={time}
          timeFormat={timeFormat}
          timeStyle={timeStyle}
        />
      ) : (
        <></>
      )}
    </WatchContainer>
  );
};

export default Watch;

 

WatchContainer.tsx

const WatchContainer = ({ children, style,setIsRender }: proptype) => {

  useEffect(() => {
    setIsRender(true);
  }, []);

  return (
    <div style={style} className="watch-container">
      {children}
    </div>
  );
};

export default WatchContainer;

 

process

1. Watch에서 size props에 해당하는 width와 height를 정해준다.

2. WatchContainer는 Watch에서 정해준 width와 height를 바탕으로 자신을 랜더링한다.

랜더링되면 WatchContainer내부의 useEffect가 작동한다. 이것은 isRender을 true로 바꿔준다.

3. isRender가 true이므로 조건부랜더링에 의해 자식컴포넌트(다각형)을 랜더링한다. 사이즈가 정해진 다음 랜더링되므로 제대로 동작한다.