개발/React&Redux

[React] 영화 추천 사이트 제작하기 (마무리)

xuwon 2024. 11. 13. 05:30

안녕하세요.
벌써 React 미니 프로젝트가 마무리 되었습니다.

5단계 미션은 배포하는 것 뿐이라서 4단계와 같이 작성할 생각입니다!

 

4단계 미션은 바로

Supabase를 이용한 회원가입, 로그인,
소셜로그인 및 북마크 기능 구현

 

입니다!
(정말... 정말 어려웠습니다. ㅜㅜ)

이번에 제가 구현한 기능은

1. 회원가입 구현하기
2. 로그인/로그아웃 구현하기
3. 소셜 로그인 (카카오) 구현하기
4. 북마크 기능 구현하기
5. netlify를 통해 배포하기

입니다!
가장 어려웠던 건... 제가 supabase 자체를 처음 써봐서 그냥 모든게 다 어려웠어요. ㅠㅠ 허엉

그래도 한 번 해봅시다......

https://supabase.com/

 

Supabase | The Open Source Firebase Alternative

Build production-grade applications with a Postgres database, Authentication, instant APIs, Realtime, Functions, Storage and Vector embeddings. Start for free.

supabase.com

supabase 홈페이지입니다!

우선 코드를 짜기 전 해야하는 것들로는

1. Supabase 대시보드에서 새 프로젝트 생성
2. 필요한 라이브러리 설치 (@supabase/supabase-js)
3. Project URL, API Key를 받아서 환경 변수에 저장

요렇게 입니다. 이렇게 하면 supabase와 로컬 서버가 연결됩니다!

https://supabase.com/docs/guides/auth/passwords?queryGroups=flow&flow=implicit

 

Password-based Auth | Supabase Docs

Allow users to sign in with a password connected to their email or phone number.

supabase.com

이메일 로그인/회원가입 정보가 담긴 공식 문서입니다.

저는 공식 문서 보고도 모르겠어서... 채찍피티에게 물어보며 했습니다 흑흑

우선 Supabase Client를 설정해야 합니다.

// supabaseClient.js

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; // .env 파일에 저장된 URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_API_KEY; // .env 파일에 저장된 API 키

const supabase = createClient(supabaseUrl, supabaseAnonKey);
  
export default supabase;

 


그리고 이제 본격적으로 함수를 작성해주면 됩니다. ㅎㅎ

 

1. 회원가입 구현하기

저는 Context API를 사용해서 전역 상태로 관리를 해주었습니다.
여러 컴포넌트에서 사용자 정보를 접근해야 할 수도 있고, 로그인 상태가 일관되게 유지되어야 하기 때문입니다!

AuthContext.jsx 파일을 만들어서 여기서 로그인, 회원가입, 로그아웃, 사용자 정보 요청 함수 등 여러 함수와 상태를 관리합니다.

 


Context API를 사용하는 방법은 다음과 같습니다.

1. Context 생성하기
  const AuthContext = createContext();  

2. Context Provider 생성하기
Provider는 Context가 공유될 컴포넌트 트리를 감싸고, 하위 컴포넌트에 데이터를 전달하는 역할을 합니다.

// 예시

import React, { useState } from 'react';
import { MyContext } from './MyContext';

export const MyProvider = ({ children }) => {
  const [state, setState] = useState("초기값");

  return (
    <MyContext.Provider value={{ state, setState }}>
      {children}
    </MyContext.Provider>
  );
};


3. 최상위 컴포넌트를 Provider로 감싸기
보통 App.jsx를 감쌉니다.

4. Context 데이터 사용하기
Context를 사용하고 싶은 컴포넌트에서 useContext를 이용해 데이터를 가져옵니다.


AuthContext 파일에는 상태로 관리해주어야 하는 값이 매우 많았습니다.
이거 먼저 정리하고 넘어갈게요.

const [isLogin, setIsLogin] = useState(false); // 로그인 유무
const [user, setUser] = useState(null); // user 정보를 관리
// 회원가입 유효성 검사에서의 error 관리
const [error, setError] = useState({ name: '', email: '', password: '', confirmPassword: '' });
// 로그인 유효성 검사에서의 error 관리
const [loginError, setLoginError] = useState({ email: '', password:'', error: ''});
// 유효성 검사가 완료되었는지?
const [isError, setIsError] = useState(false)
// navigate 함수
const navigate = useNavigate();

이렇게 많을 필요가 있나...
일단 이번 미션은 최적화고 뭐고 기능 구현에만 최대한 신경 썼습니다. ㅠㅠ
기능 구현도 못할 뻔했어서...

 

우선 회원가입 화면은 이렇게 생겼습니다.
회원가입에서 중요한 것은 유효성 검사인데요! 라이브러리가 있는 줄 모르고 저는 직접 함수를 작성했습니다 ㅠㅠ.

// 유효성 검사 함수
const validation = (name, email, password, confirmPassword) => {
        const errors = {};
        let hasError = false; // 회원가입 유효성 검사 끝났는지 확인하는 변수
    
        if (name.trim() === '') {
            errors.name = "이름을 정확히 입력해 주세요.";
            hasError = true;
        }
        if (email.trim() === '' || !email.includes('@') || !email.includes('.')) {
            errors.email = "Email을 정확히 입력해 주세요.";
            hasError = true;
        }
        if (password.length < 8) {
            errors.password = "비밀번호가 너무 짧습니다. 8자 이상으로 작성해주세요.";
            hasError = true;
        }
        if (password !== confirmPassword) {
            errors.confirmPassword = "비밀번호가 일치하지 않습니다.";
            hasError = true;
        }
    
        setError(errors); // 오류 메시지 업데이트
        setIsError(hasError); // 오류 여부 설정
    
        return hasError;
};

우선 유효성 검사는

1. 이름이 빈 값
2. 이메일이 빈 값이거나 @가 없거나 .이 없는 경우
3. 비밀번호가 8자리보다 짧은 경우
4. 비밀번호와 비밀번호 확인이 일치하지 않을 경우

이렇게 4가지 경우로 나눠서 진행하였습니다.

에러가 발생할 경우 errors 객체에 해당 내용을 담아주었습니다.
또한 hasError 플래그를 true로 두어, 유효성 검사가 완료되지 않았다는 것을 알려주었습니다.
(회원가입 함수에서 유효성 검사 함수를 호출할 것이기 때문에 유효성 검사가 완료되지 않으면 회원가입 요청을 못하도록 했습니다.)

 

// Supabase 회원가입 함수

const signUp = async (name, email, password, confirmPassword) => {
        // 유효성 검사
        const hasValidationError = validation(name, email, password, confirmPassword);
    
        if (hasValidationError) {
            console.log('회원가입 중단');
            return; // 유효성 검사 실패 시 중단
        }
    
        // 회원가입 요청
        const { data, error } = await supabase.auth.signUp({ email, password });
    
        if (error) {
            if (error.message.includes("already registered")) {
                setError({ email: "이미 등록된 이메일입니다." });
            } else if (error.status === 400) {
                setError({ email: "유효하지 않은 이메일입니다." });
            }
        } else {
            setUser(data.user); // 필요 없음
            setError({ name: '', email: '', password: '', confirmPassword: '' });
            return true;
        }
};

회원가입 요청 함수는 async, await를 사용해서 작성했습니다.
회원가입 요청은 서버에 데이터를 보내고, 서버에서 응답을 받는 과정이 포함되기 때문에
비동기적으로 요청을 보내고 데이터를 받아오도록 했습니다.

우선 hasValidationError 변수로 유효성 검사의 결과를 불린값으로 받아옵니다.
통과면 그 뒤 로직을 진행하고, 실패면 중단합니다.

  const { data, error } = await supabase.auth.signUp({ email, password });  
통과했을 경우 회원가입 요청을 넣고, dataerror를 받아옵니다.
supabase에서 제공하는 회원가입 함수인 supabase.auth.signUp()을 사용했습니다.

data = {
  user: {
    id: "user-id",                // 고유한 사용자 ID
    email: "user@example.com",     // 가입한 이메일 주소
    created_at: "timestamp",       // 계정 생성 시간
    // 추가적인 사용자 정보가 포함될 수 있음
  },
  session: null,                    // 회원가입 시 바로 로그인하지 않으므로 일반적으로 null
}

 

여기서 data는 이런 형식으로 들어옵니다.
저는 회원가입 후 setUser(data.user)user를 업데이트 해줬는데, 생각해보니 로그인도 안했는데 정보를 저장할 필요가 없어요.
삭제해줄게요.

아무튼 요청을 보내고 dataerror를 받아오는데
error가 잡히는 2가지 경우를 받아서 유효성 검사 때와 똑같이 error에 저장해주었습니다.
(이미 등록된 이메일 or 유효하지 않은 이메일)

error가 잡히지 않을 경우 error 객체를 초기화 하고, true를 반환합니다.
(회원가입이 다 되면 모달을 띄우기 위해서입니다.)

  export const useAuth = () => useContext(AuthContext);  
이렇게 useAuth 함수를 export 해줘서 외부에서 접근할 수 있도록 합니다.

유효성 검사가 정상적으로 진행되는 모습입니다.

회원가입 후 모달도 잘 뜨는 모습입니다.

const { signUp, error, setError } = useAuth(); // signUp 함수와 에러 상태 가져오기
const [onModal, setOnModal] = useState(false);

const handleSignUp = async () => {
        const isSuccess = await signUp(name, email, password, confirmPassword);

        if (isSuccess) {
            console.log("회원가입 성공");
            setOnModal(true)
        } else {
            console.log("회원가입 실패");
        }
};

useAuth를 사용해서 signUp, error, setError를 받아옵니다.
modal을 띄우기 위해 onModal도 상태로 관리해 줍니다.

handleSignUp 함수를 생성하여 signUp 함수를 호출하고,
성공/실패를 isSuccess로 받아와서 성공하면 onModaltrue로 업데이트 해줍니다.

모달의 [X] 버튼을 누를 경우 메인 화면으로 이동하도록 했고, 로그인 버튼을 누를 경우 /login 경로로 이동하도록 했습니다.
(navigate 함수 사용)

 


이렇게 하면 회원가입은 끝입니다!

 

2. 로그인/로그아웃 구현하기

이 로그인/로그아웃 함수 역시 Context API로 전역에서 관리해 주었습니다.

 

1. 로그인

먼저 로그인입니다.

// Supabase 이메일 로그인 함수

const signIn = async (email, password) => {
        if (email.trim() === '') {
            setLoginError({ email: "이메일을 입력해주세요." });
            return;
        } else if (password.trim() === '') {
            setLoginError({ password: "비밀번호를 입력해주세요." });
            return;
        }

        const { data, error } = await supabase.auth.signInWithPassword({
            email,
            password,
        });

        if (error) {
            setLoginError({ error: "이메일 또는 비밀번호를 잘못 입력하셨습니다." });
        } else {
            setUser(data.user);
            setIsLogin(true);
            setLoginError({ error: '' })
            navigate('/');
        }
};

유효성 검사 먼저 진행해 줍니다.
로그인은 비교적 간단합니다.

1. 이메일 입력 안 했을 때
2. 비밀번호 입력 안 했을 때

이렇게 두가지만 해주면 됩니다.
이 유효성 검사에서 걸릴 경우는 회원가입 때와 마찬가지로 요청 못 보내도록 return 해줍니다.

유효성 검사에서 통과했다면 supabase.auth.signInWithPassword 함수를 사용해서 emailpassword를 전달해줍니다.
그리고 dataerror를 받아오면 됩니다.

error가 발생할 경우 loginError에 업데이트 해주고,

아닐 경우엔

1. user에 data.user 업데이트
2. isLogin을 true로 업데이트 (Nav-Bar 디자인 구분 위해)
4. loginError 초기화
5. main으로 이동

을 해주면 됩니다.

isLogintrue일 경우, Nav-Bar에 있던 기존 로그인 버튼과 회원가입 버튼이 없어집니다.

로그인하면 이렇게 변해요.


2. 로그아웃

다음은 로그아웃입니다.

// 로그아웃 함수 

const logOut = async () => {
        const { error } = await supabase.auth.signOut();
        if (error) {
            console.error('로그아웃 오류:', error.message);
        } else {
            setUser(null); // user 정보 초기화
            setIsLogin(false);
            navigate('/'); // 로그아웃 후 로그인 페이지로 이동
        }
};

 

  const { error } = await supabase.auth.signOut();  
supabase.auth.signOut 함수를 이용해서 error를 받아옵니다.

error가 발생하는 경우 에러 메시지를 출력하고,
아닌 경우엔 그냥 모든 것들을 초기화 한다고 생각하시면 됩니다.

 


이렇게 하면 로그인/로그아웃도 정상적으로 작동됩니다!

3. 소셜 로그인 (카카오) 구현하기

평소 정말 즐겨쓰던 소셜 로그인입니다.
정말 편리한데,,, 직접 구현하려니 머리가 아프네요.

https://supabase.com/docs/guides/auth/social-login/auth-kakao

 

Login with Kakao | Supabase Docs

Add Kakao OAuth to your Supabase project

supabase.com

카카오 로그인 관련 공식 문서입니다.


1. Kakao Developers에서 애플리케이션 설정

- 로그인 후 새 애플리케이션을 생성합니다.
- 플랫폼 탭으로 이동하여 웹 플랫폼을 추가하고, 애플리케이션의 Redirect URI를 등록합니다. (supabase의 Callback URL)
- REST API 키를 복사하여 Supabase Dashboard에서 설정할 때 사용합니다.



2. Supabase Dashboard에서 Kakao Provider 설정

- Authentication > Settings에서 Kakao를 OAuth Provider로 활성화합니다.
- Kakao Developers에서 복사한 REST API Key를 입력합니다.


기본 설정은 끝났으니, 로그인 함수를 구현해 보겠습니다.

useEffect(() => {
        if (!window.Kakao.isInitialized()) {
            window.Kakao.init("Javascript_key"); // 카카오 앱 키로 SDK 초기화
        }
}, []);

카카오는 우선 앱 키로 SDK를 초기화 하는 작업이 필요하다고 합니다.
그래서 useEffect를 사용해 처음 렌더링 될 때 앱 키로 초기화 될 수 있도록 했습니다.
(카카오 디벨로퍼의 JavaScript 키를 복사해왔습니다.)

// Kakao 로그인 요청 함수

const handleKakaoLogin = async () => {
        const { data, error } = await supabase.auth.signInWithOAuth({
            provider: 'kakao',
        });

        if (error) {
            console.error('Kakao 로그인 오류:', error.message);
            return;
        }

        if (data?.url) {
            window.location.href = data.url; // Kakao 인증 페이지로 리디렉션
        }
};

로그인 함수입니다.

supabase.auth.signInWithOAuth 함수를 사용해서 provider 키값으로 kakao를 전달했습니다.
그리고 data를 정상적으로 받아와서 data.url이 존재할 경우, 리디렉션 될 수 있도록 작성했습니다.
(위에서 설정한 Redirect URI로 이동합니다.)

  <Route path=' Redirect URI ' element={<AuthCallback />} />  
이동하려면 라우트 설정 먼저 해야겠죠.

이렇게 하면, 카카오 로그인 요청 시 리디렉션 되어 AuthCallback 컴포넌트로 넘어갑니다.

// AuthCallback.jsx

import { useEffect } from 'react';
import { useAuth } from './AuthContext';

const AuthCallback = () => {
    const { handleAuthCallback } = useAuth();

    useEffect(() => { 
        handleAuthCallback();
    }, []);

    return <></>;
};

export default AuthCallback;

특별한 건 없고, handleAuthCallbackAuthContext에서 받아서 호출하는 코드입니다.

// 로그인 후 콜백에서 세션 정보 설정 함수

const handleAuthCallback = async () => {
        const { data: { session }, error } = await supabase.auth.getSession();

        if (error) {
            console.error('세션 가져오기 오류:', error.message);
            return;
        }

        if (session) {
            // 로그인 상태 업데이트
            setIsLogin(true);
            navigate('/'); // 로그인 후 메인 페이지로 이동
        }
};

supabase.auth.getSession 함수를 통해 session을 받아옵니다.
session 안에는 사용자 정보가 user로 담겨 있답니다.

정상적으로 받아왔다면 인증도 성공했고 로그인도 성공했다는 뜻이니, isLogin도 업뎃해주고 메인으로 이동합니다.

이렇게 하면 카카오 로그인도 정상적으로 동작을 합니다!
로그아웃은 이메일과 같은 함수를 사용하면 됩니다.

 

문제는...

이렇게 하면 새로고침을 할 때마다 로그인이 풀립니다.
따라서 쿠키에 저장된 session을 불러와서 로그인 상태를 유지시켜 보겠습니다.

// 페이지 로드 시 쿠키 기반 세션 복원
useEffect(() => {
    const restoreSession = async () => {
        const { data: { session } } = await supabase.auth.getSession();
        if (session) {
            setUser(session.user)
            setIsLogin(true);
        }
    };

    restoreSession();

    // 세션 상태가 변경될 때마다 로그인 상태 업데이트
    const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
        if (session) {
            setIsLogin(true);
        } else {
            setIsLogin(false);
        }
    });
	
    // 컴포넌트가 언마운트될 때 authListener를 해제
    return () => {
        authListener?.unsubscribe();
    };
}, []);

 

restoreSession 함수는 페이지가 로드될 때 쿠키에 저장된 세션을 복원하기 위한 함수입니다.

supabase.auth.getSession()을 통해 현재 로그인된 세션 정보를 가져옵니다.
로그인한 세션이 없으면 sessionnull이 됩니다.

setUser(session.user)로 로그인한 사용자 정보를 session.user로 설정합니다.
isLogintrue로 바꾸어 로그인 되었다는 것을 인식합니다.

supabase.auth.onAuthStateChange는 세션 상태가 변경될 때마다 콜백을 실행합니다.
로그인, 로그아웃 상태를 감지할 수 있습니다.

이러면 로그인, 로그아웃 시 isLogin을 업데이트할 필요가 없겠네요!
추후에 수정하도록 할게요.

 


이렇게 하면 새로고침 해도 로그인이 유지됩니다!
휴우.. 힘드네요.

 

4. 북마크 기능 구현하기

우선 supabaseTable Editorbookmark 테이블을 만들어 줍니다.
그리고 movie_id, user_id 속성을 추가합니다.

디테일 페이지에 가서 하트 버튼을 누르면, movie_iduser_id를 받아와서 bookmark 테이블에 저장하고,
마이페이지에서 불러올 때는 bookmark 테이블에서 데이터를 받아와서 출력해주면 되겠죠.

// bookmark.js

import supabase from "./supabaseClient"

// bookmark 추가
export const addBookmark = async (movieId, userId) => {
    console.log('User ID:', userId); // 확인용 콘솔 출력
    console.log('Movie ID:', movieId); // 확인용 콘솔 출력
    
    if (userId) {
        const { data, error } = await supabase
            .from('bookmark')
            .insert([{ user_id: userId, movie_id: movieId }]);
        
        if (error) {
            console.error("Error adding bookmark:", error);
        } else {
            console.log("Bookmark added:", data);
        }
    } else {
        console.error("User ID is not available");
    }
};

// bookmark 받아오기
export const fetchBookmarks = async (userId) => {
    if (userId) {
        const { data, error } = await supabase
            .from('bookmark')
            .select('*')
            .eq('user_id', userId); // userId와 일치하는 것들만
        
        return data;
    }
};

// bookmark 삭제
export const deleteBookmark = async (movieId, userId) => {
    console.log('User ID:', userId); // 확인용 콘솔 출력
    console.log('Movie ID:', movieId); // 확인용 콘솔 출력

    if (userId) {
        const { data, error } = await supabase
            .from('bookmark')
            .delete()
            .eq('user_id', userId)
            .eq('movie_id', movieId)

        console.log(data)
    }
}

 

이 함수들은 디테일 페이지에서 호출해주면 됩니다!

// MovieDetail.jsx

useEffect(() => { 
        const getBookmark = async () => {
            const data = await fetchBookmarks(user.id); // user.id로 데이터 받아오기
            setBookmarkDatas(data || []); // 데이터가 없으면 빈 배열로 설정
        }
        if (user?.id) getBookmark(); // user.id가 존재할 경우 함수 호출
}, [user]);

const handleAddBookmark = async () => { // 빈 하트를 누를 경우
        if (user?.id) {
            await addBookmark(id, user.id); // 북마크 추가
            const updatedBookmarks = await fetchBookmarks(user.id); // 추가한 데이터로 업뎃
            setBookmarkDatas(updatedBookmarks || []); 
        } else {
            alert('로그인을 해주세요.')
            navigate('/login');
        }
};

const handleDeleteBookmark = async () => { // 빨간 하트를 누를 경우
        if (user?.id) {
            await deleteBookmark(id, user.id); // 북마크 삭제
            const updatedBookmarks = await fetchBookmarks(user.id); // 삭제한 데이터로 업뎃
            setBookmarkDatas(updatedBookmarks || []);
        }
};

우선 맨 처음에 bookmark에 있는 데이터를 받아옵니다.

왜 받아오냐면, 이 데이터를 받아와서
  bookmarkDatas.some(data => data.movie_id === id)  
이 값에 따라 빨간 하트를 표시할 건지 (데이터에 있음)
빈 하트를 표시할 건지 (데이터에 없음)
결정해야 하기 때문입니다.

그리고 빈 하트를 누르면 handleAddBookmark 함수가 실행되고, bookmarkDatas가 업데이트 됩니다.
로그인이 되지 않은 경우 user.id를 찾을 수 없으니 로그인 하라는 alert 창을 띄워줍니다.

꽉찬 하트를 누르면 handleDeleteBookmark 함수가 실행되고, bookmarkDatas가 업데이트 됩니다.

이제 bookmark 데이터를 받아서 마이페이지에서 렌더링만 해주면 돼요.

// MyPage.jsx

import { useEffect, useState } from "react";
import { fetchBookmarks } from "../bookmark";
import supabase from "../supabaseClient";
import { BASE_URL, API_READ_ACCESS_TOKEN } from '../../config.js'
import MovieCard from "./MovieCard";
import '../App.scss';
import { useAuth } from "../AuthContext.jsx";

export default function MyPage({ isDark }) {
    const [userId, setUserId] = useState('');
    const [favoriteDatas, setFavoriteDatas] = useState([]);
    const [movieDetails, setMovieDetails] = useState([]);
    const { getUser, user, loginType } = useAuth();

    useEffect(() => { // user.id 받아와서 userId로 업데이트 하기
        if (!userId) {
            const getUserInfo = async () => {
                await getUser();

                if (user) {
                    setUserId(user.id);
                }
            };
            getUserInfo();
        }
    }, [userId, loginType, user]);

    useEffect(() => { // bookmark 데이터 받아오기
        const getBookmark = async () => {
            const data = await fetchBookmarks(userId);
            setFavoriteDatas(data || []); // 데이터가 없으면 빈 배열로 설정
        }
        if (userId) getBookmark();
    }, [userId]);

    useEffect(() => {
        const fetchAllFavorites = async () => {
            const requests = favoriteDatas.map(favoriteData =>
                fetch(`${BASE_URL}/movie/${favoriteData.movie_id}?language=ko-KR`, {
                    headers: {
                        Authorization: `Bearer ${API_READ_ACCESS_TOKEN}`,
                        Accept: 'application/json',
                    },
                }).then(response => {
                    if (!response.ok) {
                        throw new Error('Failed to fetch movie details');
                    }
                    return response.json();
                })
            );

            const results = await Promise.all(requests);
            setMovieDetails(results);
        };

        if (favoriteDatas.length > 0) {
            fetchAllFavorites();
        }
    }, [favoriteDatas]);

    return (
        <div className={`${isDark ? "dark" : ""} myPage w-full`}>
            <p className="mx-auto text-[30px] popularText pt-[150px] pl-[50px]">북마크</p>
            <div>
                <div className='cards' style={{
                    display: 'flex',
                    flexWrap: 'wrap',
                    justifyContent: 'center',
                    padding: '40px',
                    gap: '20px',
                    overflow: 'auto'
                }}>{movieDetails.map((movieListData) => (
                    <MovieCard key={movieListData.id} movieListData={movieListData} />
                ))}
                </div>
            </div>
        </div>
    )
}

우선 user.id를 받아와서 userId로 업데이트 하고, bookmark 테이블에서 데이터를 받아와서 favoriteDatas에 업데이트합니다.

이 데이터를 통해 영화의 정보를 보여줘야 하는데,
이 데이터에는 movie_iduser_id 밖에 없어서, API 호출을 통해 데이터를 받아옵니다.

useEffect(() => {
        const fetchAllFavorites = async () => {
            const requests = favoriteDatas.map(favoriteData =>
                fetch(`${BASE_URL}/movie/${favoriteData.movie_id}?language=ko-KR`, {
                    headers: {
                        Authorization: `Bearer ${API_READ_ACCESS_TOKEN}`,
                        Accept: 'application/json',
                    },
                }).then(response => {
                    if (!response.ok) {
                        throw new Error('Failed to fetch movie details');
                    }
                    return response.json();
                })
            );

            const results = await Promise.all(requests);
            setMovieDetails(results);
        };

        if (favoriteDatas.length > 0) {
            fetchAllFavorites();
        }
}, [favoriteDatas]);

fetchAllFavorites 함수는 favoriteDatas에 있는 모든 movie_id를 기반으로 여러 개의 영화 상세 정보를 동시에 가져오는 비동기 함수입니다. movie_id에 대해 API 요청을 보내고, 결과를 모두 수집하여 영화의 상세 정보 목록을 얻습니다.

그럼 requestsfavoriteDatas에 들어있는 영화에 대한 상세 정보가 계속 들어오게 됩니다.

  const results = await Promise.all(requests);  
를 통해 모든 영화에 대한 API 요청이 완료될 때까지 기다린 후에 다 되면 requestsresults에 할당합니다.
그리고 이 resultsmovieDetails에 들어가게 됩니다.

북마크로 추가한 데이터가 없을 수도 있으니,
length를 확인하여 favoriteDatas에 값이 하나라도 있을 때만 fetchAllFavorites 함수를 호출해줍니다.

그리고 의존성 배열로 favoriteDatas를 주어, 이 데이터가 변할 때마다 새로 데이터를 받아오면 됩니다.

이제 이 movieDetailsmap으로 순회해서 MovieCard 컴포넌트 호출해주면 끝입니다.

 


진짜 마지막 배포입니다.

 

5. Netlify를 통해 배포하기

배포하려면 카카오 디벨로퍼에 있던 도메인이랑 supabaseSite URL도 수정이 필요합니다.

이 카카오 디벨로퍼의 도메인을 배포한 사이트 도메인으로 바꿔줍니다.

supabase의 Site URL도 바꿔주면 됩니다.

이제 Netlify에서 하라는대로 깃허브 연동해서 배포하면 끝이에요

https://xumovie.netlify.app/

 

XU Movie

XU Movie: 영화 소개 페이지

xumovie.netlify.app


진짜 오래 걸린 것 같습니다...
힘들었지만 얻은게 그만큼 많은 미니 프로젝트였던 것 같슴니다.