티스토리 뷰

프로젝트

메인 프로젝트: useEffect

네스사 2023. 7. 31. 00:54

이번 메인 프로젝트에서 많은 것을 배웠지만, 가장 큰 성과는 useEffect의 구조에 대해서 확실히 알게 되었다는 것이다. 사실 지금까지 useEffect를 많이 사용하기는 하였지만, useEffect에 대해서 깊게 생각해 본 적은 없었다. 그저 "처음 렌더링시 실행되고, 의존성 배열에 있는 상태가 바뀌면 또다시 실행된다. 추가로 return으로 원마운트시에 clean-up 함수를 지정할 수 있다"라고 알고 있었다. 물론 저 말이 틀린 것도 아니고, 실사용시에도 저 정도만 알고 있어도 된다. 그러나 가끔 무한 루프가 발생할 때가 있었는데, 이게 useEffect에서 발생한다는 것은 알았지만, 왜 그런지는 알지 못했다. 추가로 실행 시점을 제대로 알지 못해서 저번 recoil게시글에서의 오류의 원인이 되기도 했다. 

 

2023.07.28 - [메인 프로젝트] - 메인 프로젝트: recoil

 

메인 프로젝트: recoil

이제까지 redux는 전역 상태 관리의 표준이라고 할 정도로 위세를 가지고 있었다. 실제로 다른 강의나 코드 스테이츠 같은 부트캠프도 redux를 우선적으로 가르친다. 그러나 redux는 사용하기 불편

lhs9602.tistory.com

 

이번시간에는 useEffect를 다시 한번 복습하고, 추가로 참조 자료형의 상태에 대한 주의점을 함께 알아볼 것이다.

 

 

useEffect 순서


    useEffect관한 설명은 이전 글에서 했으니 생략하고, 이번  프로젝트에서 알게 된점을 위주로 서술할 것이다. useEffect에 대한 내용은 아래의 링크로 첨부한다.

2023.04.03 - [프론트엔드/React] - useEffect

 

useEffect

Hook Hook은 함수 컴포넌트에서 React state와 생명주기 기능을 연동(hook)할 수 있게 해주는 함수다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해준다. 컴포넌트 안에

lhs9602.tistory.com

    우선 처음 이야기 할 것은 useEffect가 정확히 언제 실행되는가? 이다. 대부분의 개발자들은 렌더링시 실행된다고 알고 있지만 이 렌더링이 어느 시점인지를 정확히 알아야 제대로 쓸 수 있다. 
    컴포넌트에서 예시를 들어보자. 컴포넌트는 내부에 js코드와 리턴 값으로 html을 리턴한다. 그리고 실행순서는
    js->html
      이 순서로 코드가 진행된다. 그럼 우리는 useEffect는 js코드니 js가 실행되는 타이밍에 실행되는 군! 이라고 생각할 수 있다. 그러나 useEffect는 앞써 말했듯이 "렌더링"시 실행된다. 렌더링은 화면에 출력을 의미하며, 이는 html코드라고도 할 수 있다.

따라서 useEffect는 html 이후에 실행되기에 순서는

    js->html->useEffect
    순이다.
    이걸 모르고 useEffect에 html에 영향을 주는 코드를 설정하게 한다면 html은 실행되기 전이니 제대로 실행이 된지 않는다. 이전 recoil 당시에 오류도 이런 맥락이였다.  

useEffect의 주의점


useEffect 사용시, 의외로 리렌더링이 계속 이어지는 무한 루프를 접할 수 있다. 대부분의 무한 루프는 의존성 배열에 넣은 상태를 useEffect내부에서 set함수로 변경하면서 발생한다. 대략적인 로직은 다음과 같다.

 

1. 렌더링시 useEffect가 실행되어서 set함수로 상태를 바꾼다.

2. 상태를 바꾸었기에 react가 이를 인지하고, 리렌더링을 한다.

3. 리렌더링 후, 상태를 바꾸었기에 useEffect가 다시  실행된다.

4.set함수로 또다시 상태가 변경된다.

5.react에 변경을 감지하고 다시 리렌더링을 한다.

6. 이를 계속 반복한다.

 

여기서 중요한 점은 모든 상태에서 이런 일이 일어나는 것이 아닌 참조 자료형에서만 일어난다는 것이다. 참조 자료형은 두 상태의 값이 같다라도, set함수로 변경하면 다른 값이라고 인식된다. 이는 참조 자료형은 값자체를 저장하는 것이 아닌 값이 있는 주소를 저장하기 때문이다. 

 

예시로 배열 [1]을 하나 만들어 set함수로 상태에 저장했다고 가정하자. 그 후, 다시 [1]를 만들어서 set함수에 저장한다. 두 배열 모두 [1]의 값을 가지지만, 두 배열이 저장된 주소는 다르기에 다른 배열로 취급된다. 이 때문에 무한 로딩이 발생하는 것이다.

 

때문에 참조 자료형을 useEffect에서 사용하려면, 이를 주의하고 종료조건을 설정해야 한다. 다음은 이번 프로젝트에서 해당 문제가 일어난 코드이다.

 

 const ImagesDate = useImageUpload(urls);

useEffect(() => {
      setUploadedImages(ImagesDate as File[]);
    }
  }, [ImagesDate]);

간단히 코드 소개를 하자면 useImageUpload는 url을 받아서 이미지 파일로 변환해 주는 usequeries를 가진 hook이고 ImagesDate에 리턴 값이 들어간다.  setUploadedImages는  props로 받은 uploadedImages의 set함수로 useImageUpload의 결괏값이 필요하다.

 

usequeries는 비동기 hook이고 여러장의 이미지가 들어오기에 useEffect를 사용해서 ImagesDate에 이미지가 업로드가 될 때마다 실행되게 해서 이미지를 전부 setUploadedImages에 저장한다. 의존성이 빈 배열이 아닌 이유는 초기에 ImagesDate는 아직 데이터가 오지 않아 loading상태이고, 초기 렌더링시에는 아무 데이터도 없기에 이미지가 성공적으로 업로드되어서 suecess상태일 때 데이터를 저장해야 하기 때문이다. 

 

이렇게 보면 문제가 없어 보이지만, 파일도 참조 자료형이라는 것을 잊지 말아야 한다. 대략적인 무한 루프의 로직은 다음과 같다.

 

  • UpdateImage 컴포넌트가 렌더링된다.
  • useImageUpload(urls) 함수가 실행되고, 그 결과인 ImagesDate가 생성된다.
  • useEffect 함수가 호출된다. 이때 ImagesDate가 의존성 배열에 있으므로, ImagesDate 값이 변경되면 이후에 useEffect가 다시 실행된다..
  • useEffect 내부에서 setUploadedImages(ImagesDate as File []);가 실행되면서 uploadedImages 상태가 변경된다.
  • 상태 변경으로 인해 컴포넌트가 리렌더링 된다.
  • 다시 useImageUpload(urls) 함수가 실행되고, 그 결과인 ImagesDate가 생성된다..
  • 새로 생성된 ImagesDate는 이전 렌더링에서 생성된 ImagesDate와 메모리 상의 참조가 다르므로 React는 이를 변경된 값으로 간주한다
  • 이로 인해 useEffect가 다시 실행되며, 그 내부에서 다시 setUploadedImages(ImagesDate as File []);가 실행된다.
  • 이 과정이 계속 반복되면서 무한 루프가 발생하게 된다.

자세히 보면 useImageUpload를 막으면 될 것 같지만 hook이기에 usememo, usecallback으로 감싸서 실행을 제한하는 방법은 불가능하다. 그저 이전 캐시값을 이용하는 옵션을 사용하여 불필요한 요청을 줄이는 방법밖에는 없다. 이 방법을 사용해도 새로운 값이 리턴되니, 무한 루프의 해결책은 아니다.

 

따라서 나는 useEffect에 종료 조건을 설정했다.

 

 const ImagesDate = useImageUpload(urls);
  const [prevImagesDate, setPrevImagesDate] = useState<File[]>([]);

useEffect(() => {
    if (JSON.stringify(ImagesDate) !== JSON.stringify(prevImagesDate)) {
      setUploadedImages(ImagesDate as File[]);
      setPrevImagesDate(ImagesDate as File[]);
    }
  }, [ImagesDate]);

간단히 설명하면, prevImagesDate 상태를 하나 새로 만들고 거기에도 ImagesDate를 저장한다. 그리고 useEffect 실행 시 조건문으로 JSON화 시킨 ImagesDate와 prevImagesDate 값을 비교해서, 다른 값일 때만 내부 함수를 실행시킨다. 만약 같은 값이라면 더 이상 내부 함수가 실행되지 않고, 더 이상의 리렌더링도 일어나지 않아 종료된다.

 

console.log로 찍은 내부 상황. 불필요한 렌더링이 2번쯤 발생하지만 무한 루프는 방지했다.

 

여기서 왜 uploadedImages랑 비교하지 않고 prevImagesDate를 하나 더 만들어서, 코드를 무겁게 했냐는 의문이 있을 수 있다. 그러나 이는 어쩔 수 없는 조치였다. 사실 이 뒤에 이미지를 배열에서 제거하는 코드가 있는데, 제거하면 uploadedImages와 ImagesDate의 값이 달라지기에 다시 useEffect가 실행돼서 제대로 제거가 되지 않는다. 때문에 초기 렌더링시에 저장되는 이미지를 저장하는 상태가 따로 필요했다.

 

 

앞으로의 목표


useEffect의 무한 루프를 잡기는 했지만, 아직 갈 길은 멀다. 왜냐면 아직 완벽히 렌더링을 안정시킨 것이 아니기 때문이다. 당장 위에 코드에서도 2번의 불필요한 렌더링이 있었다. 별거 아닌 것처럼 보이지만, 모든 컴포넌트에서 이런 일이 발생하면 사이트가 무거워져 성능에 문제가 생긴다. 내가 생각하는 일류 개발자의 조건 불필요한 렌더링 없이는 클린 코드를 만들 수 있냐 이기에, 이를 앞으로의 과제로 삼겠다.

'프로젝트' 카테고리의 다른 글

메인 프로젝트: recoil  (1) 2023.07.28
메인 프로젝트: react-query  (0) 2023.07.26
메인 프로젝트: 비동기 오류 중앙 제어  (0) 2023.07.04
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함