Dog foot print

useEffect 조금 더 자세히 사용해보기 본문

REACT

useEffect 조금 더 자세히 사용해보기

개 발자국 2023. 3. 27. 13:44

useEffect 란 ?

useEffect 함수는 렌더링 이후 컴포넌트와 외부의 시스템을 연결해주기 위해 만들어진 훅이다. 별도로 컨트롤이 가능하다면, “useEffect”를 사용해, 사이드 이펙트를 발생시키기도 한다.

useEffect Types

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
//
type EffectCallback = () => (void | Destructor);
type DependencyList = ReadonlyArray<unknown>;
//
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };

useEffect는 첫번째 인자인 effect렌더링 이후에 사용 할 콜백을 의미한다.. deps는 의존성 목록으로 “effect”로 전달한 콜백 함수의 실행 조건을 의미한다.

 

마운트 시 사용하기

보통 컴포넌트가 “최초 렌더링 되었다”는 의미를 “ 마운트(Mount) 되었다. “라고 표현한다. 보통 컴포넌트가 렌더링 이후에 이루어지는 작업은 최초 렌더링시 필요한 API를 호출 하거나, 소켓연결이나 시간으로 제어해야하는 동작 같이 단 한번만 실행되어야 하는 작업들일 것이다.마운트시 사용하기 위해서는 의존성에 빈 배열 []을 전달하면 된다.

 

아래의 예제는 함수형 컴포넌트에서 setInterval 을 호출해 1초 마다 자신의 상태를 1씩 증가시키는 잘못된 코드이다.

import React, {  useState } from 'react'

export default function Effect_1() {

  const [count, setCount] = useState(0)

  setInterval(()=>{
    setcount((prev) => prev + 1)
    console.log("Update")
  },1000)


  return (
    <div>
      {count}
    </div>
  )
}

이 코드의 실행 결과는 다음과 같은데, 이는 setInterval에서 setCount를 호출 할 떄 마다 Effect_1 함수가 다시 실행되어 setInterval이 다시 호출되기 때문이다.

 

 

위의 예시처럼 렌더링이 발생될 때마다 실행되지 않고, 단 한번만 코드가 실행되어야 하는 경우 useEffect를 사용하고 빈 배열 []을 전달하여 최초 한번 만 실행되게 끔 해야 한다.

 

아래 예시는 위의 코드를 useEffect를 사용해 제어한 코드이다.

import React, {  useState,useEffect } from 'react'

export default function Effect_1() {

  const [count, setcount] = useState(0)

  useEffect(() => {
    setInterval(()=>{
      setcount((prev) => prev + 1)
      console.log("one second later");
    },1000)
        console.log("mount")
  },[])

  console.log("Update")
  return (
    <div>
      {count}
    </div>
  )
}

setCount로 인하여, 함수가 렌더링 되어도 setInterval이 다시 호출 되지 않는다.

 

 

언 마운트시 사용하기

언 마운트(un mount)는 마운트(mount)의 반대되는 말로 컴포넌트가 더 이상 사용되지 않게 되었을 때를 의미한다. 언 마운트시 필요한 작업을 clean up이라고 하는데, 이때는 소켓 연결의 해지나, 구독과 같은 것들을 해지하는 등과 같이 컴포넌트의 흔적을 없애는 행위들을 합니다

 

아래의 예시는 button을 클릭해, Effect_1을 화면에서 제거 했음에도 setInterval함수가 계속 동작하는 잘못된 코드입니다.

import React,{useRef, useState,useEffect} from 'react'

function Effect_1() {

  const [count, setcount] = useState(0)

  useEffect(() => {
    setInterval(()=>{
      setcount((prev) => prev + 1)
      console.log("one second later");

    },1000)
        console.log("mount")
  },[])

  console.log("Update")
  return (
    <div>
      {count}
    </div>
  )
}

export default function App() {

    const [display,setDisplay] = useState(true);

    return (
        <div>
            <button onClick={() => setDisplay((prev)=> !prev)}>Click</button>
            {display ? <Effect_1/> : <></>}
        </div>
    )
}

버튼을 눌러 화면에서 “Effect_1” 컴포넌트를 제거 했음에도, setInterval함수가 계속 실행되어 one second later가 출력 됩니다. 다만 컴포넌트는 실제로 제거된 상태이기에 Update는 더 이상 출력되지 않습니다.

 

 

이처럼 컴포넌트가 제거되어 있지만 함수 내에서 구독이나 지속적으로 동작하는 함수를 제거하지 않으면, 컴포넌트가 다시 마운트 되었을 때 이중으로 연결이 지속되게 되며, 함수내에서 스코프가 계속 유지되어 가비지 컬렉터가 연결된 변수들을 제거하지 못하게 됩니다.

 

아래는 mount때 정의되어 있는 setInterval함수를 "unmount" 시 제거하는 코드입니다.

import React,{useRef, useState,useEffect} from 'react'

function Effect_1() {

  const [count, setcount] = useState(0)

  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1

      })
      console.log("Mount")
    },1000)

    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])

  console.log("Update")
  return (
    <div>
      {count}
    </div>
  )
}

export default function App() {

    const [display,setDisplay] = useState(true);

    return (
        <div>
            <button onClick={() => setDisplay((prev)=> !prev)}>Click</button>
            {display ? <Effect_1/> : <></>}
        </div>
    )
}

이제 Effect_1을 화면에서 제거 하였을 때 clearInterval이 작동하여, 더 이상 setInterval함수가 작동하지 않습니다.

 

 

의존성 목록 주입하기

useEffect는 특정 값이 변경될 때 마다 전달한 콜백 함수의 실행 시기를 결정 할 수 있습니다. 이 때 함수의 실행 조건에 해당하는 변수를 의존성이라 하며, 이 변수를 여럿 사용 할 수 있게 끔 배열에 전달하는데, 이 배열의 이름을 의존성 목록이라고 합니다.

아래는 위의 예제에서 count값이 짝수인지 홀수 인지에 따라 화면에 문구를 렌더링 기능을 추가한 예시이다.

import React,{useRef, useState,useEffect} from 'react'

function Effect_1() {

  const messages = ["짝수입니다.", "홀수입니다."]
  const [count, setcount] = useState(0)
  const [message, setMessage] = useState(messages[0])

  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1
      })
      console.log("Mount")
    },1000)

    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])
  useEffect(()=>{
    if(count % 2){
      setMessage(messages[1])
    }else{
      setMessage(messages[0])
    }
  },[count])

  return (
    <div>
      {count}
      <div>{message}</div>
    </div>
  )
}

export default function App() {

    const [display,setDisplay] = useState(true);

    return (
        <div>
            <button onClick={() => setDisplay((prev)=> !prev)}>Click</button>
            {display ? <Effect_1/> : <></>}
        </div>
    )
}

결과는 다음과 같습니다.

 

의존성 목록 사용시 주의해야 할 점

위의 예시에서는 useEffect내에서 count 변수를 의존성으로 선택하여, 이 변수가 짝수인지 홀수인지에 따라 전달할 문자열과 함께 setMessage를 호출 합니다. 그러나, 이와 같은 경우 setCount로 인하여, 렌더링이 발생하고 setMessage로 인하여 렌더링이 두번 발생합니다.

 

 

그러므로 useEffect를 사용해서, 상태를 굳이 변경시키지 않아도 되는 작업은 useEffect 바깥에서 실행하는 것이 좋습니다.

function Effect_1() {

  const messages = ["짝수입니다.", "홀수입니다."]
  const [count, setcount] = useState(0)

  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1
      })

    },1000)
    console.log("Mount")
    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])
  console.log("render")

  return (
    <div>
      {count}
      <div>{count % 2 ? messages[1] : messages[0] }</div>
    </div>
  )
}

useEffect를 사용할 때 가장 조심해야하는 점은 의존성으로 인하여 “무한 루프”에 빠지지 않도록 조심하는 일입니다. useEffect에서 상태를 변경시킬때, 의존성 목록에 만약 같은 상태를 특정 상황일 때만 변경하지 않는다면 그 변경사항으로 인하여 다시한번 useEffect로 전달한 콜백이 실행될 것 입니다.

 

아래의 예제는 count를 의존성 배열로 전달 한 뒤, 내부에서 count변수를 변경하는 잘못된 코드입니다.

function Effect_1() {

  const messages = ["짝수입니다.", "홀수입니다."]
  const [count, setcount] = useState(0)

  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1
      })

    },1000)
    console.log("Mount")
    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])

  useEffect(()=>{
    setcount((prev) => prev + 1)
  },[count])
  console.log("render")

  return (
    <div>
      {count}
      <div>{count % 2 ? messages[1] : messages[0] }</div>
    </div>
  )
}

 

렌더링 시 마다 사용하기

useEffect의 deps 매개변수에 배열을 전달하지 않는다면 해당 useEffect는 렌더링이 발생할 때 마다 실행될 것입니다.

아래의 코드는 의존성 목록을 전달하지 않은 예시입니다.

function Effect_1() {

  const messages = ["짝수입니다.", "홀수입니다."]
  const [count, setcount] = useState(0)

  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1
      })

    },1000)
    console.log("Mount")
    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])

  useEffect(() => {
    console.log("Render")
  })

  return (
    <div>
      {count}
      <div>{count % 2 ? messages[1] : messages[0] }</div>
    </div>
  )
}

 

 

의존성을 전달하지 않은 useEffect를 사용 할 때는 매우 조심해야 합니다. 무한 루프에 빠져 지속적으로 렌더링을 유발 시킬 수 있습니다. 렌더링과 무관한 작업을 하거나 정말 조심히 상태를 업데이트 해야합니다. 그러므로 의존성 목록과 함께 사용하는 것을 적극 권장합니다.

customHook

useEffect도 외부에서 정의하고 컴포넌트 내에서 호출 할 수 있습니다. 주로 재활용성이 높은 함수들을 따로 모아 사용 합니다.

아래의 예제는 “count”의 이전 값을 저장하여 렌더링 된 이후 이전 count값을 사용 할 수 있는 예제입니다.

function customHook(value :number){
  const prev = useRef(value)

  useEffect(()=>{
    prev.current = value
  },[value]);

  return prev.current
}

function Effect_1() {

  const messages = ["짝수입니다.", "홀수입니다."]
  const [count, setcount] = useState(0)
  const prevCount= customHook(count);
  useEffect(() => {
    const myInterval = setInterval(()=>{
      setcount((prev) => {
        console.log(`${prev + 1} second later`);
        return prev + 1
      })

    },1000)
    console.log("Mount")
    return () => {
      clearInterval(myInterval)
      console.log("Un mount")
    }
  },[])

  return (
    <div>
      <div>
        이전 값은 {prevCount}입니다.
      </div>
      {count}
      <div>{count % 2 ? messages[1] : messages[0] }</div>
    </div>
  )
}

 

결과는 다음과 같습니다.

반응형
Comments