TIL30. 리액트 커스텀 hooks 만들기

imloopy·2022년 6월 2일
0

Today I Learned

목록 보기
33/56
post-custom-banner

오늘 배운 것들

  • useResize
  • useLocalStorage & useSessionStorage
  • useForm
  • useTimeout
  • useInterval
  • useDebounce

useResize

useResize는 화면의 사이즈가 변화하여 요소의 view 크기가 변경되었을 때 원하는 동작을 수행할 수 있도록 하는 hook이다. 요소의 크기가 변화함을 감지하기 위하여 web api인 ResizeObserver를 이용한다.

  • useRef를 이용하여 DOM을 참조한다.
  • 리액트에서 사이드 이펙트를 처리하는 useEffect hook에서 해당 DOM을 ResizeObserver를 통해 observer를 observe한다. observer.observe(myRefObject)
  • dom이 변경되거나 제거될 때 disconnect해야 하므로 이를 clean up function에 삽입해준다.

그동안 useIntersectionObserver만 사용해봤는데, 다양한 dom의 상태를 감지하는 여러 api가 존재함을 알았다. 이를 잘 활용한다면, 강력한 홈페이지를 만들 수 있을 것 같다.

useLocalStorage & useSessionStorage

useLocalStorage는 localStorage에서 값을 꺼내고 저장하는 역할을 하는 hook이다. 이전에 만든 hook과 동일하므로 한번 복습하는 차원으로 만들어 보았다.

  • 현재 값에 접근하기 위하여 useState로부터 반환받은 storedValue와, try…catch구문으로 처리된 setValue 함수를 반환하도록 하였다.

sessionStorage와 localStorage api 사용법이 완전히 동일하기 때문에, sessionStorage는 따로 만들어보진 않았다. 근데, useStorage 하나만 사용하고, 외부에서 모드에 따라 localStorage와 sessionStorage를 선택할 수 있도록 만드는 방법도 좋을 것 같다고 생각한다.

useForm

useForm hook은 form관 관련된 로직을 처리하기 위하여 만든 hook이다.

form을 처리하는 많은 라이브러리들이 있으나, form 데이터 처리를 위해 필요한 것들을 파악하고, 처리 로직에 대해 이해해보고자 직접 작성하였다.

form을 처리하기 위해서는 다음의 역할들을 수행할 수 있어야 한다.

  • 데이터 저장 → useState로 객체 형식의 데이터를 처리
  • 에러 정보 저장 → 마찬가지로 useState로 객체 형식의 데이터를 처리
  • 로딩중 여부 → 현재 로딩중인지 로딩중인지 아닌지, useState로 boolean 데이터를 사용
  • handleChange → form 내부 입력값에 변화가 일어날 때 데이터를 저장하기 위한 콜백함수
  • handleSubmit → form의 submit 이벤트가 감지될 때 수행할 콜백 함수
  • validate → form 내부의 입력이 정확한지에 대한 validate 함수 실행

handleSubmit 호출 시, 만약 validate 함수가 있다면 validate를 수행한다. 만약 아무 에러가 존재하지 않다면 await submit(values)를 실행하고, 그렇지 않다면 error를 저장한다.

이전에 만든 useInput에 비하여 조금 더 발전된 형태의 hook 같다. 데이터를 입력 받는 부분과 데이터가 변화할 때 처리하는 콜백 함수를 useInput으로 따로 분리하여 useInput의 사용성을 증가시키는 방법도 좋을 것 같다.

useTimeout

여기서부터 약간 뇌정지가 옴

타임아웃 로직을 실행하기 위한 hook이다. useTimeout hook을 만들기에 앞서 useTimeoutFn hook을 먼저 만들어 재사용성을 극대화 하고자 하였다. useTimeoutFn은 다음의 역할을 수행한다.

  • run: run 함수는 현재 실행 중인 timeoutId를 중지하고, 새로운 timeout을 수행하고 해당 아이디를 useRef로 받은 ref 객체에 저장한다.
  • clear: 현재 실행 중인 timeoutId를 중지한다.

주의해야 할 점은, 컴포넌트의 재생성 및 컴포넌트가 파괴가 될 때를 생각하여 useEffect로 clear를 호출해주어야 원하지 않는 sideEffect가 발생하지 않는다.

useTimeFn을 직접 사용할 경우, 사용자가 원하는 시점에 setTimeout을 실행할 수 있으며, useTimeoutFn으로부터 만든 useTimeout hook을 사용할 경우, 해당 컴포넌트가 화면에 표시되면 바로 setTimeout 타이머가 실행된다.

사실 setTimeout을 바로 실행하는 방식으로 로직을 작성하는 법은 알았는데, 이것을 분리하여 재사용성을 극대화 하는 방법은 굉장히 참신하다고 생각했다. (사실 조금만 더 고민하면 생각해낼 수 있을 텐데 이런 부분이 조금 부족한듯 하다)

useInterval

useInterval 역시 useTimeout과 만드는 방법이 비슷했다. 마찬가지로 useIntervalFn을 먼저 만든 뒤, useInterval은 useInterval로부터 전달받은 함수들을 이용하도록 하였다.

useDebounce

에러 고치는데 거의 한시간 썼음

useTimeoutFn으로부터 useDebounce 함수를 만들 수 있다고 생각해서 useTimeout을 바탕으로 useDebounce 함수를 작성했는데, 생각대로 동작하지 않았다.

// 기존 코드
const useDebounce = (cb, deps, ms) => {
	const [run, clear] = useTimeoutFn(cb, ms)
	useEffect(run, [run, ...deps])
}

이 방식으로 코드를 작성하였고, 테스트 할 컴포넌트는 다음과 같이 작성했다.

const Test = () => {
	// debounce
	//...
	return (
		<div>
			{debounceResult}
			<input type="text" onChange={(e) => {
					setValue(e.target.value)
				}
			} 
			/>
		</div>
	)
}

deps에 value를 참조하도록 하여, value가 갱신될 때마다 debounce가 실행되도록 하였다. 이 방식으로 작성했을 때, debounce 로직 자체는 잘 실행되나(console.log를 찍어보면 세팅한 시간이 지난 다음에 찍히는 것을 확인할 수 있었다.) value의 값이 갱신되지 않는 문제가 있었다.

  1. 첫 번째 이유로는 deps를 복사하기 때문에 이 때 deps의 참조 주소가 변경되어 원하는 대로 변경이 일어나지 않는다고 생각 → deps를 외부에서 받는 방식을 포기하고 debouceValue를 받는 것으로 선회하였음 (복사 하지 않으면 될 거 같기도 하지만 일단 deps를 외부에서 받는 방식을 포기하고 다른 방법으로 해야겠다고 생각)
  2. 이 방법도 제대로 동작하지 않아 useTimeoutFn hook에 문제가 있는지 확인하였다. 확인해보니 콜백 함수 최적화를 위하여 useRef로 콜백 값을 할당해 준 것이 문제였다. 콜백 함수의 변경이 일어날 때 디바운스 로직을 실행해야 하지만, useRef로 저장한 값은 해당 값이 변경되어도 useEffect가 재호출되지 않기 때문에 제대로된 debounce 로직을 실행할 수 없었다. 따라서 useRef에 함수를 할당해 준 부분을 없애고, dependency에 콜백 함수를 넣어주었더니 제대로 동작하였다.

솔직히 이 방식도 썩 맘에 드는 것은 아니지만 일단 동작은 하기 때문에 이 방식을 선택하기로 했다. 나중에 고칠 수 있으면 한번 더 고쳐야겠다. (역시 강의랑 다른 방법으로 하면 이런 문제가 생길 수 있구나...)

// 고친 코드
const useDebounce = <T>(value: T, ms = 0) => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  const [run] = useTimeoutFn(() => setDebouncedValue(value), ms);
  useEffect(run, [run]);
  return debouncedValue;
};

느낀 점

  • 최적화는 잘 모르고 하면 오히려 독이 될 수 있음을 알았다.
    • useRef를 이용하면 값이 변경되어 재호출이 발생하더라도 값이 유지된다고 들었는데, 해당 경우에서는 오히려 재호출이 되어야 함에도 불구하고 재호출이 일어나지 않아 문제점을 해결하는데 많은 시간을 썼다…
  • 요즘 뭔가 즐거운 일이 없다. 리액트 과제 들어가기도 전에 벌써 지친 것같이 힘들다. 다른 팀원분들께 많이 죄송하다. 팀의 분위기를 해치지 않도록 더욱 주의해야겠다.

출처

is it safe to use ref.current as useeffects depedency?

post-custom-banner

0개의 댓글