주니어 개발자들이 useEffect를 사용할때 자주 하는 실수 1편.

엄강우·2022년 11월 22일
0

You don't know React

목록 보기
8/9

이 글은 평소와 같이 유튜브를 보다가 굉장히 도움 될만한 정보라고 생각해서 소개할겸 저 스스로도 리마인드 할겸 블로그에 저장해두려고 합니다. 이 글의 주제는 제목과 같이 useEffect를 사용할 때에 쉬이 간과할 수 있는 부분을 소개합니다.
혹시라도 저의 글이 아닌 원본을 보시고 싶다면 여기로 가셔서 보시면 될 것 같습니다. 시작하겠습니다.

useEffect의 이해

function App() {
  const [number, setNumber] = useState(0)
  
  useEffect(() => {
    console.log("rerendered : ", number) 
  })
  
  console.count("render")
  
  return (
    <div>
      <button onClick={() => setNumber(prev => prev+1)}>Increase</button>
    </div>
  )
}

다음과 같이 코드를 작성하면 Increase버튼을 누를때 마다 컴포넌트가 리렌더링 되고 console.countnumber가 같은 숫자가 될 것입니다.
useEffect는 인자로 콜백함수와 dependency라는 것을 함께 받습니다. dependency가 가질 수 있는 값은 대표적으로 3가지로 나눌 수 있습니다.

  1. 아무것도 전달하지 않을때
    이때는 컴포넌트리 리렌더링 될때마다 useEffect의 콜백함수가 실행됩니다.
  2. 빈 배열로 전달할 때
    mount그리고 unmount될 때만 실행됩니다.

    mount
    컴포넌트가 처음 실행될 때
    unmount
    컴포넌트가 화면에서 사라질때

  3. 그리고 특정 변수를 전달할때
    특정 변수가 변할때 콜백함수를 전달합니다.

그럼 다음의 예를 한번 보겠습니다.

function App() {
  const [number, setNumber] = useState(0)
  const [value, setValue] = useState('')

  useEffect(() => {
    console.log('rerendered : ', value)
  }, [value])

  console.count('render')

  return (
    <div>
      <input type='text' onChange={(e) => setValue(e.target.value)} />
      <button type='button' onClick={() => setNumber((prev) => prev + 1)}>
        Increase
      </button>
    </div>
  )
}

다음과 같이 useEffectdependency를 설정해준다면 input창에 입력값이 바뀐다면 useEffect의 콜백이 실행이 되지만 button을 통해 number의 값을 변경한다면 useEffect의 콜백이 실행되지 않습니다.

그럼 이제 간단하게 useEffect의 기본에 대해 알아 보았으니 좀 더 심화로 들어가 보겠습니다.

function App() {
  const [name, setName] = useState('')
  const [state, setState] = useState({
    name: '',
    selected: false,
  })

  useEffect(() => {
    console.log('The state has changed, useEffect runs!')
  }, [state])

  const handleAdd = () => {
    setState((prev) => ({ ...prev, name }))
  }
  const handleSelect = () => {
    setState((prev) => ({ ...prev, selected: true }))
  }

  return (
    <div>
      <input type='text' onChange={(e) => setName(e.target.value)} />
      <button type='button' onClick={handleAdd}>
        Add Name
      </button>
      <button type='button' onClick={handleSelect}>
        Select
      </button>
      {`{name:${state.name} , selected:${state.selected}}`}
    </div>
  )

다음과 같은 코드를 한번 봅시다.
일단 input의 입력값에 따라 name변수가 변하고요
Add버튼을 누르면 state.name의 값이 name과 같아집니다.
Select버튼을 누르면 state.selecttrue로 변경합니다.
그리고 useEffectstatedependency로 가집니다.

그럼 상황을 가정해봅시다.
input에 입력값을 입력하면 useEffect가 동작할까요? 동작하지 않겠죠? 왜냐면 name변수에 의존하지 않기 때문입니다.
그럼 Add버튼을 클릭한다면? useEffect의 콜백함수가 실행될 것입니다.
그럼 Select번트을 클릭한다면? useEffect의 콜백함수가 실행되겠죠. 그런데 좀 이상합니다. Select의 버튼을 한번 클릭할때는 state.select가 바뀌었으니 useEffect의 콜백함수가 실행되는 것은 알겠지만 여러번 클릭하면 state.select가 변하지 않음에도 불구하고 useEffect의 콜백함수가 계속해서 실행됩니다.

왜?

우선 리액트의 비교 매커니즘에 대해 알아 보겠습니다. 리액트는 변경에 민감합니다. 변경이 되지 않은 컴포넌트와 변경이 된 컴포넌트를 잘 구분하기 때문에 리액트 또한 좋은 성능을 가진다고 할 수 있죠. 그래서 리액트는 변경을 감지하기 위해 객체 비교 또한 아주 빠르게 비교할 수 있는 얕은 비교를 사용합니다.

얕은 비교란?
얕은 비교란 객체의 프로퍼티가 아닌 객체가 가진 주소만으로 객체를 비교하는 것을 의미하는 것입니다.

그럼 얕은 비교를 하는 것이 왜 이런 결과를 낳았을까요? useState의 훅을 위해 객체를 변경하게 되면 리액트 내에서는 불변성을 유지하면서 기존 객체의 프로퍼티를 교체하는것이 아닌 새 객체를 만들어서 저장하게 됩니다. useEffect는 얕은 비교를 통해 실질적인 객체의 프로퍼티는 변경되지 않았지만 속만 같은 다른 객체를 감지하게 되는 것입니다. 그럼 useEffect는 아! 객체가 변경된것이구나 하고 콜백함수를 실행하게 하는 것입니다.

그럼 이를 막기 위해서는 여러가지 방법이 있습니다.

// 1
const user = useMemo(() => {
  return {...state}
}, [state.name, state.select])
useEffect(()=>{
}, [user])

이 방법은 React가 제공하는 useMemo를 이용한 방법으로 state.namestate.select가 변할때만 user가 새로운 객체로 바뀝니다. 그럼 useEffect또한 user가 바뀌지 않으면 콜백함수를 실행하지 않겠죠?

// 2
useEffect(() => {
},[state.name, state.select])

물론 useEffect에 직접 dependency를 설정해주어도 괜찮습니다. 하지만 이 방법은 컴포넌트가 복잡해졌을때 dependency를 너무 어지럽힐 수 있으니 그럴때는 1번 방법을 이용하면 됩니다.

오늘은 여기까지이며 내일에는 useEffectcleanup Function에 대해 알아보겟습니다.

profile
안녕하세요 프론트엔드 개발자를 꿈꾸는 엄강우입니다.

0개의 댓글