useLayoutEffect와 useEffect 에 대해

희선·2025년 1월 17일

Toucheese

목록 보기
2/3
post-thumbnail

📍 구현하고자 하는 기능

회원가입 플로우를 구현하고있다.
유저가 Input에 이름과 전화번호 데이터를 입력하면 해당 값이 SessionStorage에 저장되며 간편 본인인증을 실행한다.
본인인증이 success된다면 페이지가 리다이렉트 되는데, 리다이렉트 된 페이지에서 사용자가 입력하여 저장된 SessionStorage의 Data가 useFormdefaultValues로 출력되도록 구현하고자 했다.



🚨 마주한 문제

본인 인증을 거치고 다시 리다이렉트 되었을 때 params를 읽어와서 state로 관리하는 다음 이동 버튼은 활성화가 되지만, 이름과 휴대폰 전화를 입력받는 inputdefaultValues 값으로 SessionStorage value값이 지정되지 않는 문제가를 마주했다...



💻 작성한 코드

  const { name, setSignupData } = useSignupStore();
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const [isActive, setIsActive] = useState(false);
  const storedData = SessionStorage.getItem('signup-storage');
  const parsedData = storedData ? JSON.parse(storedData) : null;
  const storageName = parsedData?.state?.name || '';
  const storagePhone = parsedData?.state?.phone || '';

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: storageName,
      phone: storagePhone,
    },
  });

  useEffect(() => {
    if (searchParams.get('success')) {
      setIsActive(true);
      setSignupData({ name: storageName, phone: storagePhone });
    }
  }, []);

코드를 보고 react-hook-form을 잘못 사용 하고 있는 것 같다는 생각이 들었다!그래서 react-hook-form 공식 문서를 찾아보니 reset 함수를 추가했어야 했던 실수를 발견!



⭐️ react-hook-form reset의 역할

reset 함수는 react-hook-form에서 폼을 초기화하거나 다시 설정할 때 사용된다. reset을 사용해야 하는 이유는 주로 폼의 초기값을 외부 데이터로 설정하거나, 특정 상황에서 폼 데이터를 새롭게 갱신해야 할 때 필요한것!

그래서 나는 reset 함수를 사용하지 않았기 때문에 갱신이 되지 않고 있었던것! 공식문서를 꼼꼼히 읽어보지 않은 나의 잘못..


reset 함수는 외부 데이터가 변할 때 (ex. SessionStorage의 값이 변경되었을 때) react-hook-form의 상태를 수동으로 갱신해주는데 만약 reset을 사용하지 않으면, 외부 데이터가 업데이트되더라도 폼 값은 그대로 유지가 되는 문제가 발생한다. ( 바로 내 코드의 문제다! )



💻 reset 함수를 적용하여 수정한 코드


const {
    register,
    handleSubmit,
    formState: { errors },
  	reset // reset 추가
  } = useForm({
    defaultValues: {
      name: storageName,
      phone: storagePhone,
    },
  });

  useEffect(() => {
    if (searchParams.get('success')) {
      setIsActive(true);
      setSignupData({ name: storageName, phone: storagePhone });
      reset() // reset 함수 실행
    }
  }, []);

이렇게 reset 함수를 실행 해 주었다!
근데 함수를 실행 했음에도 불구하고 defaultValues 값은 적용되지 않았다.. 난 슬플 때 춤을 춰....




🚨 또다른 문제를 마주하다

그래서 눈에 불을 켜고 온 신경을 집중하여 실행 화면을 뜯어보았다!!

useEffect.. 화면에 그려지고 왜 다시 리렌더링 되는거지?
새로고침을 하였을 때 defaultValues로 지정되었던 값이 잠깐 보였다가
깜빡이는 현상과 함께 defaultValues 출력이 휘발되는것을 파악했다.

왜 한번 더 깜빡이는 현상이 발생하는걸까?

이걸 파악하기 위해 코드에 console 출력을 하나하나 달아보았다

💻 문제 파악을 위한 console.log 찍어보기

  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: storageName,
      phone: storagePhone,
    },
  });


  useEffect(() => {
    console.log('useEffect1');
    if (searchParams.get('success')) {
      console.log('useEffect > success');
      setIsActive(true);
      setSignupData({ name: storageName, phone: storagePhone });
      reset();
    }
    console.log('useEffect2');
  }, []);

  useEffect(() => {
    console.log('isActive');
  }, [isActive]);

  useEffect(() => {
    console.log('name');
  }, [name]);



💻 수정된 코드 실행 화면

입력한 console 값은 잘 출력된다.
위에 작성된 코드에서 도달하지 못하는 부분은 없는것으로 판단! 사용자가 입력한 값이 SessionStorage에 저장된것도 확인이 되며 console에 출력도 된다.




😐 그러면 왜 defaultValues가 적용 되었다가 깜빡이는 현상과 함께 휘발되는것인가..?

조금 더 공부해보니 useEffect의 실행 순서와 관련된 캡쳐문제라는것을 알았다


🔎 useEffect의 캡쳐

React의 클로저 작동 방식 때문에 useEffect 내부에서 참조하는 값이 해당 값의 최신 상태가 아닌, 훅이 처음 실행될 당시의 값을 참조하는 상황을 의미한다.

? 클로저(Closure)와 캡쳐(Capture)의 관계

✔️ 클로저

함수가 생성될 때 그 함수가 참조하는 외부 변수들을 함수와 함께 기억한다. 즉, 함수 내에서 외부 변수를 참조하는 경우 함수가 생성된 시점의 환경을 기억하는 것!

✔️ 캡쳐

클로저에서 외부 변수들을 참조하는 것을 캡쳐라고 부른다. 함수가 특정 값을 참조할 때 그 값을 캡쳐하여 나중에 함수가 실행될 때 참조할 수 있도록 하는것!



⛳️ useEffect의 실행 시점

내 코드에서는 useEffect가 실행되는 시점에 따라 상태 값이 예상대로 갱신되지 않고 그로 인해 reset() 함수가 호출되어 폼 값이 초기화되는 타이밍이 맞지 않는 상황!!

reset()이 호출될 때, defaultValues로 설정된 값들이 초기화되기 전에 reset이 먼저 실행되어 폼 값이 빈값으로 초기화되고, 그 이후에 useEffect에서 외부 데이터를 가져오는 순서 문제로 인해 값이 갱신되는 타이밍이 맞지 않아 발생한 문제였다.

결론

useEffect 내부에서 정의한 것은 렌더링과 동시에 실행되는것이 아니라
실행 예약 개념이라는것이다.
렌더링이 완료된 후 브라우저가 화면을 그린 후에 작업을 예약하고, 그 작업은 React가 리렌더링을 완료한 이후에 실행한다.

컴포넌트가 렌더링되고 DOM 업데이트 -> useEffect는 실행할 작업을 비동기적으로 예약
-> react가 리렌더링 실행 -> useEffect는 그 변경이 화면에 반영된 이후에 실행


useEffect는 실제로 비동기적으로 실행되기 때문에 렌더링이 완료된 후 다음 타이밍에 실행되는 방식이다. 정확히 말하면 setter가 실행 예약되고, 리렌더링이 이루어지기 전에 reset()이 호출되면서 발생한것이다.




🛠️ 해결방안

그럼 우선 해결할 수 있는 방법을 고민해보자
브라우저가 화면을 그리기 전에 setter가 업데이트 되고 reset이 실행되어야 한다는 부분에서 useEffect가 아닌 useLayoutEffect에 대해 알아보았다.



⚖️ useEffect와 useLayoutEffect

useEffectuseLayoutEffect
실행 시점렌더링 후 브라우저가 화면을 그린 뒤렌더링 후 브라우저가 화면을 그리기 전
실행 방식비동기적 (브라우저 화면 그리기 후)동기적 (브라우저 화면 그리기 전)
주요 용도비동기 작업, 데이터 가져오기, API 호출,
이벤트 리스너 등록 등
UI 레이아웃 측정, DOM 요소 크기 및 위치 조정,
화면 그리기 전에 UI 수정 등
렌더링 성능렌더링 성능에 영향을 미치지 않음크고 복잡한 작업일 경우 화면 그리기 전에
동기적으로 실행되므로 성능에 영향을 미칠 수 있음

문서로만 보았을 때는 useEffectuseLayoutEffect로 변경하면 해결 될 것이라 보인다. 하지만 렌더링 성능에서 문제가 생길 수 있지만 회원가입 플로우의 간단한 부분이라 큰 영향을 미칠 것 같지 않다는 판단에 useLayoutEffect로 코드를 수정해보았다

💻 useLayoutEffect 적용하여 수정된 코드

  useLayoutEffect(() => {
    console.log('useEffect1');
    if (searchParams.get('success')) {
      console.log('useEffect > success');
      setIsActive(true);
      setSignupData({ name: storageName, phone: storagePhone });
      reset();
    }
    console.log('useEffect2');
  }, []);

마찬가지로 실행 여부를 판단하기 위해 console.log또한 그대로 유지 한 후 테스트를 해보았다!


💻 useLayoutEffect 적용한 실행 화면

본인인증을 한 후 리렌더링이 이루어진 이후에도 SessionStorage data가 잘 적용되며 휘발되지 않는것을 확인했다! 문제 해결 완료!!
기존에도 SessionStorage data가 기본값으로 잘 적용되었지만 useEffect의 실행순서에서 꼬여버린것이라 비교적 금방 문제 해결을 할 수 있었던 것 같다




✍🏻 오늘의 회고

useEffect만 사용해봤고 useLayoutEffect는 언제 사용하는지 몰랐는데 이것때문에 이틀 내내 고민했다.. 나름 다양한 해결책을 찾아보려 노력했었던 것 같다.
클로저에 대해 항상 어려워만 했고 이론만 공부하기에는 이 개념은 어렵다 생각했는데 막상 내 코드에서 이런 문제를 현실에서 마주하니 오히려 이해못했던 이론이 깔끔하게 정리된 느낌이었다.

이제 useLayoutEffect의 역할과 장단점, useEffect와의 다른점을 배우고 사용해보았으니 앞으로 이런 로직을 구성할 때 더 다양한 방법으로 고안 할 수 있을 것 같고 문제를 다시금 마주했을때는 빠르게 해결 할 수 있을 것 같다!
오늘도 조금 더 배웠다!

profile
FE 개발자가 되기 위한 땅굴 파기! 🌱

1개의 댓글

comment-user-thumbnail
2025년 1월 17일

덕분에 깜빡임 해결했습니다! 감사해요

답글 달기