preonboarding-internship / qna-12th

3 stars 0 forks source link

useEffect 의존성 배열 명시와 무한루프 관련 질문 #7

Closed saemileee closed 1 year ago

saemileee commented 1 year ago

안녕하세요 멘토님 :) 지난 세션 useEffect 관련한 양질의 강의를 듣고 useEffect를 제대로 사용하지 못하고 있었던 지난 날을 반성하면서 여러 케이스를 테스트 해보며 체화하고 있던 중 몇 가지 궁금한 사항이 생겨 질문 드립니다.

1. 임포트한 함수를 useEffect 내부에서 쓸 경우 의존성 배열 명시

// a.ts
export default function a (){
  return 1
}
// app.tsx
import React, { useEffect, useState } from "react";
import a from './a'

export default function App() {
  useEffect(() => {
    a()
  }, []);

  return (
    <div>
    </div>
  );
}

위의 경우 useEffect에서 사용하고 있는 a 함수는 외부에서 호출하고 있습니다.

지난 세션에서 예시로 말씀해 주신 props에서 setState를 받아 useEffect 내부에서 사용할 때 의존성 배열에 setState를 추가하는 것, 함수 컴포넌트를 바깥으로 이동시키고 사용하는 것과 동일한 맥락으로, a 함수가 정적인 함수인지 app 컴포넌트 내에서는 확인할 수 없으니 의존성 배열에 a가 추가되어야할 것 같은데, 이 경우는 오히려 a를 명시하면 린트에서 경고 문구가 뜨더라구요.. 이유가 궁금합니다.

2. 무한루프

1) 컴포넌트로 모듈화한 함수 사용 시 무한루프가 발생하지 않는 이유

// moduleFn.ts
import { useState } from "react";
export default function moduleFn() {
  function a() {
    return 1;
  }
  return { a };
}
// app.tsx
import moduleFn from './moduleFn'

export default function App() {
   const { a } = moduleFn();

  useEffect(() => {
    a()
  }, [a]);

  return ;
}

위의 경우에는 모듈화 한 a 함수만 사용하여 useEffect를 실행하고 있습니다.

이 경우 모듈 함수를 임포트 한 후 구조분해 할당으로 선언한 a 함수를 컴포넌트가 렌더링 될 때 참조비교하여 다른 함수로 인식하고 무한루프가 발생할 것으로 예상하였는데 최초 한 번 실행 후 실행되지 않고 있습니다.

이 부분에서 1) App 컴포넌트가 마운트 되며 2) useEffect는 실행되지만 3) 이후 App 컴포넌트 내에서는 변경될 상태가 없고 4) 그에 따라 App 컴포넌트를 다시 렌더링 할 이유가 없으니 effect가 재실행되지 않는건가? 생각하였습니다.

2) state를 사용하지 않고 커스텀 훅을 사용할 경우 무한루프가 발생하는 이유

// useCustomHook.ts
import { useState } from "react";
export default function useCustomHook() {
  const [state, setState] = useState(0);
  function a() {
    setState((prev) => prev + 1);
    return 1;
  }
  return { state, a };
}
// app.tsx
import React, { useEffect } from "react";
import a from './useCustomHook'

export default function App() {
    const { a } = useCustomHook();

  useEffect(() => {
    a()
  }, [a]);

  return ;
}

위 코드에서는 커스텀 훅 내부에서 a 함수state를 세팅하고 있고, a 함수만 app 컴포넌트에서 재선언하여 사용하였는데 무한루프가 발생하였습니다.

2-1) 번에서 추측한 내용을 바탕으로 위와 같은 코드에서도 App 컴포넌트 내에 변경될 상태가 없으니 커스텀 훅의 함수를 사용하더라도 최초 실행만 되고 마무리 될 줄 알았는데 위 코드에서는 무한루프가 발생하였습니다.

그래서.. 다시 한 번 실행 순서를 생각해보기로 했습니다..

다시 생각해 본 실행 순서..

2-1) 컴포넌트로 모듈화 한 함수 사용 (무한루프 X)

  1. App 컴포넌트 최초 렌더링
  2. app.tsx 에서 useEffect 내 a 함수 호출
  3. a 함수를 감싸고 있는 moduleFn 에서 변화 될 state가 없으니 moduleFn과 a 함수는 다시 생성되지 않음
  4. app.tsx에서 a 함수는 이전 값과 동일하다고 판단
  5. useEffect 미실행

2-2) 커스텀 훅으로 모듈화한 함수만 사용 (무한루프)

  1. App 컴포넌트 최초 렌더링
  2. app.tsx 에서 useEffect 내 a 함수 호출
  3. a 함수를 감싸고 있는 useCustomHook 에서 state가 변경됨감지
  4. useCustomHook 함수 재생성 (렌더링?)
  5. app.tsx에서 a 함수는 이전 값과 다르다고 판단
  6. useEffect 실행

위와 같이 추측하면 각 케이스에서 무한루프가 발생하거나 발생하지 않는 이유를 수긍할 수 있을 것 같은데요. 만약 위의 추측이 맞다면

  1. App 컴포넌트 내에서 직접적으로 쓰이는 state가 없고
  2. effect가 컴포넌트 렌더링 자체에 관여하지 않더라도
  3. useEffect가 실행되면 useEffect를 포함하고 있는 컴포넌트는 다시 렌더링 되면서 컴포넌트에 포함된 함수도 다시 생성되는 것이 맞을까요?

이렇게 생각하더라도 2-1)의 실행 순서 4번에서는 a 함수의 참조값을 비교하기 때문에 useEffect가 재실행될 것 같은데.. 그렇지 않는 이유가 정말 궁금합니다..

복잡한 머리 속을 최대한 정리해 본 질문이지만 다소 가독성이 떨어지더라도 양해 부탁드립니다. 언제나 감사드립니다 🙇

yeonuk-hwang commented 1 year ago

안녕하세요, 수업 내용을 기반으로 궁금한 부분들을 여러 실험을 통해서 체화하려고 시도하고, 질문 하는 모습이 너무 좋네요 :)

질문 사항들에 대해 하나씩 답변드리겠습니다.

1. 임포트한 함수를 useEffect 내부에서 쓸 경우 의존성 배열 명시

a는 오늘 수업에서의 ref.current를 의존성 배열에 안 넣는 이유와 동일하다고 보면 될 것 같습니다. a 함수는 App 컴포넌트의 리렌더링에 전혀 영향을 미칠 수 없는 값이기에 설령 변경이 되더라도, 리렌더링이 트리거 되지 않고, 따라서 useEffect 또한 재실행 될 수 없습니다. 따라서 a의 변경이 effect에 유의미한 영향을 미치지 못하기에, a 함수는 unnecessary denpendencies로 판단, 린트에서 경고를 출력해준다고 이해하시면 되겠습니다.

2. 무한루프

컴포넌트로 모듈화 한 함수 사용 시 무한루프가 발생하지 않는 이유

// moduleFn.ts
import { useState } from "react";
export default function moduleFn() {
  function a() {
    return 1;
  }
  return { a };
}

// app.tsx
import moduleFn from './moduleFn'

export default function App() {
   const { a } = moduleFn();

  useEffect(() => {
    a()
  }, [a]);

  return ;
}

이 코드는 최초 렌더링 이후 effect 함수를 통해서 a 함수가 호출되지만, a 함수는 내부에서 state update를 수행하지 않기에, 다음 렌더링이 발생하지 않게 되고 따라서 이 코드는 무한루프가 발생하지 않습니다.

// useCustomHook.ts
import { useState } from "react";
export default function useCustomHook() {
  const [state, setState] = useState(0);
  function a() {
    setState((prev) => prev + 1);
    return 1;
  }
  return { state, a };
}

// app.tsx
import React, { useEffect } from "react";
import a from './useCustomHook'

export default function App() {
    const { a } = useCustomHook();

  useEffect(() => {
    a()
  }, [a]);

  return ;
}

마찬가지의 이유로 위 코드는 a 함수가 state update를 수행하므로, 리렌더링이 발생, 리렌더링 이후에 a 함수 호출, 이후 state update, 렌더링이 반복되기에 무한루프에 빠진다고 볼 수 있습니다.

즉 모든 질문에서 핵심적으로 이해해야 되는 내용은 "useEffect가 호출되는 시점은 컴포넌트가 렌더링 되는 과정에서 호출된다." 입니다.

컴포넌트 렌더링과 useEffect의 흐름을 정리하자면

  1. 컴포넌트가 최초 렌더링 된다.
  2. 렌더링 과정에서(컴포넌트 함수의 본문을 실행하는 과정에서) useEffect가 호출된다.
  3. useEffect의 최초 호출과정에서 React는 effect callback을 호출하고 useEffect의 인자인 의존성 배열을 어딘가에 저장해둔다.
  4. 컴포넌트가 리렌더링 되면 렌더링 과정에서 useEffect가 다시 호출된다.
  5. useEffect가 호출되면 React는 이전의 의존성 배열과, 새로운 의존성 배열을 비교, 변경된 값이 있다면 새로운 effect callback을 호출해주고, 새로운 의존성 배열을 다음 비교를 위해서 다시 저장해둔다.

즉, state의 변경이 없어서 컴포넌트가 리렌더링 되지 않는다면 3~4에 해당하는 과정은 일어날 수 없다는 점을 이해하시면 헷갈렸던 부분들을 이해하실 수 있을겁니다.

도움이 되셨길 바라며, 추가적으로 궁금한 사항 있으시면 이어서 코멘트 달아주세요 :)

saemileee commented 1 year ago

모듈화해서 임포트한 컴포넌트의 state를 사용하지 않더라도 해당 모듈 내에서 state가 바뀌면 해당 state를 컴포넌트에 직접적으로 사용하지 않더라도 의존성 배열의 값이 변경 되었다고 인지하고 리렌더링 되는군요! 많은 도움이 되었습니다 :) 정말 감사드립니다!