본문 바로가기
JS/React 강의

[React] 13. useEffect() 사용법 총정리

by GiraffePark 2023. 7. 20.

안녕하세요. 박기린 입니다.

이번엔 React Hooks 중 하나인 useEffect()에 대해 알아보겠습니다.

 

 


Effect(Side Effect)란 무엇일까?

useEffect()를 알기 위해선, Effect가 무엇인지를 먼저 알아야 합니다.

여기서 Effect는 Side Effect와 동일한 의미를 지닙니다.

 

React의 주요한 임무는 UI 렌더링입니다. 유저가 키보드를 누르든 클릭을 하든, 입력에 반응하여 필요할 때 UI를 다시 렌더링합니다.

React의 역할

- state, props를 관리한다.
- state와 props를 기반으로 JSX와 DOM을 평가하고, UI를 렌더링한다.
- 유저의 입력을 실시간으로 확인한다.

이러한 React의 역할들은 '화면에 무언가를 가져오는 것(Bringing something on the creen)'에 포함됩니다.

React 컴포넌트들은 단지 함수이기 때문에 위에서 아래로 코드가 실행됩니다. 그리고 이 컴포넌트 함수 내의 코드들은 유저의 입력에 따라 화면에 무언가를 가져오는 일을 합니다. 이때 state를 사용하거나, 핸들러 함수를 사용할 수 있습니다.

 

Side Effect는 위에서 말한 React의 역할인 UI 렌더링에서 벗어난 모든 것들을 의미합니다.

좀 더 깊게 들어가서 설명하자면, Side Effect는 부수효과라는 의미를 지녀서, input-output 이외에 또 다른 값이 변경되는 상황을 의미합니다. 

 

예를 들면, http request 보내기, 브라우저 저장소(Local storage)에 무언가를 저장하고 가져오기 등등이 있습니다. 또는 비동기 함수나 타이머 생성도 있습니다.

만약 쇼핑 사이트에서 물건을 장바구니에 담으면, 화면에는 장바구니 아이콘에 숫자가 +1만 되지만, 그 내부에는 서버와 긴밀히 http request를 주고받으면서 계정 정보를 업데이트 하는 등 React 외부에서 일이 일어나고 있습니다.

 

이러한 작업들은 웸앱에 필수로 포함된 작업들이지만, '화면에 무언가를 가져오는 것'과는 관련이 없습니다. 

-> 따라서 Effect(Side Effect)란, 일반적인 React 컴포넌트 함수 밖에서 일어나는 일들을 말합니다.

 

 

 


Side Effect를 처리해주는 useEffect()

React 컴포넌트 안에 http request가 들어가게 된다면 어떻게 될까요?

state가 변경될 때마다 컴포넌트가 재렌더링됨과 동시에 http request가 또 보내질 것입니다. 이러면 성능저하는 물론이고, 잘못하면 버그나 무한루프가 발생할 수도 있습니다.

이러한 Side Effect를 처리하기 위한 React Hook이 있는데, 그것이 바로 useEffect()입니다.

 

useEffect()의 주요한 역할은, Side Effect를 일으킬 타이밍이 언제인지를 정확하게 지정해주고, 이 Side Effect가 실행될 때 UI 렌더링에 악영향을 끼치지 않도록 방지합니다. 그리고 Side Effect가 실행된 후에 업데이트된 정보가 있어 새롭게 UI를 그려야할 경우, 이에 맞춰서 컴포넌트를 재렌더링하기도 합니다.

 

 


useEffect()의 구조

useEffect( 함수, [ 의존성들-dependencies ] )

useEffect()는 두 개의 인수를 받습니다.

첫 번째는 함수입니다.

두 번째는 의존성 배열입니다. 

 

첫 번째 인수로 주어질 함수 안에는 어떤 SideEffect 코드라도 넣을 수 있습니다. 

이 함수는, 두 번째 인수로 주어질 [의존성 배열] 안에 담긴 값에 변경사항이 생길 경우에만 실행됩니다. 즉, 이 의존성 배열이 useEffect 내부의 함수가 실행될 타이밍을 알려준다고 할 수 있습니다.

만약 useEffect()가 포함된 컴포넌트가 재렌더링되더라도 이 함수는 실행되지 않습니다. 오로지 [의존성 배열] 안에 담긴 값이 변경될 때만 실행됩니다.

 

 

(의존성 배열이란 무엇인지 더 깊이 있게 설명한 글 : https://arnopark.tistory.com/846 )

 

[React] 20. useEffect와 useCallback에서 의존성 배열을 사용하는 이유 (Dependencies Array 설명)

안녕하세요. 박기린입니다. useEffect와 useCallback에는 의존성배열 (dependencies array)를 인수로 받는 영역이 있습니다. useEffect 설명글 : https://arnopark.tistory.com/770 useCallback 설명글 : https://arnopark.tistory.com

arnopark.tistory.com

 

 

 

 

useEffect( 함수 )

만약 위처럼, 두 번째 인수가 없는 경우엔, 컴포넌트가 재렌더링 될 때마다 useEffect() 내부의 함수가 실행됩니다.

 

 

useEffect( 함수 , [ ] )

만약 위처럼, 두 번째 인수에 [] 빈 배열이 주어진 경우엔, 처음 컴포넌트가 실행된 순간에만 useEffect() 내부의 함수가 실행됩니다.

그 이후에는 어떤 일이 있어도 함수가 실행되지 않습니다. 딱 처음 컴포넌트가 실행될 때만 작동합니다.

 

 

 


useEffect()의 Cycle 순서

1. 컴포넌트가 렌더링됨 (마운트)

2. useEffect()의 첫 번째로 인수로 넘겨진 함수가 실행됨

 2.1 이때 실행된 함수로 state, props가 변경된 경우, 컴포넌트 재렌더링

3. 대기 상태에서, useEffect()의 두 번째 인수로 넘긴 의존성 값들에 변경사항이 발생하는 지를 체크

4. 변경사항이 발생하면 useEffect()의 첫 번째 인수로 넘겨진 함수가 실행됨

 4.1 이때 실행된 함수로 state, props가 변경된 경우, 컴포넌트 재렌더링

5. 컴포넌트가 사라지면 같이 사라짐 (언마운트)

 

 

 


useEffect() 사용해보기 - 1. 빈 의존성 배열

useEffect()를 통해, 인터넷 브라우저에 로그인 정보를 저장해서, 사이트를 새로고침해도 로그인이 유지되는 기능을 구현해보겠습니다.

 

 

1. useEffect()없이 구현해보기

useEffect() 없이 컴포너트 내에서 로그인 저장 기능을 구현했습니다.

위처럼 구현하면 무한루프가 발생합니다.

 

1. App 컴포넌트가 시작되면서, localStorage.getItem('isLoggedIn')이 실행됩니다.
2. LocalStorage 내부에 저장된 isLoggedIn이 1인 것을 확인한 후, setIsLoggedIn(true)가 실행됩니다.
3. setState()가 일어났기 때문에 App 컴포넌트가 재실행됩니다.

4. 다시 App 컴포넌트에서 localStorage.getItem('isLoggedIn')이 실행됩니다.
5. 또, LocalStorage 내부에 저장된 isLoggedIn이 1인 것을 확인한 후,  localStorage.getItem('isLoggedIn')이 실행됩니다.
6. setState()가 일어났기 때문에 App 컴포넌트가 재실행됩니다. <- 무한루프

 

useEffect()를 사용하면 이러한 상황을 방지할 수 있습니다.

 

 

2. useEffect()로 구현해보기

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const storedUserLoggedInInformation = localStorage.getItem('isLoggedIn');

  useEffect(() => {
    if (storedUserLoggedInInformation === '1') {
      setIsLoggedIn(true);
    }
  }, []);

  const loginHandler = (email, password) => {
    localStorage.getItem('isLoggedIn', '1');
    setIsLoggedIn(true);
  };

  const logoutHandler = () => {
    localStorage.removeItem('isLoggedIn');
    setIsLoggedIn(false);
  };

 return( {... jsx ...} )
}

 

 

 

  useEffect(() => {
    if (storedUserLoggedInInformation === '1') {
      setIsLoggedIn(true);
    }
  }, []);

useEffect의 의존성 배열이 빈 배열[]로 주어지면, 처음 App 컴포넌트가 실행될 때 한 번만 useEffect() 내부 함수가 실행되고, 그 외에는 실행되지 않습니다.

 

1. App 컴포넌트가 시작되면서, localStorage.getItem('isLoggedIn')이 실행됩니다.
2. useEffect() 내부의 함수가 실행됩니다. - LocalStorage 내부에 저장된 isLoggedIn이 1인 것을 확인한 후, setIsLoggedIn(true)가 실행됩니다.
3. setState()가 일어났기 때문에 App 컴포넌트가 재실행됩니다.

4. 다시 App 컴포넌트에서 localStorage.getItem('isLoggedIn')이 실행됩니다.
5. useEffect()의 의존성 배열이 빈 배열[]이라서, useEffect() 내부의 함수가 다시 실행되지 않습니다. <- 무한루프 해결

 

 


useEffect() 사용해보기 - 2. 특정 의존성의 값이 변경되는 경우

useEffect()의 의존성 배열에, 빈 배열이 아닌 실제 변수(state/props)가 들어가는 경우를 알아보겠습니다.

예시 중 하나로 '이메일/비밀번호 유효성 검사'가 있습니다.

 

 

1. useState()만을 이용해서 유효성 검사 구현

const Login = (props) => {
  const [enteredEmail, setEnteredEmail] = useState('');
  const [enteredPassword, setEnteredPassword] = useState('');
  const [formIsValid, setFormIsValid] = useState(false);

  const emailChangeHandler = (event) => {
    setEnteredEmail(event.target.value);

    setFormIsValid(
      event.target.value.includes('@') && enteredPassword.trim().length > 6
    );
  };

  const passwordChangeHandler = (event) => {
    setEnteredPassword(event.target.value);

    setFormIsValid(
      event.target.value.trim().length > 6 && enteredEmail.includes('@')
    );
  };

 

emailChangeHandler : 이메일(아이디) 입력 칸에 쓰이는 핸들러 함수
passwordChangeHandler : 비밀번호 입력 칸에 쓰이는 핸들러 함수

useState만 사용한 경우, 위처럼 이메일과 비밀번호 핸들러 함수에 각각 유효성 검사 코드 (setFormIsValid) 를 넣어놔야 합니다.

이런 방식으로 구현하면, 입력창에 입력이 들어올 때마다 setFormIsValid()가 작동하게 되어서, 퍼포먼스에 부정적인 영향을 줍니다.

 

 

 

2. useEffect()를 이용해서 유효성 검사 구현

useEffect()를 사용하면, 이메일과 비밀번호의 유효성검사 코드를 하나로 묶을 수 있습니다. 거기에다가 키보드를 입력할 때마다 유효성검사( setFormIsValid() )가 작동되는 것을 막기 위해, setTimeOut(타이머)를 적용해서 0.5초 동안 키보드 입력이 멈췄을 때만 유효성 검사를 할 수 있도록 구현할 수 있습니다.

 

useEffect(() => {
    const identifier = setTimeout(() => {
      console.log('Checking form validity!');
      setFormIsValid(
        enteredEmail.includes('@') && enteredPassword.trim().length > 6
      );
    }, 500);

    return () => {
      console.log('CLEANUP');
      clearTimeout(identifier);
    };
  }, [enteredEmail, enteredPassword]);

의존성배열에는 [변화를 체크하고자 하는 state - enteredEmail, enteredPassword]를 추가해주면 됩니다. 

 

이렇게 설정하면,

1. 유저가 이메일, 패스워드를 입력할 때마다 enteredEmail, enteredPassword 값이 변경되고,

2. enteredEmail, enteredPassword가 변경된 것을 감지해서 useEffect() 내부 함수가 작동합니다.

3. setTimeout으로 0.5초를 기다린 후, 내부 코드가 실행됩니다. 만약 0.5초가 끝나기 전에 유저가 또 입력을 하면, 타이머는 초기화됩니다.

4. 0.5초가 지나면 타이머(setTimeout) 내부 함수가 실행됩니다.

 

    return () => {
      console.log('CLEANUP');
      clearTimeout(identifier);
    };

5. 실행이 끝나면 useEffect의 return 함수가 작동합니다. 이때 return 키워드 뒤에 나오는 함수클린업 함수라고 부릅니다.

클린업 함수를 사용해서, useEffect에서 실행한 모든 Side Effect를 정리해주면 좋습니다. setTimeout으로 불러온 타이머는 clearTimeout() 함수를 이용해서 정리할 수 있습니다.

 

* useEffect의 return 함수(클린업 함수)가 실행되는 순간 : 컴포넌트나 의존성이 변경되어서, useEffect가 만료되는 순간입니다.

 

 

 

구현 결과

 

 

 

*헷갈릴 수 있는 부분

useEffect(() => {
    const identifier = setTimeout(() => {
      console.log('Checking form validity!');
      setFormIsValid(
        enteredEmail.includes('@') && enteredPassword.trim().length > 6
      );
    }, 500);

    return () => {
      console.log('CLEANUP');
      clearTimeout(identifier);
    };
  }, [enteredEmail, enteredPassword]);

아까 유효성검사 구현에 사용한 useEffect() 구문을 다시 살펴보겠습니다.

위의 useEffect() 코드 안에 들어간 함수는, LocalStorage, http request를 사용하지 않았습니다. 오히려 react state를 업데이트 하는 seFormIsValid()라는 코드를 사용했습니다. 아까 Side Effect에 대해 알아봤을 때, state로 UI가 재렌더링 되는 것은 Side Effect에 해당하지 않는다고 했었던 거 같은데, 왜 useEffect()에서 사용했을까요?

 

 

[Side Effect는 부수효과라는 의미를 지녀서, input-output 이외에 또 다른 값이 변경되는 상황을 의미합니다.] 라는 표현을 위에서 했습니다.

  const emailChangeHandler = (event) => {
    setEnteredEmail(event.target.value);
  };

emailChangeHandler()는 email 입력창에 들어가는 핸들러 함수입니다. 이 함수는 '이메일 입력창의 키 입력을 듣고, 입력된 데이터를 저장하는 것'이 주된 임무입니다.

input : 유저의 키 입력 - output : 입력받은 정보를 enteredEmail에 저장

그리고 이외의 작업은 모두 Side Effect에 해당한다고 볼 수 있습니다.

 

 

 

  const emailChangeHandler = (event) => {
    setEnteredEmail(event.target.value);

    setFormIsValid(
      event.target.value.includes('@') && enteredPassword.trim().length > 6
    );
  };

위 코드처럼, 유효성검사(setFormIsValid)까지 같이 진행하게 된다면 input-output 관계에서 벗어난 Side Effect라고 볼 수 있습니다.

 

 


useEffect() 최종 평가

지금까지 여러 개의 useEffect() 사용 방식을 알아봤습니다.

마지막으로, useEffect() 코드와 출력 결과를 차례대로 나열하면서, 지금까지 알아본 내용이 이해가 되었는지 점검해보겠습니다.

 

 

1. 의존성 배열이 없는 경우

  useEffect(() => {
    console.log("EFFECT RUNNING");
  });

로그인 컴포넌트가 첫 등장을 하면서, 'EFFECT RUNNING' 콘솔이 찍힙니다.

키보드를 눌러서 키를 입력하면, state가 변경되고, 변경된 state를 적용시키기 위해 컴포넌트가 업데이트 될 때마다, useEffect() 함수가 같이 실행됩니다.

결과 : 키를 누를 때마다 콘솔이 찍힙니다.

 

 

 

2. 빈 의존성 배열이 주어진 경우

  useEffect(() => {
    console.log("Effect running");
  }, []);

빈 의존성 배열이 추가되었습니다.

로그인 컴포넌트가 첫 등장할 때만, 'EFFECT RUNNING' 콘솔이 찍힙니다.

그 이후로는 어떤 키를 눌러도, 콘솔이 찍히지 않습니다.

결과 : 처음 한 번만 실행됩니다.

 

 

 

 

3. 의존성 배열에 의존성 변수가 담긴 경우

  useEffect(() => {
    console.log("Effect running");
  }, [enteredPassword]);

의존성 배열에 의존성 변수가 추가됐습니다.

로그인 컴포넌트가 첫 등장했을 때 콘솔이 찍히고, 의존성 배열에 든 enteredPassword의 값이 변경될 때마다 콘솔이 찍힙니다.

(password 입력창에 키보드를 누를 때마다 콘솔이 찍힘)

 

 

 

4. 클린업 함수가 포함된 경우

  useEffect(() => {
    console.log("Effect running");
    
    return () => {
      console.log("Effect cleanup");
    }
  }, [enteredPassword]);

이번엔 클린업 함수가 추가됐습니다.

클린업 함수는, 로그인 컴포넌트가 DOM에서 제거될 때 실행됩니다.

 

 

 

처음 컴포넌트가 실행될 때는 'EFFECT CLEANUP'이 콘솔에 찍히지 않습니다.

 

 

 

하지만 Password를 치기 시작하면, 키보드 입력과 동시에 enteredPassword state가 변경되고, 변경된 state를 반영하기 위해 컴포너트가 재실행 될 때마다, 클린업 함수가 실행됩니다.

 

 

 

 

5. 클린업 함수가 있는데, 빈 의존성 배열이 주어진 경우

  useEffect(() => {
    console.log("Effect running");
    
    return () => {
      console.log("Effect cleanup");
    }
  }, []);

클린업함수가 있을 때, 의존성 배열의 의존성을 전부 제거한 후, 빈 배열만 인수로 준다면?

 

 

 

컴포넌트가 처음으로 등장할 때, 이메일과 비밀번호 입력에 의한 컴포넌트 재렌더링 등 어떤 경우에도 'EFFECT CLEANUP' 콘솔이 찍히지 않습니다.

 

 

 

 

대신에, 예를 들어 로그인을 성공해서, 로그인 컴포넌트가 DOM에서 완전히 사라진 경우에만 클린업 함수가 실행됩니다.

 

 

 

 

반응형