똑같은 삽질은 2번 하지 말자
React vol.18 (트러블 슈팅중 배웠던 개념들) 본문
개요
리액트로 프로젝트를 진행하게 되면서 이걸 알았으면 삽질을 덜 할수 있었을 듯한 개념에 대해 정리한다.
1. 의존성 배열의 중요성
useCallback 이나 useEffect의 의존성 배열에 상태를 포함시키지 않으면 해당 훅 내의 코드는 상태의 최신 값을 볼 수 없다. 대신에 상태의 값은 훅이 처음 호출될 때의 값으로 고정 된다. 즉 useCallback과 useEffect는 의존성 배열에 명시된 값들이 변경될 때만 새로운 콜백을 생성하거나 코드 블록을 다시 실행한다.
클로저의 개념과 관련이 있는데 클로저는 함수와 그 함수가 생성된 렉시컬 환경의 조합이다. useCallback이나 useEffect의 코드 블록은 그들이 생성될 때의 렉시컬 환경을 캡처한다. 이렇게 캡처된 렉시컬 환경은 훅의 코드 블록이 실행될 때 사용되며, 의존성 배열에 명시된 값들이 변경되지 않는 한 변경되지 않는다.
2. useRef와 useState의 차이
useRef와 useState는 React에서 상태를 관리하는 데 사용되지만 서로 다른 목적과 특징을 가지고 있다. 아래에 이 두 훅의 주요 차이점과 useRef를 사용하는 이유에 대해 알아보자. (참고로 지금 해결할 문제는 마우스 이동할때 마다 어떤 특정 UI를 감춰야 하고, 마지막 마우스 이동 + 3초뒤 UI가 비표시가 되는 요건을 만족해야 하는 상황이다.)
재렌더링
useState는 상태가 변경될 때마다 컴포넌트를 재렌더링한다. 반면에 useRef는 값이 변경되더라도 컴포넌트를 재렌더링하지 않는다. 이 특징 때문에 useState를 사용하여 타이머 ID를 관리하면 마우스 이동이 감지될 때마다 컴포넌트가 재렌더링 되며, 이는 성능에 영향을 미칠 수 있으며, 예상치 않은 렌더링 동작을 유발할 수 있다.
값의 유지
useRef는 .current 프로퍼티를 통해 값을 유지하며, 이 값은 컴포넌트의 라이프 사이클 동안 유지된다. 이러한 이유로 useRef는 이전 값에 액세스하거나 변경사항을 추적하기 위해 종종 사용된다. useState는 상태를 변경하기 위해 세터 함수를 제공하며, 이전 상태 값에 액세스하려면 콜백 함수를 사용해야한다.
타이머 관리
useRef는 타이머 ID를 저장하고 추후에 clearTimeout을 호출하기 위한 안전한 방법을 제공한다. useRef의 .current 프로퍼티는 컴포넌트의 라이프 사이클 동안 일관된 값을 유지하므로, 이벤트 핸들러 내에서 타이머 ID에 안전하게 액세스할 수 있다. useState를 사용하면 상태 변경과 관련된 복잡성 때문에 예기치 않은 동작이 발생할 수 있으며, 이는 특히 비동기 작업(예: setTimeout 및 clearTimeout)과 관련하여 문제를 유발할 수 있다. 이러한 이유로 useRef를 사용하여 타이머 ID를 관리하는 것이 더 안정적이고 예측 가능한 동작을 제공할 수 있는 것이다.
나는 이때 timerId를 useState로 관리함으로써 마우스를 계속해서 움직이면 사라져야 할 컨트롤러가 사라지지 않고 예측할 수 없는 타이밍에 표시되고 비표시되는 등등 제어하기 힘든 상태가 되었었다. 그래서 timeoutId를 useRef로 관리하게 됨으로써 이런 현상을 없앨 수 있었다.
3. 헷갈리기 쉬운 이벤트 콜백함수 등록방법의 차이
- onClick={handleClick}
- onClick={handleClick(target)}
- onClick={() => handleClick(target)}
이건 vue나 바닐라 자바스크립트를 할 때나 자바스크립트를 사용하면 언제나 등장했던 개념이지만 다시 한번 확실하게 정리하고 넘어가고 싶었다.
onClick={handleClick} 은 클릭을 했을 때 콜백함수로써 handleClick함수의 참조 값을 넘긴 형태이다. 참조 값을 넘김으로써 onClick이벤트와 내가 넘긴 함수가 바인딩이 된다. 이걸 살짝 풀어서 쓰면 onClick={(clickEventInfo) => handleClick(clickEventInfo)}와 똑같은 형태이다.
onClick={handleClick(target)} 은 이렇게 이벤트 함수를 등록하는 코드가 실행되는 순간 함수가 실행되기 때문에 이렇게 하면 망한다. 즉, 클릭을 할 때 실행되는게 아니라 컴포넌트 함수가 실행될 때 handleClick이 실행되는 것이다.
onClick={() => handleClick(target)} 은 onClick={handleClick} 을 살짝 풀어서 쓴것과 비슷한 형태이다. 하지만 다른 점은 내가 원하는 값을 함수의 인자로 넘길 수 있다는 것이다. 즉 clickEventInfo같은 이벤트 관련정보가 아니라 내가 원하는 다른 인자를 넘기고 싶을 때 이렇게 쓰면 된다.
4. 자식 컴포넌트의 요소에 ref를 할당해서 그 ref를 부모에서 접근하기 (forwardRef)
인증번호를 입력하는 폼이 있고 그 폼은 6개의 input 박스로 구성되어 있다. 여기서 하나의 인풋폼에 숫자를 입력하면 자동으로 다음 인풋폼으로 포커스가 이동하는 움직임을 구현해야 하는 상황이다. 나는 이 문제의 해결방법 으로써 인증번호의 자릿수 만큼 ref를 할당해서 DOM 조작이 가능한 상태를 만들어서 입력 이벤트와 연결 시키는 것을 생각했다.
여기서 제일 시간이 많이 들었던 부분은 컴포넌트에 ref를 할당하는 부분이었는데 나의 BasicInput이라는 컴포넌트는 내부에는 input 태그를 가지고 있었다. 그리고 부모 컴포넌트에서는 이 자식 컴포넌트의 input 태그에 focus를 조절할 수 있게 해야하는 상태였다. 이때 forwardRef를 해야하는지도 모르고 그냥 부모컴포넌트에서 바로 자식 컴포넌트에 ref를 할당했는데 왜 포커스가 이동이 안되지? 라는 삽질을 좀 했었다. 조금 찾아보니 forwardRef를 사용하라는 가르침을 받았다..! 이럴 때 사용하는 친구였구나!
BasicInput.tsx
import React, { forwardRef } from 'react';
import { Input, InputProps } from '@nextui-org/react';
export type BasicInputProps = InputProps;
const BasicInput = forwardRef<HTMLInputElement, BasicInputProps>((props, ref) => {
return <Input {...props} ref={ref} />;
});
BasicInput.displayName = 'BasicInput';
export default BasicInput;
ParentComponent.tsx
import React, { useRef } from 'react';
import BasicInput from './BasicInput';
export default function ParentComponent() {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div>
<BasicInput ref={inputRef} placeholder="Type here..." />
<button onClick={handleFocus}>Focus the input</button>
</div>
);
}
삽집을 한 이유로써는 vue에서는 자식 컴포넌트의 ref를 forwardRef와 같은 설정이 없어도 접근 가능했었기 때문에 그 부분에서 착각을 했었던거 같다.