[삽질] 리액트 useEffect 삽질 (props, state, variable, useEffect)

Goyoung2·2023년 1월 5일
1
post-thumbnail

안녕하세요~ 👋

오늘은 리액트로 삽질.. 한 기록을 간단하게 적어보려고 해요.

요즘은 리액트 개발에 함수형 컴포넌트를 더 많이 사용하실텐데요. 함수형 컴포넌트의 장점 중 하나가 useEffect라고 생각해요.

useEffect의 depenency array를 활용하면 컴포넌트 업데이트가 마법처럼 쉽게 해결되는 기분이에요. 하지만... 과연 그럴까요??
리액트 코드가 점점 복잡해지고, 렌더링 성능을 고려해서 리액트 개발을 하다보면 useEffect만큼 골칫거리인 녀석이 또 없어요.

useEffect에 대해서는 조금 아래에서 다루기로 하고...

props, state, variable의 차이를 알고계신가요?

리액트를 다루다보면 이들의 차이를 금방 알게 되실거에요. 그런데 정말로 우리가 생각한대로 리액트가 동작을 하고 있을까요? 아마 아닐 수도 있어요. 아래에 간단한 리액트 코드를 봐볼게요.

// Case1: save props in state (bad)
// num1 = 1
export default function SimpleComponent({ num1 }) {
  const [num2, setNum2] = useState(num1)
  const num3 = num1 + num2
  
  return <div>{num3}</div>
}
  • 위 코드에서 props로 num1을 받고, state로 num2에 num1을 저장해요. 그리고 num3 variablenum1 + num2를 계산해서 저장해요.
    num3는 어떻게 될까요? 당연하게도 1+1 이므로 2가 출력돼요.
  • 이 상태에서 props인 num1을 2로 바꿔봅시다. props가 변경됐으므로 num1은 2가 돼요. 그렇다면 num2는 어떻게 될까요? num2에 num1을 저장하니 num2도 2가 될까요? 그렇지 않아요. num2는 그대로 1이에요. 그래서 num3는 2+1 즉 3이 돼요. 뭐죠??
  • 이번엔 브라우저를 새로고침하면 어떻게 될까요? num3는 2+2 즉 4가돼요. 이상하지 않나요?? 어떤 때는 2+1이고, 어떤 때는 2+2가 되는걸까요?

props를 state에 저장할 경우 사이드 이펙트가 발생할 수 있어요!

  • props를 state에 저장하게되면 의도치 않은 동작을 하게 될 가능성이 있어요.
  • props가 변경되어도 state는 유지돼요.
  • 첫 마운트 시에는 props를 state에 저장할 수 있어요. 하지만 props가 변경되어 리렌더가 될 경우 props의 변경사항을 state에 다시 저장하지 않아요.

저는 props가 변경되면 컴포넌트가 리렌더 될거고, 리렌더는 props를 state에 저장시킬거라 생각했어요. 하지만 props를 state에 넣을 경우 의도한대로 동작하지 않았어요. 여러분들도 props를 state에 저장하는 실수를 하지 않길 바래요.

그렇다면 어떻게 해야할까요?

아래 코드를 봐보시죠. 위에 있던 예시에서 살짝만 변경을 해봤어요. num2에 num1을 저장하지 않고, num2 자리에 (num2 || num1)을 사용했어요. || 연산자를 사용해서 num2가 falsy할 경우 num1으로 대체하는거에요. 여기서 num2는 undefined에요.
(* falsy: undefined, null, 0, '')

// Case2: do not save props in state (Good)
// num1 = 1
export default function SimpleComponent({ num1 }) {
  const [num2, setNum2] = useState()
  const num3 = num1 + (num2 || num1)
  
  return <div>{num3}</div>
}

이렇게 하면 num1이 변경될 때마다 num3가 올바르게 변경돼요! num1이 1이면 num3는 2, num1이 2이면 num3는 4가 되는거죠. 또 setNum2()를 사용해서 state를 변경할 경우 num1 + num2를 실행하므로 언제나 올바른 결과가 나오게 할 수 있어요.
|| 연산자가 조금 거슬린다면 삼항연산자를 사용해도 좋을 것 같아요.
const num3 = num1 + (num2 === undefined ? num1 : num2)

props를 state에 담아야하는 경우 고민이 필요해요.

// Case3: props in useEffect (consider)
// num1 = 1
export default function SimpleComponent({ num1 }) {
  // prop를 state에 저장하고 useEffect사용해서 업데이트
  const [num2, setNum2] = useState(num1)
  const num3 = num1 + num2
  
  // props를 useEffect에 넣을 경우, props 변경시 불필요한 리렌더링이 발생하기 때문에 성능에 좋지 않을 수 있어요.
  useEffect(() => {
    setNum2(num1)
  }, [num1])
  
  return <div>{num3}</div>
}

위 코드를 보면, props를 useEffect의 의존성 배열에 담아서 num2를 set하는 진풍경을 볼 수 있는데요... 이렇게 하면 의도한대로 num3를 구할 순 있지만, num2를 저장하기 위해 아래처럼 불필요한 리렌더가 발생되어 성능이 저하돼요.
num1 변경 -> 리렌더 -> setNum2() -> 리렌더
위에 설명했던 || 연산자를 활용하면 리렌더를 발생시키지 않는 코드를 짤 수 있어요. 이외에도 성능을 높이면서 리액트를 개발하는 여러 방법들이 있는데요.

저는 아래 컬럼에서 많은 도움을 얻었어요. 읽어보시고 useEffect를 잘못 사용하고 있는 부분이 없는지 점검해보세요~
링크: useEffect 잘못 쓰고 계신겁니다.

useEffect는 언제 써야할까요?

useEffect는 컴포넌트가 렌더된 후 동작해요. 좀 더 자세히 보면 컴포넌트가 마운트, 업데이트, 언마운트 될 때 동작해요.

- props와 useEffect (consider 🤔)

props가 변경되면 컴포넌트가 리렌더돼요. 즉, props는 항상 화면에 반영이돼요. 그렇기 때문에 useEffect 내에서 props를 사용할 필요가 없어요.
단, props를 저장하고 이를 변경해야한다면 state에 저장해서 사용해야해요. 이때는 어쩔수없이 useEffect를 사용하여 props가 변경될 경우 setState()하는 방법을 사용해요.

- variable과 useEffect (Super Bad 👎)

useEffect는 렌더링의 마지막 단계에서 동작해요. 화면이 그려진 이후의 단계이죠. 이때 변수를 수정할 수 있을까요? 변수를 수정해도 렌더는 이미 끝났기 때문에 수정된 변수가 화면에 반영되지 못해요. 그래서 useEffect 내에서 변수를 수정하면 안돼요.

(리액트 렌더링 과정 참고: https://curryyou.tistory.com/486)

  • 렌더 단계(Render Phase): 새로운 가상DOM 생성 후, 이전 가상 DOM과 비교하여, 달라진 부분을 탐색하고, 실제 DOM에 반영할 부분을 결정한다.
  • 커밋 단계(Commit Phase): 달라진 부분만 실제 DOM에 반영한다.
  • useLayoutEffect: 브라우저가 화면에 Paint 하기 전에, useLayoutEffect에 등록해둔 effect(부수효과함수)가 '동기'로 실행된다. 이 때, state, redux store 등의 변경이 있다면 한번 더 재렌더링 된다.
  • Paint: 브라우저가 실제 DOM을 화면에 그린다. didUpdate가 완료된다.
  • useEffect: update되어 화면이 그려진 직후, useEffect에 등록해둔 effect(부수효과함수)가 '비동기'로 실행된다. effect에 return 부분이 있다면, 구현부보다 먼저 실행된다.

- setState와 useEffect(consider 🤔)

setState를 하면 state가 업데이트되면서 컴포넌트를 리렌더해요. 리렌더를 통해서 새롭게 화면을 구성하기 때문에 변경된 state가 화면에 반영돼요.

useEffect 내에서 setState를 사용하는건 최대한 지양해야해요. 렌더가 끝나고 페인트를 하려고하는데, setState를 만나면 다시 렌더링을 해야하기 때문이에요. useEffect 내에서 setState를 사용했다면, 잘못된 부분이 없는지 한번더 생각하고 고쳐봐야해요.

- initial fetch와 useEffect (Good 👍)

페이지 로드시 데이터 fetch를 해야하는 상황이에요. 리액트에서 데이터 fetch를 하다보면 조금 아쉬운 부분이 있어요. render 전에 데이터를 불러오면 좋을텐데... 렌더 후에 useEffect를 통해서 다시 fetch를 하면 리렌더가 발생하고 불필요한 리렌더가 될텐데... 렌더 전에 fetch를 하는 방법은 없을까요?
결론부터 말하자면, 기존처럼 의존성에 빈배열을 줘서 mount시에 useEffect 내부에서 fetch를 하는게 최선이에요. 나아가 에러핸들링, api상태, 데이터 캐싱, 취소, 지연시간 등 다양한 fetch 컨트롤이 필요하다면 react-query, swr 등의 전문 라이브러리를 사용하는게 좋아요.

react fetch에 대한 자세한 내용은 아래 컬럼을 참고해주세요. (영어 주의)
링크: how-to-fetch-data-in-react

- on-demand fetch와 useEffect (Bad 👎)

특정 상황에서 fetch를 해야하는 상황이에요. state 변경시에 useEffect를 사용해서 fetch를 하는게 좋은 방법일까요?

예를 들어볼게요. 6자리 비밀번호를 입력했을 때, 자동으로 로그인 버튼이 눌리며 fetch를 하는 상황을 생각해볼게요. 아래처럼 코드 작성이 가능할거에요.

useEffect(() => {
  if(password.length === 6) {
    fetch('/login')
  }
}, [password])

우리는 비밀번호를 입력했고, setPassword를 통해서 password가 변경되면서 리렌더 됐을거에요. password가 변경됐으므로 useEffect 내에서 6자리 체크를 하고 fetch를 실행할거에요. 아무 문제 없어보여요. 아직 까지는 말이죠.
그런데 코드가 복잡해지고 useEffect 내에 password 뿐만 아니라 다른 의존성 변수들이 늘어난다면 얘기는 달라져요.

다른 의존성 변수들 때문에 useEffect 내부의 코드가 여러번 실행될 가능성이 있어요. 이게 POST 요청이었다면 데이터가 바뀌거나 손상될 수 있어요. 이외에도 의도치 않게 useEffect 때문에 버그가 발생하는 경우가 정말 정말 자주 발생해요.

on-demand fetch의 경우 이벤트 함수 내에서 fetch를 처리하는게 안전해요. 이벤트 함수내부에서 조건을 만족했을 때 fetch를 실행하고 다시 조건을 초기화 하는 방식으로 사이드 이펙트를 방지할 수 있어요.

- Mount, Unmount와 useEffect (Very Good 👍)

마운트, 언마운시에 사용하는 useEffect는 아주 좋아요. 딱 한번만 실행되기 때문에 사이드 이펙트가 발생할 여지가 적고, 불필요한 리렌더도 발생시키지 않아요.

결론!

'리액트 버그는 useEffect에서 발생한다'는 말이 있어요. useEffect는 다양한 이유로 사용되기 때문에, 의도치 않게 사이드 이펙트(부작용, 버그)를 발생시킬 가능성이 있어요. 내가 적절한 곳에 useEffect를 사용했는지, 의존성 배열이 과도하진 않은지, 불필요한 리렌더를 발생시키지는 않는지 등 다시 한번 더 고민해보고 더 좋은 개발을 할 수 있는 모두가 되기를 바래요~ ^-^

긴 글 읽어주셔서 감사해요. 주니어 FE개발자라 부족한 부분이 있을 수 있어요. 댓글 남겨주시면 고치도록 할게요!
안녕히 계세요~ 👋

profile
프론트엔드 엔지니어로 일하고 있어요. 제품, 동료, 성장을 중요시해요. 겸손, 존중, 신뢰를 갖춘 동료가 되기 위해 노력해요. 😄

0개의 댓글