Ref 잘못 사용한 내 코드 수정하기

최주희·2024년 9월 12일
0

코드 리팩토링

목록 보기
3/3
post-thumbnail
post-custom-banner

[React] 함수형 컴포넌트에서 ref 사용에 대한 고민

회사의 현업 개발자 제롬님께 모르는 것을 쏟아냈다. 어느 정도 갈피를 잡을 수 있었다. 그동안 아무것도 모르고 사용했던 ref의 용도도 제대로 이해하게 되었고, 언제 어떻게 사용하는 게 좋은지도 알게 되었다. (Merci beaucoup.😃)

1. 우선 forwardRef 제거

[IndustryForm]

<DropDownBox isWholesaler={isWholesaler}>
  <Dropdown
    name='sectorId'
    options={industryData}
    placeholder='업종 카테고리를 선택해주세요'
  />
</DropDownBox>

[DropDown]

const Dropdown = forwardRef<HTMLDivElement, Props>(
  ({ options, placeholder, name }, ref) => {
    const industryRef = useRef<HTMLDivElement>(null);
    useClickOutside(industryRef, () => setIsOpen(false));

    return (
      <DropdownContainer ref={ref}>
        {/* Dropdown content */}
      </DropdownContainer>
    );
  }
);

export default Dropdown;

forwardRef는 드롭다운을 열고 닫는 기능을 제어하려고 사용했다. 하지만.. 이 코드는 불필요하고, 단지 공간만 차지 할 뿐이었다.

forwardRef를 잘 모르고 썼었다.
나는 dropdown 외부를 클릭하기 위해서는 부모에 ref를 전달해서 부모가 직접 드롭다운을 열고 닫기를 제어해야하는 줄 알았다.

정말 잘못된 생각이었다.

내 코드에서는 굳이 부모 컴포넌트가 dropdown 컴포넌트의 내부 DOM 요소에 직접 접근하거나 조작할 필요가 없기 때문에 쓰지 않아도 된다.

이미 Dropdown 내부에서 상태를 관리하고 있기 때문에 부모는 신경 쓸 필요가 없고 UI는 상태에 따라 동적으로 렌더링하고 있기 때문이다.

function Dropdown({ options, placeholder, name }: Props) {
  const industryRef = useRef<HTMLDivElement>(null);
  useClickOutside(industryRef, () => setIsOpen(false));

  return (
    <DropdownContainer ref={industryRef}>
      {/* Dropdown content */}
    </DropdownContainer>
  );
}

export default Dropdown;

Q. 만약 상태로 관리하지 않고 제대로 forwardRef를 사용했더라면 어떻게 코드를 짰을까?

이렇게 자식 컴포넌트를 조작하는 로직이 필요하다.

function parentComponent() {
	const dropdownRef = useRef(null);
	
	const openDropdown = () => {
	   dropdownRef.current.style.display = 'block'; // 부모가 직접 DOM에 접근해 열고 닫기 제어
	}
	
	return (
	<div>
		 <button onClick={openDropDown}>모달 오픈!</button>
		 <Dropdown ref={dropdownRef}/>
	</div>
	)

}
const Dropdown = forwardRef<HTMLDivElement, Props>(
  ({ options, placeholder, name }, ref) => {
    const industryRef = useRef<HTMLDivElement>(null);
    useClickOutside(industryRef, () => setIsOpen(false));

    return (
      <DropdownContainer ref={ref}>
        {/* Dropdown content */}
      </DropdownContainer>
    );
  }
);

2. 한번만 사용되어야하는 ref

ref 계속 사용해도 되는 줄 알았다.

ref는 특정 DOM 요소에 대한 직접적인 참조를 생성하는데, 한 번에 한 개의 요소만 참조 할 수 있다.

따라서 PlaceRef를 여러 요소에 사용하면 React는 ref.current에 마지막으로 할당된 요소만 기억하게 된다.

<InputWrapper ref={placeRef}> // 여기서 placeRef에 할당됨
  ...
  {isPostcodeVisible && (
    <div className='post-modal' ref={placeRef}> // 다시 placeRef에 할당되며, 이전 참조는 사라짐
      <DaumPostcode onComplete={handleSelect} />
    </div>
  )}
</InputWrapper>

Q. 그럼 난 왜 두번 참조를 했는가

InputWrapper도 모달 개념이라고 판단해 닫아줘야한다고 생각했고, 주소검색을 위해 열리는 post-modal도 당연히 닫히는 대상이라 둘 다 추가 했다. 생각해보면 실제로 닫히는 대상은 post-modal뿐이다. InputWrapper는 모달이 포함된 전체 영역일뿐.


결론

forwardRef는 자주 사용하지 않는다. 필요할 때만 신중하게 사용하는 것이 중요하고, 대부분 컴포넌트 내부에서 상태를 관리하는 방식이 더 적절하다.

리액트 19에서는

forwardRef를 감싸지 않아도 props로 ref를 전달 받을 수 있게 됐다.

// 19 이후

function App() {
  const buttonRef = useRef();

  const onButtonClick = () => console.log(buttonRef.current);

  return (
    <Button ref={buttonRef} onClick={onButtonClick}>
      클릭
    </Button>
  );
}

const Button = ({ ref, ...props }) => {
  return <button ref={ref} {...props} />;
};

참고블로그

profile
큰 목표보단 꾸준한 습관 만들기
post-custom-banner

0개의 댓글