useEffect 안에 함수를 정의하는 이유

낭만감자·2024년 12월 16일
0

TIL

목록 보기
4/5
post-thumbnail

원티드 프리온보딩 강의에서 강사님이 수강생 누군가 쓴 코드를 보고 설명을 해주시는데

“fetchData를 바깥쪽에 쓴게 아니라 안쪽에 넣어놨네요. 오 그래도 배운 분이시군요.” 라고 하셨다.

eslint 를 사용하다보면 useEffect 에서 사용하는 함수를 useEffect 외부에 정의하면 밑줄로 경고가 나오긴 하는데 그 이유에 대해서 잘 몰라서 찾아보았다.

1. 불필요한 함수 재정의 방지

공식문서의 useEffect 문서에서 ‘불필요한 함수 의존성 제거하기’ 항목에 들어가면 다음과 같이 적혀있다.

공식문서

불필요한 함수 의존성 제거하기

Effect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우, 너무 자주 실행될 수 있습니다. 예를 들어 이 Effect는 매 렌더링 후에 다시 연결됩니다. 이는 렌더링마다 createOptions 함수가 다르기 때문입니다.

```jsx
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() { // 🚩 이 함수는 재 렌더링 될 때마다 새로 생성됩니다
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }  
useEffect(() => {
    const options = createOptions(); // 함수가 Effect 안에서 사용됩니다
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
  // ...
```

리렌더링마다 함수를 처음부터 생성하는 것 그 자체로는 문제가 되지 않고, 이를 최적화할 필요는 없습니다. 그러나 이것을 Effect의 의존성으로 사용하는 경우 Effect가 리렌더링 후마다 다시 실행되게 합니다.

렌더링 중에 생성된 함수를 의존성으로 사용하는 것을 피하세요. 대신 Effect 내에서 함수를 선언하세요.

즉, 함수는 리렌더링 할 때마다 새로 생성이 되는데, 함수가 useEffect의 의존성 배열 안에 들어가있을 경우, 리렌더링 될 때마다 useEffect가 실행된다는 것. 이 방식이 비효율적이기때문에 useEffect 내부에서 함수를 정의하는 것을 권장하는 것이다.

1-1. 리렌더링이란?

컴포넌트가 다시 그려지는 상황을 말하는 것으로, 다음과 같은 상황에서 리랜더링이 발생한다.

1-2. 리렌더링이 발생하는 상황

  1. state 변경
  • useState 로 관리하는 상태 값이 변경될 경우.
  • 아래 코드에서 count 가 업데이트 되면 재렌더링
const [count, setCount] = useState(0);

const handleClick = () => setCount(count + 1);
  1. props 변경
  • 부모 컴포넌트에서 자식 컴포넌트로 전달된 props 가 변경된 경우
  • 아래 코드에서 name 이 바뀌면 재렌더링
<ChildComponent name="HyeWon" />
  1. context값 변경
  • useContext를 통해 읽고있는 context 값이 바뀔경우
  • context 값인 'light' 가 바뀔 경우 재렌더링
const ThemeContext = React.createContext('light');
  1. 부모 컴포넌트가 렌더링 될 경우
  • 부모 컴포넌트가 재렌더링 되면 그 안의 모든 자식 컴포넌트가 재렌더링된다.
  1. 강제 렌더링
  • forceUpdate 나 상태 업데이트 없이 트리거되는 특정 상황에서 강제로 재렌더링을 유발할 수 있다.

1-3. useEffect 가 실행되는 상황

1. 컴포넌트가 처음 렌더링 될 때 (Mount)

  • 컴포넌트가 화면에 처음 렌더링 될 때 useEffect 실행
useEffect(() => {
  console.log('컴포넌트가 마운트됨!');
}, []); // 빈 배열 -> 처음 렌더링 시 한 번 실행

2. 의존성 배열에 있는 값이 변경될 때 (update)

  • count 값이 변경될 경우 useEffect 실행
useEffect(() => {
  console.log('count 값이 변경됨!');
}, [count]); // count가 변경될 때만 실행

3. 컴포넌트가 사라질 경우 (UnMount)

  • clean-up 함수를 반환하면 컴포넌트가 사라질 때 실행됨
  • ex. 이벤트리스너 제거, 타이머클리어 등 .
useEffect(() => {
  console.log('컴포넌트가 마운트됨!');
  
  return () => {
    console.log('컴포넌트가 언마운트됨!');
  };
}, []); // 빈 배열 -> 마운트/언마운트 시 실행

4. 의존성 배열이 없는경우, 모든 렌더링마다 실행

useEffect(() => {
  console.log('매 렌더링 시 실행!');
}); // 의존성 배열 없음

1-4. 결론

따라서 함수와 useEffect는 각각 다음과 같은 규칙을 가지는데,

  • 함수 → 렌더링 될 때마다 생성
  • useEffect → 의존성 배열의 값이 변경될 때마다 생성

만약 useEffect의 의존성 배열에 함수를 넣는다면..

재렌더링 될 때마다 (= 함수가 생성될 때마다) useEffect가 실행되기 때문에 매우 비효율적이다.

이때, useEffect 안에 함수를 정의할 경우, useEffect가 실행 될 때에만 함수가 생성되기때문에 불필요한 함수 재정의를 방지할 수 있다.


2. 데이터 일관성 유지

useEffect 내부에 함수를 정의하면 항상 최신 상태의 props와 state를 참조할 수 있다.

2-1. stale closure 문제

자바스크립트에는 closure 라는 개념이 있다.

useEffect 는 props 나 state 의 상태 변화에 따라 실행이 되는데,

값이 업데이트 될 때마다 props, state 가 제대로 반영되지 않을 경우, 이전의 props, state 값을 참조할 가능성이 있다.

2-2. 자바스크립트 closure 의 특징

클로저란, 외부 함수 안에 내부 함수가 정의되어있는 구조를 가진다.

이때, 내부 함수는 외부 함수의 변수와 상태에 접근할 수 있다.

function outerFunction() {
  let count = 0; // 외부 함수의 변수

  function innerFunction() {
    count++; // 외부 함수의 변수를 참조 및 변경
    console.log(count);
  }

  return innerFunction; // 내부 함수를 반환
}

const closure = outerFunction(); // outerFunction 실행, closure에 innerFunction 저장
closure(); // 1  = innerFunction() 과 동일한 의미
closure(); // 2
closure(); // 3

여기서 innerFunction() 을 세번 호출하면, count 는 3이 된다.

이때 들 수 있는 의문
innerFunction 을 호출한 것인데, 그럼 count라는 변수 정의가 안된 상태(= let count =0; 이 없는 상태) 에서 count 값이 업데이트 된거잖아??

그니까 count++ 가 동작을 안 해서 결국 count 는 0인거 아니야?? 왜 count 가 3이 되는거지?

  1. count 라는 변수는 outerFuncion 이 실행될 때 메모리에 생성이 되지만,
  2. closure 의 특성 때문에 innerFunction 역시 이 변수를 참조하고, 값을 변경할 수 있으므로
  3. innerFunction 을 호출하는 것 만으로도 count 라는 변수에 접근하여 값을 변경하는 것이 가능하다.

그렇기 때문에 count 는 3이 되는 것.

2-3. 그래서 왜 closure 가 문제인가?

결과적으로 closure 의 특성을 가짐으로써 함수는

  1. 함수가 정의된 시점 (생성 시점) 의 상태나 props 를 기억하게 되어
  2. 값이 변경되어도 최신 값을 참조하지 못할 수 있다.

이 문제를 stale closure 문제라고 한다.

따라서, useEffect 내부에 함수를 정의하면 항상 최신 상태와 props를 보장하므로, 데이터 흐름이 일관적이고 안전하다.

2-4. 근데 왜 공식 문서에 따로 관련 내용이 없지?

stale closure in react 라고 구글링을 해보면 거의 2022년 글을 마지막으로 최신 글은 보이지 않는다.

  • useEffect와 같은 훅은 설계상 의존성 배열(dependency array)을 명시적으로 요구함으로써, 상태나 props가 바뀔 때 훅이 다시 실행되도록 했다.
  • 이 덕분에 stale closure 문제가 발생할 가능성이 줄어들었고, 따라서 이를 깊게 다룰 필요가 적어진 것.
  • 특히, ESLint 규칙을 사용하면 의존성 배열의 누락이나, stale closure 가능성을 자동으로 감지하고 경고를 띄워주기 때문에 도구를 통해 이 문제를 방지할 수 있다.

+) 꼭 stale closure 문제가 아니더라도, 함수를 내부에 정의하면 의존성 배열에 필요한 의존성 값들을 모두 찾아서 적지 않아도 되니까 간편하다.

profile
웹 프론트엔드 취준생 🥔

0개의 댓글

관련 채용 정보