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 |
댓글