본문 바로가기

javascript/react

[React] useEffect Hook

특정 프로그램을 만드는 경우를 생각해보자. 리액트에 의해 관리되는 State을 이용하여 표현할 수 있는 부분도 존재하지만, 특정 데이터를 가져오거나 동작을 구독 방식으로 ( 특정 이벤트에 반응하여 작업 수행 ) 구현하는 경우 등의 업무는 State 관리만으로는 처리가 불가능하거나 매우 어렵다. 이렇듯 리액트는 컴포넌트의 렌더링과 관계없는 영역인 Side Effect 까지 State로 관리할 수는 없는데, 이런 부분을 관리하기 위해서 등장한 hook이 useEffect이다.

useState은  set~ 함수를 이용하여 State을 변화시키고, 화면의 re-rendering 을 야기한다. 비슷하게 useEffect는 State의 변화에 따라 특정 함수의 실행을 야기한다. 이때, 해당 함수는 다음과 같은 구조를 가진다.

useEffect(EffectCallBack, Dependencies : []);

const callBack = () => {
	...
};

useEffect의 인자는 2가지가 존재한다.

  • EffectCallback : Dependencies에 해당하는 값이 변했을 때 실행되는 콜백 함수. 인자와 반환 값이 없다.
  • Dependencies : useEffect의 Callback 함수 내에서 사용되는 State 변수.

useEffect hook 은 Dependencies에 전달된 State 변수들의 변화를 감지하여 Effect Callback을 호출한다. 이때 Dependencies는 배열의 구조를 취하며, 해당 배열에 사용되는 변수들이 들어가는 구조이다. 

Effect Callback은 race condition을 피하기 위해 항상 동기적으로 처리되므로, async / await 함수로 작성될 수 없다. 만약 비동기 동작이 필요하다면, 내부에서 비동기 동작을 수행하는 코드를 작성하고, 이를 처리하는 방식으로 코드를 바꿀 필요가 있다.

콜백 함수가 async일 수 없음을 알리고 있다.
필요하다면 콜백 함수 내부에서 inner function을 선언하여 사용해야 한다.
프로그램을 처음 실행하면 fetch를 통해 데이터를 가져온다.

 

useEffect는 크게 3가지 방식으로 동작할 수 있다.

  1. Dependencies로 아무것도 전달하지 않아, 모든 state 변화에 반응하게 만드는 경우
  2. Dependencies로 빈 배열을 전달하여, 맨 처음에만 실행되도록 하는 경우
  3. Dependencies를 전달하여 State들의 변화를 감지, 콜백 함수를 실행하는 경우

첫번째 방식은 거의 사용되지 않는데, 사용되는 모든 State에 대응할 일이 거의 없기 때문이다. 이 경우 EffectCallback 내부에서 state의 변화가 일어나는 코드를 작성하면, Callback 내부에서의 변화에 대응하여 계속해서 값을 변경할 수도 있기 때문에, 오히려 권장되지 않는다. 가능하다는 것만 알아두자.

두번째 방식은 웹 사이트에 처음 방문한 시점에 여러가지 코드 및 정보를 fetch 등을 통해 외부에서 가져오려고 하는 경우에 유용하다. 이 경우 Callback 안에 정의된 동작은 맨 처음에만 발생하고, 이후 새로고침을 하여 state 정보를 초기화하지 않는 이상 발생하지 않게 된다.

import { useEffect, useState } from "react"
import Item from "./Item";

const ItemList = (props) => {
    const [list, setList] = useState([]);
    useEffect(() => {
        (async () => {
            const req = await fetch('https://swapi.dev/api/people');
            const ilist = await req.json();
            console.log(ilist.results);
            setList(ilist.results);
        })();
    }, []);

    return (
        <div>
            {list.map((elem) => <Item name={elem.name} height={elem.height} key={elem.name} />)}
        </div>
    );
}

export default ItemList;

위 코드에서는 useEffect을 이용하여 웹사이트를 연 첫 순간에만 콜백 함수를 실행, swapi로부터 정보를 가져온다. 해당 콜백은 어떤 dependency도 가지지 않기 때문에 초기에만 한번 실행된다.

 

세번째 방식은 특정 state의 변화에 반응하여 콜백을 수행한다. 여기서는 내가 만든 Lock 컴포넌트를 이용하여 설명한다.

https://github.com/blaxsior/JS_Study

 

Lock 컴포넌트는 Select tag로 지정된 LockNumber 컴포넌트로 부터 특정 자리의 숫자 값을 받고, 이를 실제 비밀번호와 비교한다. 값의 비교는 현재 입력된 비밀번호를 의미하는 enteredPassword가 가리키는 값이 변경될 때 발생해야 하는데, 이를 useEffect로 구현한다.

 

const Lock = (p: { minNum: Readonly<number>, maxNum: Readonly<number>, password: Readonly<number[]> }) => {
    const [isUnlocked, setIsUnlocked] = useState<boolean | null>(null);
    const [enteredPassword, setEnteredPassword] = useState(new Array(p.password.length).fill(p.minNum));

    useEffect(() => {
        for (let i = 0; i < p.password.length; i++) {
            if (p.password[i] !== enteredPassword[i]) {
                setIsUnlocked(false);
                return;
            }
        }
        setIsUnlocked(true);
    }, [enteredPassword, p.password]);

useEffect에는 실제 비밀번호를 의미하는 p.password와 현재 입력 중인 비밀번호를 의미하는 enteredPassword가 dependencies 로 전달된다. 따라서 해당 값들이 변경되면 callback 함수가 실행된다.

 

 const valueChangeHandler = (num: number, index: number) => {
        setEnteredPassword(prev => {
            const arr = [...prev];
            arr[index] = num;
            return arr;
        })
    }

값의 변경은 LockNumber 컴포넌트에게 콜백 함수를 전달하여 lifting up 하는 방식으로 수행한다.

const LockNumber = ({ minNum, maxNum, onChange, index }: { minNum: number, maxNum: number, onChange: (arg0: number, arg1: number) => void, index: number }) => {
    const [num, setNum] = useState(minNum);

    const numChangeHandler = (e: React.ChangeEvent<HTMLSelectElement>) => {
        setNum(+(e.target.value));
        onChange(+(e.target.value), index);
    };

    const options = [];
    for (let i = minNum; i <= maxNum; i++) {
        options.push(
            <option
                value={i}
                key={`idx${index}option${i}`}>
                {i}
            </option>)
    }

    return (
        <select
            value={num}
            id = {`idx${index}Num`}
            onChange={numChangeHandler}
        >
            {options}
        </select>
    )
};

LockNumber 컴포넌트 내부에서는 자체적으로 자신의 숫자를 num이라는 state로 관리하며, 값이 변경될 때 마다 이를 위로 전달할 수 있게 select 태그의 onChange 프로퍼티에 등록된 numChangeHandler 콜백을 등록했다.

이후 다른 option을 선택할 때 마다 select의 value 값이 변경되고, 이를 트리거로 onChange에 등록된 numChangeHandler이 실행되어 Lock 컴포넌트에게 자신이 가진 숫자 정보를 반환한다.

위 코드의 결과는 다음과 같다.

해당 페이지를 처음 방문하는 경우

초기 Lock 컴포넌트의 모습. 전부 css로 구현되었다.

 

기본적으로 useEffect의 dependencies 에 특정 값이 존재하더라도 맨 처음 순간에는 callback이 한번 수행된다. 위 사진의 경우, dependency로 아무 것도 전달 받지 못한 코드에 의한 결과인데, 맨 처음 null로 지정되어 있는 isUnlocked 가 useEffect로 인해 false로 변경되어 맨 처음 한번, isUnlocked의 업데이트로 한번 callback이 실행되어 총 2번 실행되었다.

좌물쇠 번호를 변경하는 경우

 

각각의 상태가 변경됨에 따라 콜백 함수가 실행되고 있다.

좌물쇠를 푼 경우

LockNumber 컴포넌트에서의 값이 변경되면서 changeHandler이 실행되어 이와 연관된 Lock의 enteredPassword의 값이 변경된다. 이로 인해 해당 state가 dependency로 등록되어 있는 useEffect의 Callback이 실행 되고, setIsUnlocked(true) 가 수행된 결과로 re-rendering이 발생, Lock 컴포넌트에서 styles.success가 활성화 되었다.

해당 과정은 useEffect가 enteredPassword의 변화에 반응하기 때문에 가능하다.

 

+ clean-up function

사실 useEffect에 전달되는 EffectCallback은 clean-up function을 반환할 수 있다. clean-up function은 EffectCallback에서 수행될 수 있는 업무 중 초기화가 필요한 경우에 대해 해당 코드를 초기화하기 하는 내용을 담고있는 함수이다. 해당 함수는 다음번에 해당 EffectCallback이 실행되기 바로 직전에 수행된다. 즉 1번째에 EffectCallback이 실행되면, return을 통해 반환한 clean-up function은 2번째로 EffectCallback이 실행되기 직전에 실행된다.

우리가 setTimeout을 clean-up function 내부에서 사용하고 있고, 몇초 이내에 다시 렌더링이 되는 경우 clearSetTimeout을 통해 현재 활성화 되어있는 setTimeout 함수를 초기화 하고 싶을 수 있다. 이 경우, 다음과 같이 코드를 작성한다.

useEffect(() => {
	const id = setTimeout( () => {
    	... // do something
    }, 1300); // 1.3 초 이후에 내부 코드 실행
    
    return () => clearTimeout(id); // clean-up function
}, [...dependencies]);

위 코드의 경우, 1.3초 내에 다시 콜백 함수가 수행되면 이전의 setTimeout 관련 정보가 초기화된다.

결론

useEffect hook은 특정 상태 변화에 반응하여 수행해야 하는 작업이 있거나, 프로그램을 시작한 맨 처음에 수행해야 하는 작업이 있는 경우에 유용하다. 특정 변수의 변화에 따라 수행해야 하는 작업이 담긴 EffectCallback 및 변화를 감지하는 대상인 dependencies을 실인자로 받으며, 필요한 경우 clean-up function을 등록하여 작업을 초기화 하도록 구현할 수도 있다.