useEffect 정리하기(2)

    반응형

    useEffect

     

    useEffect 사용 시 주의사항


    • useEffect의 의존성 배열 관리
      useEffect의 두 번째 인자로 전달되는 의존성 배열은 이 배열의 유무, 배열의 값 존재 유무, 배열의 값 변경 여부에 따라 effect를 재 실행하기 때문에 의존성 배열의 잘못된 지정, 누락 등과 같은 실수가 있다면 컴포넌트가 렌더링 될 때마다 실행될 수 있는 문제가 발생한다.

    • 무한 루프 방지
    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          setCount(count + 1); // You want to increment the counter every second...
        }, 1000)
        return () => clearInterval(intervalId);
      }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
      // ...
    }

     

    무한 루프의 예시는 리엑트 공식 문서에 잘 나와있는데 이 코드를 살펴보면 컴포넌트가 렌더링 되고 useEffect에서 intervalId 함수가 1초 뒤에 실행된다. 그리고 clean-up 함수가 실행되는데 useEffect 내부의 의존성 배열에 count 가 있기 때문에 useEffect가 다시 실행된다. 즉 useEffect 내부에서 state를 업데이트하고 해당 상태가 의존성 배열에 포함되어 있기 때문에 "상태 업데이트 -> useEffect 실행 -> 상태 업데이트 -> useEffect 실행 ~~ " 의 무한 루프에 빠지게 된다.

     

    • 적절한 clean-up 함수 사용
      useEffect 내에서 비동기 요청, 이벤트 리스너, 타이머 설정 등과 같은 작업 수행시에는 clean-up 함수를 사용해서 메모리 누수를 방지하여 컴포넌트가 언마운트 될 때 작업을 정리해 주어야 한다.

    • useEffect Race condition 주의
      Race condition은 쉽게 말해 서로 다른 두 가지의 요청이 이루어졌을 때 Race condition(경쟁 상태)이 이루어져 어떤 요청이 먼저 처리되는지에 따라 다른 결과물을 표시하는 것을 말한다.

     

    이 외에도 useEffect는 SSR에서 수행하지 않거나, 렌더링 되기 전에 useEffect에 들어있는 상태를 업데이트할 때는 useLayoutEffet를 쓴다거나, 반복문이나 조건문 내부에서는 useEffect를 호출할 수 없다 등의 주의사항이 있다. 더 자세한 내용은 공식 문서에 나와있으니 꼭 공식 문서를 확인하면서 공부하도록 하자

     

    반응형

     

    그렇다면 내 코드의 문제점은 무엇인가?


    const router = createBrowserRouter([
      {
        path: '/',
        element: <App />,
        errorElement: <ErrorPage />,
        children: [
          { path: '/', element: <HomePage /> },
          {
            path: '',
            element: <AuthenticatedRoute />,
            children: [
              { path: 'posts', element: <PostList /> },
              { path: 'posts/:id', element: <PostDetail /> },
              { path: 'posts/edit/:id', element: <PostEdit /> },
              { path: 'posts/new', element: <NewPost /> },
              { path: 'profile', element: <ProfilePage /> },
              { path: 'profile/edit', element: <ProfileEdit /> },
              { path: 'notification', element: <NotificationPage /> },
              { path: 'search', element: <SearchPage /> },
            ]
          },
          {
            path: 'users',
            children: [
              { path: 'login', element: <Login /> },
              { path: 'signup', element: <SignUp /> }
            ]
          }
        ]
      }
    ])
    
    export default router

     

    import { Navigate, Outlet } from "react-router-dom"
    
    import { getAuth, onAuthStateChanged } from 'firebase/auth'
    import { app } from '../firebaseApp'
    
    import { useState, useEffect } from "react"
    
    
    const AuthenticatedRoute = () => {
      const auth = getAuth(app)
    
      const [isAuthenticated, setAuthenticated] = useState<boolean>(false)
    
      useEffect(() => {
        onAuthStateChanged(auth, (user) => {
          if(user) {
            setAuthenticated(true)
          } else {
            setAuthenticated(false)
          }
        })
      }, [auth])
    
      return (
        isAuthenticated ? <Outlet /> : <Navigate to='users/login' replace />
      )
    }
    
    export default AuthenticatedRoute

     

    우선 나는 Firebase를 사용했다. 사용자 인증 정보가 없다면 루트, 로그인, 회원가입 페이지만 접근이 가능하고 그 외에 페이지들은 사용자 인증정보를 가지고 있어야 접근할 수 있도록 했다. 사용자 인증 정보를 확인할 수 있도록 AuthenticatedRoute에서 사용자 인증정보를 확인하고 있다면 하위 페이지들을 없다면 로그인 페이지로 이동할 수 있도록 했는데 이 때 로그인해서 사용자 인증정보를 가지고 있어도 하위 페이지들에 접근할 수 없었고 바로 로그인 페이지로 리다이렉션이 되는 문제가 있었다. 코드의 동작 순서는 아래와 같다.

     

    • 로그인 페이지에서 로그인
    • 메인 페이지로 이동
    • AuthenticatedRoute의 children 페이지로 이동
    • 로그인 페이지로 리다이렉션

     

    우선 첫 번째 문제는 선언된 useState의 상태 값이었다. false로 선언된 useEffect는 컴포넌트가 렌더링 된 후에 동작하기 때문에 초기 상태인 false 값 때문에 바로 Navigate를 통해 로그인 페이지로 리다이렉션 되었다. 두 번째 문제는 clean-up 함수가 없다는 것이었다. onAuthStateChanged는 비동기로 동작하는 함수인데 clean-up 함수가 없기 때문에 컴포넌트가 렌더링 되고 useEffect가 동작하고 onAuthStateChanged가 동작하는데 다시 로그인 페이지로 리다이렉션 되면서 똑같은 메서드가 계속 축척되어 메모리 누수가 발생한다. 

     

    import { Navigate, Outlet } from "react-router-dom"
    
    import { getAuth, onAuthStateChanged } from 'firebase/auth'
    import { app } from '../firebaseApp'
    
    import { useState, useEffect } from "react"
    
    
    const AuthenticatedRoute = () => {
      const auth = getAuth(app)
    
      const [isAuthenticated, setAuthenticated] = useState<boolean | null>(null)
    
      useEffect(() => {
      	const unSubScribe = onAuthStateChanged(auth, (user) => {
        	setAuthenticated(!!user)
        })
        
        return () => unSubScribe()
      }, [auth])
      
      if (isAuthenticated === null) {
      	return <div className="loader">Loding...</div>
      }
    
      return (
        isAuthenticated ? <Outlet /> : <Navigate to='users/login' replace />
      )
    }
    
    export default AuthenticatedRoute

     

    수정한 코드는 이렇다. 우선 초기 useState 값을 null을 넣어 로딩 중이라고 보일 수 있도록 설정했고, useEffect 내부에 clean-up 함수를 추가하여 메모리 누수를 방지했다. 로그인 페이지를 통해 로그인하면 useEffect를 통해 unSubScribe 함수가 동작하고 동작하는 동안 화면에 로딩 중이라고 표시된다. 이후 사용자 인증이 확인되면 정상적으로 outlet을 통해 하위 페이지들로 이동할 수 있다.

    반응형

    'React' 카테고리의 다른 글

    React, ErrorBoundary 사용해서 에러 처리하는 방법  (0) 2024.07.09
    React 웹 폰트 최적화하기(2)  (0) 2024.05.02
    React 웹 폰트 최적화하기(1)  (0) 2024.05.02
    React 이해하기  (0) 2024.04.14
    useEffect 정리하기 (1)  (0) 2024.03.21

    댓글