Studying/미니 프로젝트

mini project - 영화 페이지 만들기(5단계) - 마이페이지 구현. 보호된 페이지

creamymood 2025. 5. 22. 16:27

궁금한게 자꾸.. 생겨서 챗 지피티가 100개 켜진 내 상태..

 

 

되게 간단할 것 같지만.. 또 어떤 것들이 도사리고 있을까 .. ?
하지만.. 그 또한 재밌는거야 ~


요구 조건은 다음과 같습니다.


네브바에서, 로그인 하면 썸네일이 보이도록 기능과 UI를 구현해두었다

 

앞 단계에서  url에 /mypage 라고 직접 치면 접근 로그인이 안된 상태나, 로그아웃 뒤에도 접근 할 수 있던 문제가 있었는데

이번 단계 진행 하고 나면 해결 되겠지 (두근)

 

로직은 아래 처럼 했다. 

더보기

1. 로그인 여부를 확인한다고 했으니, 앞에서 로그인 유무를 전역으로 관리한 상태인 

   useUser() 호출해서  {loggedIn}을 꺼내 쓰면 될 예정이다.

 

loggedIn이 true면, 값이 있으면, /mypage 접근 가능.

아니라면 로그인 요구 메세지와 /login 페이지로 리다이렉트.

 

2. 이렇게 조건부 렌더링 만으로도 구현할 수 있지만, 이렇게만 할 경우 url에 직접 치면 또 접근이 가능할 것 이다.

 

3. 따라서 해당 페이지들은 보호되게 관리 해야한다. 그리고 로그인 상태일 수만 접근할 수 있도록 조건을 달아주어야 한다.


정리하자면 다음과 같다.

 

  1. 전역 상태에서 로그인 여부 관리
    • useUser() 같은 커스텀 훅으로 전역 상태(예: Zustand, Recoil, Context 등)에서 loggedIn 값을 가져옴
  2. 로그인 여부에 따른 조건부 렌더링
    • loggedIn === true → 마이페이지 접근 가능
    • loggedIn === false → 로그인 필요 메시지 보여주고 /login으로 이동
  3. 조건부 렌더링만으로는 불완전
    • URL을 직접 입력하면 접근이 가능하므로, 라우터 레벨에서도 보호가 필요
  4. Protected Route 설정
    • 라우팅 자체에서 조건을 걸어줘야 진짜 보호됨
    • 인증 안 된 사용자는 해당 페이지를 렌더링하지 않도록 함

Chat GPT가 조금 더 발전된 버전과 심화된 버전을 알려줬다.

1-1. 비동기로써 로그인 여부를 확인 하기 때문에 로딩 UI를 띄워야 한다. ↓

더보기

기본적으로 네가 만든 ProtectedRoute는 이미 좋지만, 아래와 같이 발전시킬 수 있어:

1-1. "로딩 상태" 처리 추가

로그인 여부가 비동기로 확인될 수 있는 경우가 있어 (예: 서버에 토큰 유효성 검사를 요청할 때).
그럴 땐 로그인 여부를 판단하는 동안 로딩 UI를 보여줘야 해.

const { loggedIn, loading } = useUser(); // loading 상태도 함께 관리

if (loading) {
  return <div>로딩 중...</div>; // 또는 스피너 컴포넌트
}

 

> 이 때, 이렇게 직접 작성해도 되고 로딩 컴포넌트를 만들어서 로그인이 필요한 "모든 보호된 페이지"에서 처리해줘야 함.

 

 

개별적으로 일일히 페이지마다 작성해줘도 되고

const { loggedIn, loading } = useUser();

if (loading) return <LoadingSpinner />;

if (!loggedIn) {
  return <Navigate to="/login" />;
}

// 로그인된 사용자만 볼 수 있는 컴포넌트
return <MyPageContent />;

 

 

아래처럼  공통 처리 해줘도 된다. - RequireAuth 컴포넌트로 보호

매번 위 코드를 복붙하지 않기 위해 공통 RequireAuth 컴포넌트를 만들어서 감싸기!

// RequireAuth.tsx
const RequireAuth = ({ children }) => {
  const { loggedIn, loading } = useUser();

  if (loading) return <LoadingSpinner />;

  if (!loggedIn) return <Navigate to="/login" />;

  return children;
};

 

사용 예:

<Route
  path="/mypage"
  element={
    <RequireAuth>
      <MyPage />
    </RequireAuth>
  }
/>

 

 

 

1-2. 로그인 한 유저가 다시 login 페이지 보이는 것 방지 ↓

더보기

1-2. 이미 로그인한 사용자가 /login 페이지 들어오는 것도 방지

// LoginRoute.jsx
import { Navigate } from "react-router-dom";
import { useUser } from "../hooks/useUser";

const LoginRoute = ({ children }) => {
  const { loggedIn } = useUser();

  if (loggedIn) {
    return <Navigate to="/mypage" replace />;
  }

  return children;
};

이렇게 하면 이미 로그인한 사람은 로그인 페이지에 다시 접근 못 하게 막을 수 있음

 

 

위 코드가 그렇게 구현 가능한 이유

이 코드로 구현하면 이미 로그인한 사용자가 /login URL을 직접 입력해도 로그인 페이지를 볼 수 없습니다.

const LoginRoute = ({ children }) => {
  const { loggedIn } = useUser();

  if (loggedIn) {
    return <Navigate to="/mypage" replace />;
  }

  return children;
};

 

이 컴포넌트는 보통 아래처럼 /login 라우트에 사용됩니다:

<Route path="/login" element={<LoginRoute><LoginPage /></LoginRoute>} />

작동 방식:

  1. useUser() 훅에서 loggedIn 값이 true일 경우
    즉, 로그인된 사용자이면→ /mypage로 자동 리다이렉트됩니다.
  2. return <Navigate to="/mypage" replace />;
  3. loggedIn이 false이면
    즉, 비로그인 사용자이면→ 즉, <LoginPage /> 컴포넌트가 렌더링됩니다.
  4. return children;

요약

  • 이 코드면 로그인한 사용자가 /login URL을 직접 입력해도 로그인 페이지 안 보이고 자동으로 /mypage로 이동돼요.
  • /mypage는 로그인한 사용자의 목적지로 임의로 정한 경로일 뿐이에요. 필요에 따라 바꿔도 됩니다.

필요하면 /signup, /reset-password 같은 라우트도 비슷하게 보호할 수 있어요. 원하시면 예시도 드릴게요.

 

 <Navigate to="..." replace />에서 replace란?

  • 기본적으로 Navigate를 사용하면 현재 페이지를 스택에 쌓고 새 페이지로 이동해.
  • replace를 쓰면 브라우저 히스토리에서 현재 페이지를 새 페이지로 대체해.
  • 즉, 사용자가 뒤로가기 했을 때 리다이렉트된 페이지로 다시 돌아오는 걸 방지해줘.
  • 로그인 리다이렉트처럼 이전 페이지로 돌아가면 안 되는 경우에 주로 사용해.
<Navigate to="/mypage" replace />
  • replace 없으면 뒤로가기 시 로그인 페이지로 돌아감
  • replace 있으면 로그인 페이지 히스토리 기록이 없어 뒤로가기 시 이전 페이지로 이동

 

**히스토리는 없어서, 뒤로가기로는 접근이 안되지만..

wrapper로 보호 컴포넌트로 감싸줘야 url에 쳐서 접근하는 것도 막을 수 있음.

 

1-3. useEffect로 로그인 확인 후 리다이렉트 시키기 ↓

더보기

 왜 useEffect로 로그인 확인 후 리다이렉트를 할까?

로그인은 비동기적으로 처리되기 때문이에요. 예를 들어, 사용자가 페이지를 방문했을 때 로그인 상태를 바로 확인할 수 없다면, 로그인 정보가 들어온 후에야 로그인 여부를 판단하고, 그에 따라 페이지를 이동해야 해요.

그래서:

useEffect(() => {
  if (!isLoggedIn) {
    navigate('/login'); // 로그인 안 했으면 로그인 페이지로
  }
}, [isLoggedIn]);

아래는 위 useEffect 내용에 대한 구체적인 설명. 

더보기

useEffect로 로그인 확인 후 리다이렉트? 왜 쓰는 걸까?

🔍 먼저 구조를 이해해보자

React에서 렌더링은 동기적이고, useEffect는 렌더 후 실행되는 비동기 처리 영역이야.

조건부 렌더링 vs useEffect 리다이렉트

조건부 렌더링만 쓰면?

return loggedIn ? <MyPage /> : <Navigate to="/login" />;

이건 보기엔 괜찮지만, 상태가 늦게 세팅되면 로그인 안 했다고 오해해서 리다이렉트 될 수 있어.

 

그래서 useEffect를 쓰는 경우는?

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useUser } from "../hooks/useUser";

const MyPage = () => {
  const { loggedIn } = useUser();
  const navigate = useNavigate();

  useEffect(() => {
    if (!loggedIn) {
      alert("로그인이 필요합니다.");
      navigate("/login");
    }
  }, [loggedIn]);

  if (!loggedIn) return null; // 렌더링 자체도 막아줌

  return (
    <div>
      <h1>마이페이지</h1>
    </div>
  );
};
useEffect 안 쓴 것
위에서 설명한 그냥 이 파트에 useEffect로 관리만 해주는 것임

 

 

조건부 렌더링 (<Route element={loggedIn ? <Page /> : <Navigate />}) 가장 기본적인 방식 loggedIn이 항상 즉시 사용 가능할 때
useEffect 리다이렉트 상태가 비동기적으로 바뀌거나, 렌더 후 체크해야 할 때 로그인 여부를 지연 판단할 때 + UX 개선 필요할 때

 

 

 의존성 배열에는 뭘 적어야 할까?

의존성 배열에는 뭘 적어야 하지?
→ useEffect가 언제 실행될지 결정하는 부분이에요.

  • 로그인 여부만 확인하고 싶다면: [isLoggedIn]
  • 사용자 정보 객체(user)가 있다면: [user]

예시:

useEffect(() => {
  if (user && user.name) {
    console.log('유저 정보가 들어왔습니다!');
  }
}, [user]);
useEffect 사용 이유 로그인 정보가 비동기로 들어오기 때문에, 로그인 여부를 확인해 리다이렉트
사용 위치 로그인 상태를 확인할 수 있는 페이지 컴포넌트나 라우트 가드
의존성 배열 주로 isLoggedIn, 또는 user, user.id 등 로그인 관련 상태

 

 

1-4. 예외 처리: 로그인은 했지만 권한이 부족한 경우 (예: 관리자 페이지)

더보기

이건 좀.. 내 수준으로는 아직 불가 & 불필요.. 

나중에 추가 해보겠음

그냥 이런 아이디어가 있다~ 정도.


내가 생각한 로직 + 지피티의 조언으로 완성된 마이페이지 라우트 설정과, protect 설정한 코드 ↓

더보기

1. user 전역 상태 코드에서, 네트워크 요청 관련 로딩 상태 추가하기 (UserContextProvider.jsx)

(구현 내용은 아니니까, 굳이 안해도 됩니다)

 

  • 여기서 loadding: true라는 건,
    **“지금 사용자 정보를 불러오는 중이다”**를 뜻해요.
  • 이 로딩은 단순히 "마이페이지 접속 중"이 아니라,
    사용자가 로그인돼 있는지 아닌지를 확인하는 중이라는 상태~

 

이건 추후, 마이페이지 안에서 사용할 것이다.

export function UserContextProvider ({children}) {
     const [user, setUser] = useState(null)
     const {getUserInfo} = useSupabaseAuth()
     const [loggedIn, setLoggedIn] = useState(null)
     const [loadding, setLoadding] = useState(true)
   
     useEffect(() => {
      const fetchUser = async () => {
        try {
          const UserInfo = await getUserInfo();
          if (UserInfo?.user) {
            setUser(UserInfo.user); // 전역 상태에 저장
            setLoggedIn(true);
          } else {
            setUser(null);
            setLoggedIn(false); // 비로그인 상태
          }
        } catch (error) {
          console.error("유저 정보를 가져오는 중 에러 발생:", error);
          setUser(null);
          setLoggedIn(false); // 에러가 났을 때도 비로그인 처리
        } finally {
          setLoadding(false); // 무조건 로딩 종료
        }
      };
    
      fetchUser();
    }, []);
    

    return <UserContext.Provider value={{user, setUser, loggedIn, setLoggedIn, loadding, setLoadding}}>{children}</UserContext.Provider>

}

export function useUser() {
    const context = useContext(UserContext)
    if (!context) throw new Error('UserContext가 초기화되지 않았습니다.')

 

2. app.jsx에서 불러와서 useEffect 사용 하여 로그인 유무 값 바뀔 때, 렌더링 다시 하여 이동 시킬 페이지 조건 설정 (app.jsx)

 

const ProtectRoute = ({children}) => {
  const {loggedIn} = useUser()
  const navigate = useNavigate();


  useEffect(()=>{

    if(!loggedIn) {
      alert("로그인이 필요합니다")
      navigate("/login")

    }},[loggedIn, navigate])

    return children
    
  
}

 

 

3. 프로텍트 라우트로 감싸기

return (
    <>
      <Routes>
        <Route path='/' element={<Layout />}>
          <Route index element={<Home/>} />
          <Route path='/details/:id' element={<MovieDetail/>} />
          <Route path='/join' element={<Join/>} />
          <Route path='/login' element={<Login />} />
          <Route path='/search' element={<Search/>} />
          
          <Route path='/mypage' element={
            <ProtectRoute>
              <MyPage/>
            </ProtectRoute>
          }/>

        </Route>
      </Routes>

 

* Protect Route에 대한 설명은 아래와 같다. ↓ 

고정 문법은 아니고, 보호용으로 컴포넌트를 하나 만든거다.

고정 문법이나 라이브러리는 없기 때문에, 사용자 정의 컴포넌트를 만들어서 보호용으로 껍데기 하나 만들어준 것.

고정 문법은 없지만 자주 다루는 패턴이 있다고 한다. 아래에서 다뤄볼 예정

더보기

 그럼 이건 어떻게 작동할까?

<Route path='/mypage' element={
  <ProtectRoute>
    <MyPage/>
  </ProtectRoute>
}/>

이건 어떤 의미냐면:

  • element={...}는 "이 경로로 접근했을 때 어떤 컴포넌트를 보여줄지"를 의미해요.
  • 여기에 단순히 <MyPage />만 넣는 대신,
    <ProtectRoute>라는 **"보호용 껍데기 컴포넌트"**로 감싸서 전달하는 거예요.
  • 이 구조는 Higher-Order Component (HOC) 패턴과 비슷해요.

실행 순서 느낌:

  1. 사용자가 /mypage 경로로 이동함
  2. React Router는 <ProtectRoute><MyPage /></ProtectRoute>를 렌더링하려고 함
  3. ProtectRoute가 먼저 실행됨 → 로그인 여부 체크
  4. 로그인 안 되어 있으면 → alert 띄우고 /login으로 보내고, null 반환 (렌더링 안 됨)
  5. 로그인 되어 있으면 → children (즉, <MyPage />)를 렌더링함

 

protect 자주 쓰이는 공식 ↓

더보기

고정 문법은 따로 있을까?

정답: ❌ "ProtectRoute"처럼 고정된 문법은 없음!
하지만 ✅ React Router에서 <Navigate /> 같은 공식 컴포넌트를 제공해서
우리가 직접 보호용 컴포넌트를 만들 수 있게 도와줘요.


 보호 라우트를 만드는 고정 공식처럼 자주 쓰는 패턴은 있어!

예시: 가장 기본적인 보호 컴포넌트 만들기 (React Router v6 기준)

import { Navigate } from "react-router-dom";
import { useUser } from "./yourUserHook";

const RequireAuth = ({ children }) => {
  const { loggedIn } = useUser();

  if (!loggedIn) {
    return <Navigate to="/login" replace />;
  }

  return children;
};

이건 거의 **"반고정 패턴"**처럼 많이 쓰여요.
(React Router 공식 문서에도 비슷한 패턴이 나와요.)


 그럼 사용할 땐 어떻게?

<Route
  path="/mypage"
  element={
    <RequireAuth>
      <MyPage />
    </RequireAuth>
  }
/>

 

 


 

이렇게 하면 protect로 껍데기로 감싸면 보안은 괜찮아? 프론트에선 이게 최선인가? ↓

더보기

프론트엔드에서 로그인 여부를 체크하고 특정 페이지 접근을 막는 건 중요한 기능이지만,
완벽한 보안이라고 보긴 어렵고, **“사용자 경험(UX) 중심의 1차 방어선”**에 가깝다고 볼 수 있어요.


 프론트엔드 보호 방식의 한계와 역할

- 프론트에서 ProtectRoute 같은 걸 쓰는 이유는?

  • 로그인하지 않은 사용자가 UI에서만 접근을 못 하게 막음
  • UX 개선 (예: 로그인 안 한 상태로 마이페이지 누르면 자동으로 /login으로 이동)
  • 사용자가 깔끔한 흐름을 경험하게 하기 위해 필요함

-  하지만 보안 관점에서는?

  • 프론트엔드는 누구나 열어볼 수 있는 코드예요 (웹 브라우저 개발자 도구로 쉽게 열람 가능)
  • 누군가 악의적으로 코드를 조작하면 ProtectRoute는 무력화 가능
  • 예를 들어, JavaScript를 꺼버리거나 직접 API 요청을 보내버릴 수도 있어요

👩🏻‍💻그래서 진짜 보안은 어디서 이루어질까?

진짜 보안의 핵심은 백엔드!

  • 백엔드 서버에서 토큰/세션을 검사해야 해요
  • 보호된 API 요청이 들어오면:
    • "이 유저는 로그인했는가?"
    • "이 유저는 해당 리소스를 볼 수 있는 권한이 있는가?"
    • → 이걸 서버에서 무조건 체크해야 해요.

 예:

GET /mypage-data → 서버는 Authorization 헤더에 담긴 토큰을 검증함

→ 유효하지 않으면 401 Unauthorized를 리턴해버려요


그래서 프론트 보안의 현실적인 최선은?


 ProtectRoute 로그인 안 한 사용자의 UI 접근 차단
 토큰 저장 (localStorage, cookie) 인증 상태 유지
 API 호출 시 헤더에 토큰 포함 서버 인증 용도
 useEffect + navigate 로그인 안 되어 있으면 /login으로 자동 이동
 중요한 데이터 노출 ❌ 절대 프론트에서 중요한 로직(예: 관리자 체크) 직접 처리 ❌

 결론: 프론트에서의 ProtectRoute는 UX + 1차 방어선!

  • 사용자에게 “너 로그인 안 했잖아”라고 보여주는 역할
  • 진짜 보안은 항상 백엔드에서 토큰을 검증하는 로직이 필요함
  • 프론트 단에서 할 수 있는 건 "최선"이지만, "완전한 보안"은 아님

그 다음 구현 단계인, 마이페이지 화면에 로그인 한 유저의 정보 띄우기 !

마이페이지 구성한 코드 ↓

더보기

그냥 간단히 user 상태 불러와서, 이렇게 쓰면 됩니다~

 

import { useUser } from "../context/UserContextProvider";


function MyPage ( ) {
    const { user } = useUser();
   

    return(
        <>
            
                
            <p>안녕하세요, {user.userName}님!</p>
            <p>안녕하세요, {user.userName}님!</p>
            <p>안녕하세요, {user.userName}님!</p>
            <p>안녕하세요, {user.userName}님!</p>
            <p>안녕하세요, {user.userName}님!</p>


  
            


                
            
        </>
    )
}

export default MyPage

짜잔 완성! 💘 🐰

 

회색은 개인정보 가린 것


알고 있었는데 다시 공부한 부분

더보기

**useEffect는 "렌더링 이후에 실행되는 함수"**이기 때문에,
렌더링에 영향을 주는 return문은 useEffect 안에 넣을 수 없어요.


useEffect는 언제 쓰냐면:

  • navigate()
  • alert()
  • API 호출
  • 로컬 스토리지 조작 등...

➡️ 이런 "렌더링 이후의 부수 효과(side effects)" 는 useEffect에 넣는 게 맞아요.

 

리턴은, 렌더링 시 화면에 그려지는 부분!!!

 쉽게 말하면:

React에서는 컴포넌트가 먼저 렌더링(화면에 그려지는 작업) 을 해요.
그리고 나서야 useEffect() 안에 있는 코드는 실행돼요.


더 공부할 부분

1. 라우터에서 element 부분

2. <Navigate to="/login" replace />를 사용하면 navigate("/login")보다 더 깔끔하게 표현할 수 있어요.

3. React Router에서 권한 체크 로직을 "라우트 밖"에서 처리하는 방식도 있어요.
(예: createBrowserRouter 쓰는 경우)

4. 1-2. 로그인 한 유저가 다시 login 페이지 보이는 것 방지 ↓


이로써 5단계 마이페이지 부분 끝! 이제 배포를 남겨두고 있네요 ~