EnJinnier

AWS Amplify 리디렉션 오류 해결 본문

자격증/AWS

AWS Amplify 리디렉션 오류 해결

공학도진니 2024. 11. 29. 02:13

 

 

지난번에 AWS Amplify로 배포한 웹사이트를 이어서 개발하던 도중 2가지 문제를 발견했다.

문제 상황 1) 404 에러

토큰이 필요없는 로그인 페이지인데 페이지 자체는 잘 불러와지는데 콘솔에 404 Not Found 에러가 뜬다.

찾아보니 

React App에는 SPA(Single Page App) 자체 라우팅이 있으며 AWS 측에서는 기본 경로를 제외한 다른 모든 경로가 서버 측에 없기 때문에 404를 반환합니다. amplify에서 index.html로 리다이렉트 설정을 해두면, react router가 라우팅을 처리합니다.

라는 말을 https://stackoverflow.com/questions/68303944/aws-amplify-reactjs-app-trouble-reloading-page 에서 볼 수 있었다.

 

문제 1 - 해결방법

이 문제의 해결은 간단했다.

AWS Amplify의 콘솔에 들어가서 

호스팅 > 다시 쓰기 및 리디렉션 > 리디렉션 관리 버튼을 눌러주고 

이 형식 부분이 기본적으로 404로 되어있는데 200으로 변경해주면 된다.

 

이후 다시 새로고침해주면 200으로 상태 변경 완료!

 

문제 상황 2) 401 에러

로그인되지 않은 상태로 다른 페이지에 접속할 시 로그인 페이지로 리디렉션 하는 코드를 작성했는데

로컬에선 잘 작동하는데 배포된 서버에서는 작동하지 않는 문제가 발생했다.

App.jsx

이렇게 작성을 하고 로그인이 아닌 나머지 페이지는 헤더에 토큰이 있어야하도록 설정했기때문에

Unauthorized 에러 발생 -> 에러 발생시 /login 페이지로 리디렉션 되어야했는데,, 

실제 운영되고 있는 서버에서 개발자 모드로 확인해보면 이렇게 

401(Unauthorized Error) 코드만 뜨고 리디렉션이 되고 있지 않았다.

 

문제 2 - 해결 방법 1

첫번째로 시도하려 한 방법은 문제1처럼 AWS에서 리디렉션 302코드를 추가해서 오류가 날 시 로그인 페이지로 리디렉트 하는 것이다.

다만 나는 로그인 페이지만 토큰 없이 접근이 가능하고 내 프로젝트의 나머지 모든 페이지는 로그인 이후에 접근 가능하도록 하고 싶었기 때문에.. 작성해야 하는 소스 주소가 너무 많았다. 

 

해결 방법 2)

리디렉션이 안되는게 코드 문제일 수도 있다고 생각해서 코드 자체를 바꾸게 되었다. 

import { Navigate, Outlet } from "react-router-dom";

// ProtectedLayout: 인증 여부를 확인하고 모든 하위 경로를 보호
const ProtectedLayout = ({ isAuthenticated }) => {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  return <Outlet />; // 인증되었으면 하위 라우트를 렌더링
};

export default ProtectedLayout;
import { Routes, Route, Navigate } from "react-router-dom";
import { requestFcmToken } from "./api/fcm";
import { useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
import { messaging } from "./api/firebase";
import { onMessage } from "firebase/messaging";
import ProtectedLayout from "./components/common/ProtectedLayout";
import LoginPage from "./pages/LoginPage/LoginPage";
import SigninPage from "./pages/LoginPage/SigninPage";
import RecordingPage from "./pages/RecordingPage/RecordingPage";
import MainPage from "./pages/MainPage/MainPage";
import FriendListPage from "./pages/FriendPage/FriendListPage";
import OAuthRedierctPage from "./pages/LoginPage/OAuthRedierctPage";
import OtherMainPage from "./pages/OtherUserPage/OtherMainPage";
import handleFcmToken from "./components/NotificationPage/handleFcmToken";
import { getUserId } from "./api/oauth";

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  useEffect(() => {
    const checkAuthentication = async () => {
      const token = localStorage.getItem("token");
      const userId = await getUserId(); // 사용자 ID 가져오는 함수
      setIsAuthenticated(!!(token && userId));
    };
    checkAuthentication();
  }, []);
  // const [hasError, setHasError] = useState(false);
  const navigate = useNavigate();
  // useEffect(() => {
  //   const handleError = (event) => {
  //     console.error("UnauthorizedError 발생:", event.message);
  //     setHasError(true);
  //   };
  //
  //   window.addEventListener("error", handleError);

  //   return () => {
  //     window.removeEventListener("error", handleError);
  //   };
  // }, []);

  // useEffect(() => {
  //   if (hasError) {
  //     navigate("/login", { replace: true });
  //   }
  // }, [hasError, navigate]);

  useEffect(() => {
    const fetchandSendFCM = async () => {
      try {
        const id = await getUserId();
        const accessToken = localStorage.getItem("token");

        if (!id || !accessToken) {
          console.error("사용자 ID 또는 액세스 토큰이 없습니다.");
          return;
        }
        await handleFcmToken(id);
      } catch (error) {
        console.error("FCM 처리 중 오류 발생:", error.message);
      }
    };

    fetchandSendFCM();
  }, []);

  useEffect(() => {
    // 포그라운드 메시지 수신 처리
    onMessage(messaging, (payload) => {
      console.log("포그라운드 메시지 수신:", payload);
    });
  }, []);

  return (
    <Routes>
      {/* 공개 경로 */}
      <Route path="/login" element={<LoginPage />} />
      <Route path="/signin" element={<SigninPage />} />
      <Route path="/api/oauth" element={<OAuthRedierctPage />} />
      <Route path="/users/null" element={<Navigate to="/login" replace />} />
      <Route path="*" element={<Navigate to="/login" replace />} />

      {/* 보호된 경로 그룹 */}
      <Route element={<ProtectedLayout isAuthenticated={isAuthenticated} />}>
        {/* 메인 네비게이션 */}
        <Route path="/" element={<Navigate to="/home" replace />} />
        <Route path="/home" element={<MainPage />} />
        <Route path="/user/:id" element={<OtherMainPage />} />

       ~~ 나머지 루트들 ~~

      </Route>
    </Routes>
  );
}

export default App;

 

 이렇게 하고 나니 갑자기 text/html 에러가 발생했다..

분석해보니 내가 문제1을 해결하는 과정에서 모든 파일을 index.html로 리디렉션 해버렸더니 css나 js파일까지

index.html로 반환되는 문제였다.

따라서 

위 리디렉션을 추가하여 assets에 들어있는 파일들은 assets로 리디렉션 되도록 했다.

다만 내가 문제1에서 먼저 index.html로 가도록 설정해버렸었기 때문에 cloudfront에 캐시가 남아있게 됐고

그게 초기화가 불가능해서 계속 index.html로 반환되는 문제가 발생했다.

사용자 지정 헤더를 추가해보거나 캐시 초기화를 강제로 해보았는데 결국 안돼서..

그냥 배포된 서버를 아예 삭제했다가 새로 생성해서 배포하는 지경까지 이르렀더니 이제 잘 된다 ^,^ ,, 

 

그리고 또 알게된 점은 리디렉션 설정시 순서가 중요하다는 것!

더 상위 목록에 있는 것이 우선적으로 적용된다. 따라서 assets를 꼭 먼저 설정해주어야 index.html로 변환되는 불상사를 막을 수 있다.

 

오류 해결 소요시간 : 약 3h

위 처리 이후 잘되는가 싶더니 

사파리 콘솔

 

크롬 콘솔

다시 위와 같은 동일한 text/html 문제가 발생했다.

-> js 파일 및 css 파일 등의 리소스 파일 사용시 접근을 못하는 에러. 주로 경로가 잘못된 것이 원인이다. 

아직 해결 진행중..

--01.07 updated

 

try1)

https://answers.netlify.com/t/failed-to-load-module-script-expected-a-javascript-module-script-but-the-server-responded-with-a-mime-type-of-text-html-strict-mime-type-checking-is-enforced-for-module-scripts-per-html-spec/122743/2

 

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Stri

It happens simply by navigating to a blog post. The first blog post for example tries to load: https://deploybranch--astounding-syrniki-fb1750.netlify.app/posts/assets/index-CdSh20_-.js You likely have an SPA _redirects rule of /* /index.html 200 So when t

answers.netlify.com

위 사이트에서는 요청 경로에 디렉토리가 하나 더 포함되어 있는 것이 문제라고 하고 있으나 나는 이미 그 경로를 고려하여 빌드 세팅에 넣어주기도 했고 실제 요청 경로도 응답 경로와 다르지 않다..

 

try2)

결국 vercel로 배포한 후 해당 프로젝트를 종료하였다.

이후 다른 프로젝트를 배포하는 과정에서 amplify를 사용했는데 이 프로젝트와 빌드구성(react+vite)이 완전히 똑같았음에도 불구하고

리디렉션도 하지 않았는데 404에러없이 잘 작동하는 것을 확인할 수 있었고, 

두 소스코드간의 차이점을 비교해보았다.

 

Point1. 

문제가 있었던 프로젝트는 로그인 페이지 외 다른 페이지에 로그인 없이 접속시 로그인 페이지로 리디렉션하도록 되어 있었다.

  return (
    <Routes>
      {/* 공개 경로 */}
      <Route path="/login" element={<LoginPage />} />
      <Route path="/signin" element={<SigninPage />} />
      <Route path="/api/oauth" element={<OAuthRedierctPage />} />
      <Route path="/users/null" element={<Navigate to="/login" replace />} />
      {/* <Route path="*" element={<Navigate to="/login" replace />} /> */}

      {/* 보호된 경로 그룹 */}
      <Route element={<ProtectedLayout isAuthenticated={isAuthenticated} />}>
        {/* 메인 네비게이션 */}
        <Route path="/" element={<Navigate to="/home" replace />} />
        <Route path="/home" element={<MainPage />} />
        <Route path="/user/:id" element={<OtherMainPage />} />

	...
      </Route>
    </Routes>
  );
}

App.jsx

import { Navigate, Outlet } from "react-router-dom";

// ProtectedLayout: 인증 여부를 확인하고 모든 하위 경로를 보호
const ProtectedLayout = ({ isAuthenticated }) => {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  return <Outlet />; // 인증되었으면 하위 라우트를 렌더링
};

export default ProtectedLayout;

ProtectedLayout.jsx

 

Point2. 새로 배포한 프로젝트는 추가적인 리디렉션 룰이 아예 없어도 작동이 됐다.

따라서 기존 프로젝트에서도 룰을 모두 지우고 재배포를 해봤다. 

처음 접속시 페이지가 뜨고, 새로고침하면 /login으로 가면서 다시 404에러가 뜨는 상황으로 변했다.

=> 처음 접속시 페이지가 로드 된다는건 파일에는 문제가 없다는 뜻. 

경로+코드 내 리디렉션 문제로 보고 

로그인 페이지를 다시 재재리디렉션을 해줬다.

그랬더니 드디어!! 제대로 동작하는걸 확인할 수 있었다.

Why?

Amplify는 내부적으로 Cloudfront와 연결되어 S3를 저장소로 사용한다.

만약 유저가 특정 파일을 요청한다면 CF를 거치고 이때 CF에 캐시가 없다면 S3로 접근하게 되는데,

S3에 해당 파일이 없어보인다면 404를 반환한다.

이때 리디렉션을 통해 강제로 경로를 지정해주면 S3에서 해당 경로에서 파일을 찾아서 보내줄 수 있다.

 

또한 text/html 오류는 css/js 파일을 알 수 있게 되면 보안상 코드를 바로 베껴서 서비스 사용이 가능하기 때문에

amplify차원에서 막아놨다고 추정하고 있다.

(해당 문제 해결은 아주 운좋게도 amplify sme이신 창기님께서 도와주셨다)

 

 

 

Reference

나와 동일한 문제를 겪어 Amplify 사용을 중단한 사례

https://johnny-mh.github.io/blog/amplify%EC%97%90%EC%84%9C-s3-cloudfront%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0/

 

Amplify에서 S3 CloudFront로 전환하기

Amplify의 불편한 점 Amplify는 웹 서비스를 개발하고 배포하기 위해 필요한 AWS의 기능들을 짜깁기한 서비스다. github을 연동하고 설정만 조금 만지면 웹 앱 하나가 뚝딱 만들어진다. S3, CloudFront를 설

johnny-mh.github.io

 

관련해서 열렸던 issue들

https://github.com/aws-amplify/amplify-cli/issues/1995

 

Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html" · Issue #1995 · aws-amplify/

Describe the bug Just added a new project and I am using production deployment using CloudFront and S3 hosting. I have an Angular project and when I try to access the deployed code I'm getting: run...

github.com

https://github.com/aws-amplify/amplify-backend/issues/1989

 

Rewrites and redirects do not work according to the documentation configuration for single pages. · Issue #1989 · aws-amplify/

Environment information N/A Describe the bug I encountered an issue with the configuration of rewrites and redirects in my single-page application (SPA). Despite following the documentation, the ro...

github.com

 

https://docs.aws.amazon.com/amplify/latest/userguide/redirects.html

 

Setting up redirects and rewrites for an Amplify application - AWS Amplify Hosting

Setting up redirects and rewrites for an Amplify application Redirects enable a web server to reroute navigation from one URL to another. Common reasons for using redirects include to customize the appearance of a URL, to avoid broken links, to move the ho

docs.aws.amazon.com

https://go-to-zeep.tistory.com/10

 

 

'자격증 > AWS' 카테고리의 다른 글

ec2 기초  (0) 2025.01.20
AWS로 프론트엔드 서버 배포하기 (feat. AWS Amplify)  (1) 2024.11.10
[SAA-C03] AWS SAA-C03 일주일컷 합격 후기  (3) 2024.09.27
[SAA-C03] Part2: EC2 (2)  (1) 2024.09.20
[SAA-C03] Part2: EC2  (3) 2024.09.16