영화 정보 사이트를 주제로 Context, 커스텀훅, 라우터를 실습했다.
실습하면서 배운 내용들을 정리하고자 한다.
TMDB API를 사용하여 영화 정보를 가져온다.
API KEY를 js파일 내에서 직접 선언하고 GitHub 같은 공개 저장소에 업로드되면, 보안이나 기타 등등 문제가 발생한다.
따라서 .env
(환경 변수 파일) 파일을 생성하여 민감한 데이터들을 관리하고, import.meta.env
로 접근하여 사용할 수 있다!
.env
파일은 /root 에 위치시키도록 하자.
//tmdb.js
const API_KEY = import.meta.env.VITE_TMDB_KEY;
//.env
VITE_TMDB_KEY = YOUR_API_KEY
⭐ GitHub에 프로젝트를 업로드할 때,
.gitignore
파일에.env
를 추가하여.env
파일을 Git에서 무시하도록 설정하여야 한다. 이렇게 하면 해당 파일은 Git에 의해 버전 관리되지 않고, 저장소에 푸시되지 않는다.
user
를 받는다는 가정 하에 실습하기 위해 sessionStorage에 임의의 고정 user값을 저장해놓고, user
의 유무로 사용자에가 다른 화면을 보여주도록 할 것이다.
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(() => {
const savedUser = sessionStorage.getItem("user");
return savedUser ? JSON.parse(savedUser) : null;
});
const login = (userData) => {
sessionStorage.setItem("user", JSON.stringify(userData));
setUser(userData);
};
const logout = () => {
sessionStorage.removeItem("user");
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
};
AuthContext
Context 객체를 만들고, AuthProvider
컴포넌트에서 상태와 로그인, 로그아웃 로직을 작성한다. {children}
을 통해 Context를 하위 컴포넌트에서도 사용할 수 있도록 한다.
export const useAuth = () => useContext(AuthContext);
같은 파일 내에서 Custom Hook도 정의했다. useContext로 AuthContext의 상태, 로직을 가져와 useAuth()
시 Context값을 쉽게 사용할 수 있도록 한다.
즉, useAuth()
는 AuthContext
에서 제공하는 값을 사용하기 쉽게 래핑(wrapping)한 Custom Hook이다.
Context가 준비되었으니, App.js에서 <AuthProvider>
자식 컴포넌트를 감싸 자식 컴포넌트들이 Context를 사용할 수 있도록 한다.
Context로 user 상태와 로그인, 로그아웃 로직을 사용할 수 있기에 사용자에게 보여줄 화면을 user의 유/무 조건에 따라 지정해줄 것이다.
이를 위해
NonAuthenticatedRoute
: 로그인하지 않은 사용자만 접근할 수 있는 컴포넌트를 감싸는 컴포넌트ProtectedRoute
: 로그인한 사용자만 접근할 수 있는 컴포넌트를 감싸는 컴포넌트import { Outlet, Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function NonAuthenticatedRoute() {
const { user } = useAuth();
return !user ? <Outlet /> : <Navigate to="/profile" replace />;
}
export default NonAuthenticatedRoute;
import { Outlet, Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function ProtectedRoute() {
const { user } = useAuth();
return user ? <Outlet /> : <Navigate to="/login" replace />;
}
export default ProtectedRoute;
위에서 만든 useAuth
훅을 사용해 user 상태를 쉽게 접근하고, user의 유무로 <Outlet />
(자식 라우트를 렌더링) 또는 <Navigate/>
(특정 경로로 리디렉션)컴포넌트를 return 해준다.
NonAuthenticatedRoute
는 user가 없을 때 <Outlet />
을 반환해 로그인 페이지나 비회원 전용 페이지를 렌더링하고, user가 존재하면 /profile로 리디렉션하게 될 것이고,
ProtectedRoute
는 user가 있을 때 <Outlet />
을 반환해 프로필이나 보호된 콘텐츠를 렌더링하고, user가 없으면 /login으로 리디렉션하게 될 것이다.
App.js에서 위의 두 컴포넌트를 적용해주면 된다.
const App = () => {
return (
<AuthProvider>
<Router>
<Header />
<main style={styles.mainContent}>
<Routes>
<Route path="/" element={<Home />} />
<Route element={<NonAuthenticatedRoute />}>
<Route path="/login" element={<Login />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route path="/profile" element={<Profile />} />
<Route path="/popular-movies" element={<PopularMovies />} />
<Route path="/latest-movies" element={<LatestMovies />} />
</Route>
</Routes>
</main>
</Router>
</AuthProvider>
);
};
/login 라우트 컴포넌트는 NonAuthenticatedRoute
컴포넌트로 감싸 /login으로 가기 전 NonAuthenticatedRoute
컴포넌트의 로직이 실행될 수 있도록 한다.
user 상태가 비어있는 경우, 어느 경로를 가던 /login 경로로 리다이렉션 하고 있는 것을 확인할 수 있다.
동일한 방법으로 회원 전용(user가 있는 경우) 페이지어야 하는 route 컴포넌트들은 ProtectedRoute
컴포넌트로 감싸준다.
로그인 이후에는 /popular-movies와 같은 하위 route 경로들로 잘 접근이 되는 것을 확인할 수 있다.
위에서 useAuth
라는 커스텀 훅을 만들었다. 이번엔 영화 데이터를 쉽게 Fetch할 수 있는 useFetchMovies
훅을 만들어 영화 정보를 보여주는 컴포넌트에 사용할 것이다.
/hook/useFetchMovies.jsx
를 만들어 코드를 작성할 것이다.
import { useEffect, useState } from "react";
import { fetchPopularMovies, fetchLatestMovies } from "../api/tmdb";
const useFetchMovies = (type) => {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMovies = async () => {
setLoading(true);
try {
let data;
if (type === "popular") {
data = await fetchPopularMovies();
} else if (type === "now_playing") {
data = await fetchLatestMovies();
}
setMovies(data);
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
}
};
fetchMovies(); // 영화 데이터 가져오기 실행
}, [type]); // type이 변경될 때마다 다시 실행
return { movies, loading }; // 상태 반환
};
export default useFetchMovies;
useFetchMovies
훅은 type을 인자로 받는다. 내부의 fetchMovies
함수는 type 값에 따라 api 파일에서 불러온 적절한 API 호출 함수를 비동기로 호출하고, 반환된 데이터에 따라 movies와 loading 상태를 변경한다. fetchMovies
함수는 useEffect
로 감싸져 있어, type이 변경될 때마다 자동으로 실행되도록 한다.
movies, loading 상태를 return하여 다른 컴포넌트에서 이 값들을 쉽게 읽을 수 있도록 한다.
//LatestMovies.jsx
import useFetchMovies from "../hooks/useFetchMovies";
const LatestMovies = () => {
const { movies, loading } = useFetchMovies("now_playing");
if (loading) {
return <div>Loading...</div>;
}
return (
<div style={styles.container}>
<h1 style={styles.heading}>Latest Movies</h1>
<div style={styles.movieGrid}>
{movies.map((movie) => (
<div key={movie.id} style={styles.movieCard}>
<img
src={`https://image.tmdb.org/t/p/w200${movie.poster_path}`}
alt={movie.title}
style={styles.movieImage}
/>
<h2 style={styles.movieTitle}>{movie.title}</h2>
<p style={styles.movieOverview}>{movie.overview}</p>
</div>
))}
</div>
</div>
);
};
최신 영화를 보여주는 LatestMovies
컴포넌트에서 useFetchMovies
훅을 사용한 코드이다. type으로는 now_playing 전달하여 적절한 API 함수를 호출하고, movies와 loading 값에 접근하여 Loading 처리와 movies 데이터를 보여주는 화면을 적절히 return해주고 있다.
오늘은
1. 프로젝트에서 API Key값 등의 민감한 정보를 관리하는 방법과,
2. 상태 값에 따라 사용자에게 적절한 화면을 제공하는 방법,
3. Custom Hook 사용법에 대하여 실습하였다.
오늘 배운 내용들을 앞으로의 프로젝트에 적절히 활용하면 더 좋은 퀄리티의 프로젝트를 진행 할 수 있을 것 같다.
우앙 저는 저거 다 못했는데 구현을 정말 잘하셨네요!! o(^0^)o