본문 바로가기
코딩 정보/React

[React] sideEffect가 무엇이고 useEffect에 대해 알아보자

by 꽁이꽁설꽁돌 2024. 7. 19.
728x90
반응형

목차

     

    Side Effect란?

    코드가 의도한 주된 효과 외에 추가적으로 발생하는 부수 효과를 말한다.

    리액트에서는 현재의 컴포넌트에 직접적인 영향을 미치지 않는 작업이라고 한다.

     

     

    현 컴포넌트 랜더링의 과정에서 필요하지만 직접적이고 즉각적으로 영향을 미치지 않는다.

     

    sideEffect 예시

    컴포넌트 함수의 주된 목적은 랜더링이 가능한 jsx 코드를 반환하는 것인데

    아래코드는 좌표를 가져오고 정렬하는 것이므로 직접적인 영향이 없어 sideEffect임을 알 수 있다.

      //side effect
      navigator.geolocation.getCurrentPosition((position) => {
        const sortedPlaces = sortPlacesByDistance(
          AVAILABLE_PLACES,
          position.coords.latitude,
          position.coords.longitude
        );
      });

     

    그래서 부수효과를 다음과 같이 작성해 보았다. 

    그런데 실행해보면 무한루프에 빠지는 것을 볼 수 있다.

    Main.jsx

    import Places from "./components/Places.jsx";
    import { AVAILABLE_PLACES } from "./data.js";
    import { sortPlacesByDistance } from "./loc.js";
    
    function Main() {
    
      const [availablePlaces, setAvailablePlaces] = useState([]);
    
      //side effect
      navigator.geolocation.getCurrentPosition((position) => {
        const sortedPlaces = sortPlacesByDistance(
          AVAILABLE_PLACES,
          position.coords.latitude,
          position.coords.longitude
        );
        //무한 루프 발생
        setAvailablePlaces(sortedPlaces);
      });
    
      return (
        <>
    
          <main>
          </main>
        </>
      );
    }
    
    export default Main;

     

    무엇이 문제일까? 바로 setAvailablePlaces를 하면서 Main함수를 다시 실행하는데

    그 과정에서 반복적인 호출이 일어나는 것이다. 그래서 이러한 문제를 해결하기 위해서 useEffect가 필요하다.

     

    useEffect란?

    매번 컴포넌트가 렌더링 될 때 특정 조건에 의존하여 수행되며,

    컴포넌트가 최대한 순수 함수를 유지할 수 있도록 도와주는 함수

     

    useEffect의 규칙

    반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안된다. 

     

    Main.jsx

    import Places from "./components/Places.jsx";
    import { AVAILABLE_PLACES } from "./data.js";
    import { sortPlacesByDistance } from "./loc.js";
    
    function Main() {
    
      const [availablePlaces, setAvailablePlaces] = useState([]);
    
      //side effect
      //실행 시점: 앱 컴포넌트의 모든 함수가 실행된 이후
      //의존성 배열이 비었을 경우 한번만 실행
      useEffect(()=>{
        navigator.geolocation.getCurrentPosition((position) => {
          const sortedPlaces = sortPlacesByDistance(
            AVAILABLE_PLACES,
            position.coords.latitude,
            position.coords.longitude
          );
         
          setAvailablePlaces(sortedPlaces);
        });
      }, []);
    
      return (
        <>
    
          <main>
          </main>
        </>
      );
    }
    
    export default Main;

     

    APP컴포넌트 안에 들어갈 다음 코드를 실행하면 무한 루프가 일어날까?

    아래 코드는 사용자가 아이템을 클릭할때 일어나기 때문에 무한 루프가 일어나지 않는다.

    따라서 모든 부수효과에 useEffect가 필요한 것은 아니다.

      function handleSelectPlace(id) {
        setPickedPlaces((prevPickedPlaces) => {
          if (prevPickedPlaces.some((place) => place.id === id)) {
            return prevPickedPlaces;
          }
          const place = AVAILABLE_PLACES.find((place) => place.id === id);
          return [place, ...prevPickedPlaces];
        });
        
    	//이 부분을 useEffect로 감싸주어야 할까?
        // -> 규칙 위반 + 어짜피 필요없음
        const storeIds = JSON.parse(localStorage.getItem("selectedPlaces") || []);
        if (storeIds.indexOf(id) === -1) {
          localStorage.setItem("selectedPlaces", JSON.stringify([id, ...storeIds]));
        }
      }

     

    그렇다면 다음의 코드는 useEffect가 필요할까?

    필요가 없다. 그 이유는 아래 코드는 동기적으로 작동하고 즉각적으로 이루어지기 때문이다.

    import Places from "./components/Places.jsx";
    import { AVAILABLE_PLACES } from "./data.js";
    import { sortPlacesByDistance } from "./loc.js";
    
    function Main() {
    
      const [pickedPlaces, setPickedPlaces] = useState([]);
      //side effect
      //실행 시점: 앱 컴포넌트의 모든 함수가 실행된 이후
      //의존성 배열이 비었을 경우 한번만 실행
      
        useEffect(() => {
        const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
        const storedPlaces = storeIds.map((id) =>
          AVAILABLE_PLACES.find((place) => id === place.id)
        );
        setPickedPlaces(storedPlaces);
    
      }, []);
      
    
      return (
        <>
    
          <main>
          </main>
        </>
      );
    }
    
    export default Main;

     

    따라서 아래와 같이 수정하는게 적절하다.

    import Places from "./components/Places.jsx";
    import { AVAILABLE_PLACES } from "./data.js";
    import { sortPlacesByDistance } from "./loc.js";
    
    //밖으로 뺴어주어 굳이 
     const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
        const storedPlaces = storeIds.map((id) =>
          AVAILABLE_PLACES.find((place) => id === place.id)
        );
        
    function Main() {
    
      const [pickedPlaces, setPickedPlaces] = useState(storedPlaces);
    
      
    
      return (
        <>
    
          <main>
          </main>
        </>
      );
    }
    
    export default Main;

     

    브라우저 API 싱크를 위한 useEffect 사용

    import { useRef, useState } from "react";
    import Modal from "./components/Modal.jsx";
    
    function Main() {
      const [modalIsOpen, setModalIsOpen] = useState(false);
      
     	function handleRemovePlace() {
        setPickedPlaces((prevPickedPlaces) =>
          prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
        );
       setModalIsOpen(false);
        const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
        localStorage.setItem(
          "selectedPlaces",
          JSON.stringify(storeIds.filter((id) => id !== selectedPlace.current))
        );
      }
    
    
    
      return (
        <>
          <Modal open={modalIsOpen}>
            <DeleteConfirmation
              onCancel={handleStopRemovePlace}
              onConfirm={handleRemovePlace}
            />
          </Modal>
    
        </>
      );
    }
    
    export default Main;

     

    아래와 같이 코드를 작성하면 오류가 난다.

    그 이유는  dialog의 초기 값이 null값이고 아직 ref로 초기화가 되기 전 이기 때문이다.

    따라서 useEffect를 통해 해결할 수 있다. useEffect는 컴포넌트 함수가 만들어진 후에 실행하기 때문이다.

     

    이해가 안간다면 아래를 참고하자
    https://be-senior-developer.tistory.com/145

     

    [React] 랜더링의 동작과 관련 개념을 자세히 알아보자

    목차 계속 공부를 하면서 여러 오류들을 직면하는데 그 중 하나가 랜더링에 관련한 것이었다.그래서 이번에 랜더링에 대해 정리해보고자 한다.  랜더링이란? 렌더링이란 React가 컴포넌트에게

    be-senior-developer.tistory.com

    import { useRef } from 'react';
    import { createPortal } from 'react-dom';
    
    function Modal({ children, open }) {
      //초기 값이 null이기 때문에 오류가 발생한다.
      const dialog = useRef();
      if(open){
        dialog.current.showModal();
      }
      else{
        dialog.current.close();
      }
    
      return createPortal(
        <dialog className="modal" ref={dialog}>
          {children}
        </dialog>,
        document.getElementById('modal')
      );
    };
    
    export default Modal;

     

    따라서 아래와 같이 수정하는 것이 올바른 코드이다.

    import { useRef } from 'react';
    import { createPortal } from 'react-dom';
    import { useEffect } from 'react';
    function Modal({ children, open }) {
    
      const dialog = useRef();
      useEffect(()=>{
        if(open){
          dialog.current.showModal();
        }
        else{
          dialog.current.close();
        }
      }, [open])
    
    
      return createPortal(
        <dialog className="modal" ref={dialog}>
          {children}
        </dialog>,
        document.getElementById('modal')
      );
    };
    
    export default Modal;

     

    useEffect와 함께 useCallback을 써야할 때

    의존성 배열이 함수일 경우

    컴포넌트의 재실행 시 함수는 객체롤 취급되어 새로 생성되면 이전 함수와는 다르다고 판단하기 때문에 무한루프에 빠질 수 있기 때문이다. 따라서 useCallback을 통해 함수를 기억해놓아 재실행을 막을 수 있다.

    import { useRef, useState } from "react";
    import Modal from "./components/Modal.jsx";
    
    function Main() {
      const [modalIsOpen, setModalIsOpen] = useState(false);
      
      //useCallback을 통해 함수를 한번만 만들어 주고 재사용한다.
       const handleRemovePlace = useCallback(function () {
        setPickedPlaces((prevPickedPlaces) =>
          prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
        );
       setModalIsOpen(false);
        const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
        localStorage.setItem(
          "selectedPlaces",
          JSON.stringify(storeIds.filter((id) => id !== selectedPlace.current))
        );
      }, []);
    
    
    
      return (
        <>
          <Modal open={modalIsOpen}>
            <DeleteConfirmation
              onCancel={handleStopRemovePlace}
              onConfirm={handleRemovePlace}
            />
          </Modal>
    
        </>
      );
    }
    
    export default Main;

     

    import { useEffect } from "react";
    export default function DeleteConfirmation({ onConfirm, onCancel }) {
      useEffect(()=>{
        const timer = setTimeout(()=>{
          console.log("SET");
          onConfirm();
        }, 3000);
        //useEffect가 작동되기 바로 직전에 실행
        //의존성이 함수일 경우 까다로움
        //main컴포넌트가 재실행되면 새로운 함수 객체를 전달 받아서 무한루프에 빠질 수 있다. 
        //함수 객체를 새로 만들어서 다르다고 판단하기 때문에 계속 실행
        return ()=>{
          console.log("cleaning up timer");
          clearTimeout(timer);
        }
    
      }, [onConfirm])
    
      return (
        <div id="delete-confirmation">
          <h2>Are you sure?</h2>
          <p>Do you really want to remove this place?</p>
          <div id="confirmation-actions">
            <button onClick={onCancel} className="button-text">
              No
            </button>
            <button onClick={onConfirm} className="button">
              Yes
            </button>
          </div>
        </div>
      );
    }

     

     

     

    반응형