`프론트엔드 개발자` 개형이의 벽돌집

[React useEffect] 리액트 useEffect의 올바른 사용법 알아보기 본문

React

[React useEffect] 리액트 useEffect의 올바른 사용법 알아보기

개형이 2024. 2. 20. 16:00

 

🤔 프로젝트를 진행하다가 무분별하게 useEffect가 많이 사용된 곳을 본 적이 있다.

한 컴포넌트 안에 여러개의 useEffect가 있고

deps가 모두 달라 어떤 값의 변경에 따라 어떻게 동작이 수행되는지 추적하기 까다로웠던 경험도 있다.

 

useEffect를 보통 언제 사용할까 고민해 봤다.

 

- state 값이 변경될 때마다 특정 동작을 수행하고 싶을 때

- 초기 Mount시 data fetching 할 때

- 그 외 컴포넌트 mount시 단 한번 수행하고 싶은 동작을 넣을 때

 

문득 개발 중에 위처럼 쓰는 게 맞을까? 고민했고

서치를 통해 좋은 자료를 발견했다.

 

https://www.youtube.com/watch?v=SrPebT4VBYc

 

 

위 자료의 예시를 직접 해보며 useEffect가 어떻게 동작하는지 알아보고자 한다.

 

 

 


 

 

 

React 18 이후 strict mode일 경우 useEffect는 두 번 동작함

'use client'
import React, { FC, useEffect } from 'react'

const MainPage = () => {
  useEffect(() => {
    console.log('Render!')
  }, [])

  return <p>Main Page</p>
}

export default MainPage

 

- Next.js 환경에서 테스트하고 있어 client hook 사용에 대해 'use client'를 붙여줬다.

 

위처럼 코드를 동작시키면 콘솔에 어떻게 찍힐까?

 

 

 

 

react18 + strict mode일 때 두 번 찍힌 console.log

 

 

 

정말 두 번 찍혔다. strict mode를 끄고 해 보자.

 

/** @type {import('next').NextConfig} */
const nextConfig = {
  //...,
  reactStrictMode: false,
}

 

- Next config 파일에 strict mode를 활성화하지 않게 했다.

- 참고로 next.js는 기본적으로 default가 strict mode이다.

 

 

 

react 18 + strict mode off일 때 한 번 찍힌 console.log

 

 

 

왜 이런 현상이 발생하는지 간단히 기술하자면,

개발모드 (env === 'development')에 한해서 페이지 이동 후 다시 돌아왔을 때 앱이 망가지는 부분이 없는지 확인을 위함.

재사용 가능한 상태를 유지하기 위해 다시 돌아왔을 때 마운트 해제됐을 때의 이전에 사용한 것과 동일하게 구성하기 위함.

 

참고: 리액트 공식 문서

❗️결론은 마운트 해제 후 (useEffect clean up 이후) 다시 돌아왔을 때 사용해도 문제가 없는지 확인하기 위해 개발모드에서만 두 번 호출되는 듯하다.

 

 

 

React 18 이후 strict mode일 경우 clean up 순서는?

const MainPage = () => {
  useEffect(() => {
    console.log('Render!')
    return () => console.log('clean up!')
  }, [])

  return <p>Main Page</p>
}

 

- clean up 함수를 넣어주었다.

 

 

 

react 18 + strict mode일 때 clean up 호출 순서

 

 

위 console을 봤을 때 clean up의 순서는

effect 발생 -> clean up 발생 -> effect 발생

 

인 것으로 보인다.

 

 

그냥 strict mode를 끄면 안 될까?

React의 strict mode는 운영 환경에서 일어날 수 있는 잠재적인 문제나 버그를 개발 환경에서 잡아주기 때문에 필요함.

위 이유로 strict mode는 항상 켜는 것이 추천된다!

참고: 리액트 공식 문서 

 

 

 

그렇다면 올바른 useEffect 사용법은?

언급한 유튜브 영상에서 소개된 방법 중 몇 가지를 직접 해보았다.

 

 

1. props에 따른 useEffect

 

개발 중에 이랬던 경험이 있다.

🔬 props가 변경될 때 useEffect의 effect 발생을 시키고 싶어.

 

한번 해보자.

 

const MainPage = () => {
  const [value, setValue] = useState(0)
  const handleClick = () => {
    setValue(value + 1)
    console.log('value 변경')
  }
  return (
    <>
      <ChildComponent value={value} />
      <button onClick={handleClick}>TEST BUTTON</button>
    </>
  )
}
const ChildComponent = ({ value }: any) => {
  const [childValue, setChildValue] = useState('')
  useEffect(() => {
    console.log('childValue 변경')
    setChildValue('')
  }, [value])

  return <div>Child Component</div>
}

 

- 부모 컴포넌트에서 value라는 이름을 가진 state를 만들고 버튼을 누를 때마다 value가 변경되도록 함

- 자식 컴포넌트에서 부모 컴포넌트의 value를 props로 받음

- props로 받은 value를 useEffect의 deps에 넣어주고 value가 변경될 때마다 자식 컴포넌트의 state 값 초기화되도록 함

 

 

🔬 위와 같은 경우일 때 결과는 어떻게 될까?

 

 

 

 

 

예상했겠지만 console.log는 두 개가 모두 찍혔다.

이 말은,

버튼 클릭 시 value 변경과 함께 리렌더링 -> 자식에서 child value값 초기화 되면서 리렌더링

 

총 두 번 렌더링 된다.

 


이를 한 번만 렌더링 되게 하는 방법은 아래 코드와 같다.

 

const MainPage = () => {
  // ...
  return (
    <>
      <ChildComponent key={value} value={value} />
      <button onClick={handleClick}>TEST BUTTON</button>
    </>
  )
}
const ChildComponent = ({ value }: any) => {
  const [childValue, setChildValue] = useState('')
  console.log('render')

  return <div>Child Component</div>
}

 

- ChildComponent에 key 값을 value로 넣어 props로 보내준다.

- ChildComponent에서는 불필요한 useEffect를 제거한다.

 

 

위처럼 코드를 작성하면 불필요한 useEffect를 사용할 필요도 없고

key 값이 value가 바뀔 때 같이 바뀌기 때문에 childValue를 따로 리셋해 줄 필요가 없게 된다.

 

key 값이 변경되는 걸 통해 고유성을 부여하는 방법, 생각을 못해본 방법인데 놀랍다는 생각이 들었다. 🤭

 

 

❗️그렇다면 props 변경에 따라 특정 상태만 바뀌어야 하는 경우라면?

 

function Page({ value }) {
	const [a, setA] = useState('')
	const [b, setB] = useState('')

	const [prevValues, setPrevValues] = useState(value)
	if (value !== prevItems) {
  		setPrevValues(value)
  		setB('')
    }
}

 

- value가 변경될 때 b 상태만 변경하고 싶다.

- prevValues를 두어 이전 값을 저장하고 있는다.

- if 구문을 통해 value 변경됐을 때만 b 상태를 바꾼다.

 

 

위 방법을 통해 value가 변경됐을 때 b 값만 바꾸면서 불필요한 useEffect를 사용하여 리렌더링이 추가로 일어나는 것을 방지할 수 있다.

if 구문을 최상위에 넣는 그림은 피하게 됐었는데... 위 같은 방법도 있구나, 다시 생각도 해보게 되었다.

 

 

2. Event Handler에 따른 useEffect

 

간단하게 설명하자면,

이벤트 발생을 통해 변경된 값을 useEffect가 바라보고 이를 통해 해결하지 말고 이벤트 발생 호출하는 곳에서 해결하라는 것이다.

 

이벤트 핸들러 내부에서 해결할 수 있는 로직은 이벤트 핸들러 내부에 넣고 아니면 useEffect로...

 

useEffect를 사용했을 때 예상치 못하게 발생할 에러가 생기는 거보다 더 나은 선택일 수도 있겠다는 생각이 들었다.

 

 

3. Data Fetching과 useEffect

 

이 부분에 있어서도 결론만 말하자면,

사용해도 OK, 단 서버 비용이 두 배로 들 수는 있다.

물론 fetch -> refetch가 동일하다고 보장된다면!

 

난 이전 프로젝트에서 swr을 사용했던 적이 있다.

막연하게 swr을 사용했던 이유는

 

1) 데이터 패칭이 쉽고 캐시 된 데이터에서 가져올 수 있으니까 효율 측면도 GOOD

2) 재호출도 막아주니까 서버 비용 측면에서도 좋음

3) 포커스 시 refetch 자동 수행

뭐 등등...

 

❗️하지만 swr이나 react-query 라이브러리가 처음 생기게 된 배경도 무언가에 불편함이 있기 때문에 고안이 됐을 것이다.

라고 본질적인 측면에서 생각하게 된 계기가 되었다.

 

 

 


 

 

 

 

좋은 참고 자료를 보고 내가 더 보기 쉽게 하려 정리해 보았다.

useEffect 사용에 있어 꼭 한 번씩 망설이게 되는 순간이 있었는데,

좀 더 잘 분별하여 사용할 수 있게 된 것 같다.

Comments