Select 컴포넌트 구현 (react-useEffect 로 인한 무한 렌더링 문제 고려하기)

수연·2025년 5월 25일

React

목록 보기
6/6

서론

현재 프로젝트는 공통 컴포넌트를 일일이 개발하여 사용하고 있다.

기존엔 Select 컴포넌트를 사용하지 않았지만 새로운 디자인이 나와 해당 컴포넌트를 구현하며 고민을 많이 했는데 useEffect 사용하지 않기 아티클을 정독하며 얻은 구현 방법을 정리해보았다.

Select 컴포넌트

형태

컴포넌트를 구현할 때 항상 사용하고 싶은 형태를 먼저 고민한다. Shadcn UI와 Chakra UI를 사용해보면서 대략 어떻게 컴포넌트를 사용해야 편한지를 알게 되어서, 공통 컴포넌트를 구현할 때 형태에 대한 참고를 많이 한다.

<Select.Root>
  <Select.Trigger>
    <Select.Value placeholder="선택" value={value} />
  </Select.Trigger>
  <Select.Content>
    <Select.Item value="1">아이템1</Select.Item>
    <Select.Item value="2">아이템2</Select.Item>
    <Select.Item value="3">아이템3</Select.Item>
  </Select.Content>
</Select.Root>

이런 형태의 컴포넌트를 설계하고 나면 해야할 것은 바로 내부구현이다. 여기서부터 value 프로퍼티와 defaultValue 프로퍼티 중 어떤 프로퍼티를 넣어야 하는지를 고민하게 되었다.

이전에 구글 폼 클론 코딩을 할때 Chakra UI를 썼었는데 Chakra UI에서는 둘 다를 프로퍼티로 받을 수 있었다. 그래서 무언가 나도 넣어야하지 않을까? 라는 의문이 생겼는데 왜 넣어야 하는지 이해하지 못한 채 구현을 하기가 싫었다.

Chakra의 value와 defaultValue를 참고한 정의

우선 Chakra UI로 클론 코딩할 시절을 떠올려보면, defaultValue 값 만으로는 Chakra에서 제공해주는 내부 상태를 변화시킬 수가 없었다. (onChange를 올바르게 설정해주었음에도 불구하고)

즉, 초기값을 단 한번 지정해주고 그 다음부터는 내부의 동작에 의존하는 것 같았다. 그러다가 value 프로퍼티를 지정해주면 그제서야 올바르게 동작했다.

이 경험으로 비롯해 defaultValuevalue 의 차이점을 다음과 같이 정의했다.

defaultValue : 초기 상태를 정의하기 위한 값. uncontroll 컴포넌트의 초기값을 지정해주기 위해서만 쓰이며 외부에서 상태를 직접 조작할 수 없다.
value : contoll 컴포넌트로 사용하기 위해 변하는 값. 외부에서 상태를 직접 조작하기 위해 onChange(onValueChange)와 함께 사용한다.

구현해보기

defaultValue 는 초기값만을 설정하기 위해 useState의 기본값으로 넣어주고, 외부의 value 에 따라 내부의 상태 값을 변하게 만들기 위해 다음과 같은 코드를 작성했다.

const [_value, _setValue] = useState(defaultValue);

useEffect(() => {
	_setValue(value);
}, [value]);

useEffect(() => {
	onValueChange(_value); // _value의 값이 바뀌면 onValueChange 실행
}, [_value, onValueChange])

하지만 코드를 작성하면서 너무나도 자연스러운 의문이 들었다.

이거... 무한 루프 걸리기 딱 좋은 코드 아닌가?

대체로 onValueChange에는 상위에서 넘어오는 value를 같이 업데이트 시켜주는 로직이 들어간다. 그럼 다음과 같은 일이 벌어질 수도 있다.

  1. 외부의 상태인 value가 업데이트 되어 Select 컴포넌트 내부의 _value도 업데이트
  2. _value가 업데이트 되어 onValueChange 이벤트 실행
  3. onValueChange가 value의 상태 업데이트
    ...

만일 string, boolean등의 값만 value 에 들어간다면 react 내부적으로 업데이트를 처리할 테니 괜찮지만 Select 컴포넌트에는 안타깝지만 다양한 값이 들어갈 것 같았다. 더불어 string, boolean 외의 값이 들어갔을 때를 대비해 위와 같은 코드는 짜지 않는 것이 바람직하다고 여겨졌다.

useEffect 사용하지 않기

Effect가 필요하지 않은 경우

실무 코드를 작성하기 전에 읽은 아티클들을 다시 보고 있는데, useEffect 가 필요하지 않은 경우 도 그 중 하나다.

내 고민을 해결해줄 수 있는 예시는 바로 이벤트 핸들러로 이동해야 하는 로직 이었다.

어떤 코드가 Effect에 있어야 하는지 이벤트 핸들러에 있어야 하는지 확실하지 않은 경우 이 코드가 실행되어야 하는 이유를 자문해 보세요. 컴포넌트가 사용자에게 표시되었기 때문에 실행되어야 하는 코드에만 Effect를 사용하세요.

Select 컴포넌트에서 onValueChange 이벤트는 value가 바뀔 때마다 발생한다. value가 바뀔 때마다 실행되므로 useEffect 내부에서 실행된다고 생각하기 쉽지만 사실 value또한 이벤트로 바뀐다.

const Item = ({value, onClick, ...props}) => {
  const { _setValue, _value } = useSelectContext();
  
  const handleClickItem = (event) => {
	_setValue(value); // 여기서 Select 컴포넌트 내부의 _value 업데이트
    onClick?.(event);
  }
}

기존의 useEffect를 사용하는 코드에서 value가 바뀌는 것은 다음 단계를 따른다.

  1. Item 컴포넌트를 클릭한다.
  2. handleClickItem에서 _setValue_value 의 상태를 업데이트 한다.
  3. _value 가 업데이트 되어 useEffect로 value 도 업데이트 된다.

하지만 위 코드에서 보았듯 _value 가 실질적으로 바뀌는 곳은 클릭 핸들러 내부이다.

따라서 onValueChange 또한 클릭 핸들러 내부, 즉 실제로 이벤트가 발생하는 핸들러 내부 에서 호출할 수 있는 것이다. 그렇다면 기존의 useEffect 코드에 있는 로직을 모두 _setValue 가 호출되는 곳에 그대로 옮겨주면 된다.

const [_value, _setValue] = useState(defaultValue);

useEffect(() => {
	_setValue(value);
}, [value]);

// ❌ 해당 코드 삭제
useEffect(() => {
	onValueChange(_value); // _value의 값이 바뀌면 onValueChange 실행
}, [_value, onValueChange])
const Item = ({value, onClick, ...props}) => {
  const { _setValue, _value, onValueChange } = useSelectContext();
  
  const handleClickItem = (event) => {
    const nextValue = value;
    
	_setValue(nextValue);
    onValueChange?.(nextValue);
    onClick?.(event);
    
  }
}

이렇게 핸들러 내부에서만 호출하면 외부의 상태인 value가 업데이트 되고, 다시 _value가 업데이트 되지만 onValueChange는 핸들러 내부에서만 실행되므로 (useEffect로 실행 X) 다음 문제가 해결된다.

  1. 처음에 제기했던 useEffect의 값 업데이트로 인한 무한 렌더링 문제를 해결할 수 있다.
  2. 1에서 확장하여 useEffect로 인한 사이드 이펙트를 방지할 수 있다.

별 생각없이 개발을 하다보면 useEffect 로 쉽게 생각이 귀결되곤 한다. 특히나 급한 개발일수록 그런 것 같다. 하지만 이번 코드를 작성하고, 아티클을 다시 정독하면 useEffect 가 반드시 필요한 경우는 그리 많지 않은 것 같다.

그래서 틈이 날 때마다 내가 짠 코드에서 useEffect를 제거하고 있는데, 제거하면 제거할 수록 사이드 이팩트가 확실하게 줄어드는 모습을 많이 확인할 수 있었다.

더불어 이런 아티클을 읽으면서 실무에 적용하는 재미가 꽤 큰 것 같다. 요즘은 타입스크립트 책을 읽고 있는데 이전엔 보이지 않거나 와닿지 않았던 이론들이 새로 와닿는다. 실무 코드만 작성할 게 아니라 아티클도 틈틈히 읽고, 내 것으로 만들기 위해 포스트로 정리하는 습관도 다시 들여야겠다.

0개의 댓글