리액트 앱 로컬 머신에서 실제 서버로 옮기기
코드 작성을 마치고 테스트까지 완료했다고 하자!
코드 최적화를 위해 사용하는 지연 로딩이라는 개념을 알아두자.
모든 파일이 import 된 후에야 컴포넌트가 실행될 수 있는데, 작은 프로젝트는 상관이 없지만 프로젝트가 엄청나게 커진다면 모든 파일이 import되기 까지 시간이 걸릴 수 있다. 그러면 첫 화면 로딩이 느려져 사용자 경험에 좋지 않은 영향을 미칠 수 있다.
이럴 때 지연 로딩을 사용하여 코드를 최적화하여 필요할 때만 파일을 가져오게 할 수 있다.
홈페이지에 있을 때는 아직 블로그 페이지 코드가 필요하지 않다.
블로그 페이지가 참조하는 모든 코드들이 블로그 페이지를 방문했을 때만 불러와 지도록 지연 로딩을 추가해 보자.
블로그 페이지 컴포넌트를 함수로 정의하여 동적으로 임포트한 블로그 페이지 컴포넌트 파일을 반환하게 만들기
const BlogPage = () => import("./pages/Blog");
import 함수는 프로미스를 반환한다.
하지만 유효한 컴포넌트가 되려면 반환하는 jsx 같은 값이 있어야 한다.
리액트에서는 이런 함수를 감싸는 특수한 함수를 제공한다.
lazy()
로 함수를 감싸준다.
const BlogPage = lazy(() => import("./pages/Blog"));
lazy()는 실행될 때 동적으로 임포트하는 () => import("./pages/Blog")
이 함수를 인자로 받는다.
그러고나서 라우트의 엘리먼트를 설정할 때 리액트가 제공하는 <Suspense>
컴포넌트로 BlogPage를 감싸주면 된다.
이제 BlogPage를 컴포넌트로 사용할 수 있다.
로더에 지연 로딩을 적용하려면 로더에 함수를 할당하면 된다.
그리고 import("경로")
를 사용하여 로더를 가져오면 동적으로 필요할 때만 임포트하게 된다.
import 함수는 프로미스를 반환한다. 이 비동기 프로세스를 처리하려면 시간이 소요될 수 있기 때문이다.
async/await
사용해도 되지만 then(모듈 => 모듈.loader())
으로 로딩된 모듈(파일)을 받아서 loader함수를 적용하면 된다.
loader함수는 Promise<Response>
를 반환하므로
이렇게 작성하면 전체 loader함수가 마지막의 loader 함수에 의해 프로미스를 반환한다.
이 모든게 지연되어 로딩된다.
element: (
<Suspense fallback={<p>Loading...</p>}>
<BlogPage />
</Suspense>
),
loader: () =>
import("./pages/Blog").then((module) => module.loader()),
},
//이 import 함수는 BlogPage의 이 loader가 호출될 때만 실행된다.
//블로그 페이지를 방문할 때만 블로그 파일이 임포트되고 파일의 loader함수가 실행된다.
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// import BlogPage, { loader as postsLoader } from "./pages/Blog";
import HomePage from "./pages/Home";
import PostPage, { loader as postLoader } from "./pages/Post";
import RootLayout from "./pages/Root";
import { lazy, Suspense } from "react";
//필요할 때만 로딩하도록 lazy loading 만들기
const BlogPage = lazy(() => import("./pages/Blog"));
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "posts",
children: [
{
index: true,
element: (
//🔥 컴포넌트 lazy loading
<Suspense fallback={<p>Loading...</p>}>
<BlogPage />
</Suspense>
),
loader: () =>
import("./pages/Blog").then((module) => module.loader()),
}, //🔥 로더 lazy loading
{ path: ":id", element: <PostPage />, loader: postLoader },
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
이렇게 필요할 때만 파일을 lazy loading 할 수 있다.
백엔드에서 ✅게시물 데이터를 다운로드하는 요청은 있지만, 이 ❌컴포넌트의 코드를 다운로드하는 요청은 없음
PostPage도 지연로딩을 적용해 보면 이렇게 컴포넌트 코드가 포스트 페이지에 방문해야만, 지연되어 로딩되는 것을 확인할 수 있다.
작은 앱에서는 지연 로딩이 필요 없지만 알아 두자~
개발에 사용하는 코드 자체를 서버에 업로드 하지는 않는다.
현재 개발에 사용하는 코드는 가독성은 좋지만 브라우저에서 지원하지 않는 기능도 몇가지 사용 중이다.
JSX 코드는 브라우저에서 지원하지 않는다.
따라서 서버 업로드 전, 최종 사용자가 사용할 수 있는 프로덕션 용 코드로 변환해야 한다.
실제로 미리보기 웹 페이지도 변환된 코드로 브라우저에 보여주는 것이다.
우리가 npm start
명령어로 띄우는 개발 서버가 우리가 작성한 코드를 실시간으로 변환해주고 있다.
현재 이 프로젝트는 CRA로 생성해 뒀기 때문에 build 스크립트가 있는데, npm run build
로 프로덕션용 앱을 빌드하면 된다.
참고
올해부터인가 CRA 더 이상 권장하지 않는다고 하니 Next.js나 vite 등으로 리액트 앱을 만들자..^^ㅠ...
npm run build
명령어로 build를 하면 이 과정을 통해, 업로드할 준비가 완료된 최적화된 변환 코드 번들이 생성되어 build폴더가 생성된다.이 build 폴더에 있는 내용을 서버에 업로드하면 된다!
프로덕션용 앱을 빌드했으니 서버에 업로드하여 배포하면 된다.
웹사이트를 배포하는 데 중요한 점은 리액트 SPA가 정적 웹사이트라는 점이다.
(A React SPA(Single Page Appplication) is a "Static Website".)
서버에서 코드를 실행하는 호스팅 제공자도 따로 있는데, 예를 들어 풀스택 리액트 앱을 만든다면 그런 호스트를 사용해야 한다.
다양한 배포 및 호스팅 제공자가 있지만 간단하게 Firebase 호스팅을 사용하자. 공짜니깐..^^!
npm install -g firebase-tools
터미널 창을 열어 웹 앱의 루트 디렉터리에서 다음 명령어들을 실행한다.
firebase login
로그인 완료시 아래 창이 뜬다.
firebase init
프로젝트 설정 초기화 하면 아래 질문들이 나오는데, 질문에 대답하면서 프로젝트 초기 설정을 완료하면 된다.
? 이 디렉토리에 대해 설정할 Firebase 기능을 선택하세요. 스페이스키를 눌러 기능를 선택한 다음 Enter 키를 눌러 선택을 확인합니다.
호스팅: Firebase Hosting 및 (선택 사항) GitHub Action 배포 셋업
👉 호스팅만 진행할 예정이므로 호스팅만 선택
=== 프로젝트 셋업
먼저 이 프로젝트 디렉터리를 Firebase 프로젝트와 연결해 보겠습니다.
firebase use --add를 실행하여 여러 프로젝트 별칭을 만들 수 있습니다,
하지만 지금은 기본 프로젝트만 설정할 것입니다.
? 옵션을 선택하십시오: 기존 프로젝트 사용
? 이 디렉터리에 대한 기본 Firebase 프로젝트 선택:
react-deployment-development-e348(react-deployment-development-development)
i 프로젝트 react-deployment-demo-fe348(react-deployment-demo) 사용
👉 파이어베이스에 만들어 둔 프로젝트 중 지금 호스팅할 프로젝트를 선택
=== 호스팅 설정
당신의 프로젝트 디렉토리와 관련된 폴더인 public 디렉토리는 파이어베이스 배포와 함께 업로드할 호스팅 assets이 포함됩니다. 만약 assets에 build 프로세스가 있다면, build의 출력 디렉터리를 사용합니다.
? public 디렉토리로 사용할 디렉토리는 무엇입니까? build
👉 build 폴더에 프로덕션용 코드가 있기 때문에 build 로 설정
? SPA(단일 페이지 앱)으로 구성하시겠습니까(모든 URL을 /index.html로 다시 작성하겠습니까)? Yes
? GitHub를 사용하여 자동 빌드 및 배포를 설정하시겠습니까? No
? 파일 build/index.html이 이미 있습니다. 덮어쓰겠습니까? No
ibuild/index.html 쓰기를 건너뜁니다
구성 정보를 firebase.json에 쓰는 중...
프로젝트 정보를 .firebaser에 쓰는 중...
✔ Firebase 초기화 완료!텍스트
초기설정을 마치면 아래와 같이 파이어베이스 설정 파일이 생성된다.
firebase deploy
준비되면 웹 앱을 배포하면 된다.
배포 후, url이 생성된다. 호스팅 완료!
배포 후, Hosting 섹션에서 배포된 url에 커스텀 도메인을 추가할 수도 있다.
웹사이트를 오프라인으로 전환하여 접근할 수 없게 변경하려면 터미널에서 다음 명령어를 입력하면 된다.
firebase hosting:disable
웹사이트를 배포할 때 SPA로 설정하기 원하는지에 대한 질문이 있었는데 이 부분은 아주 중요한 부분이다.
배포한 웹사이트는 여러 페이지 간에 이동, 즉 라우트 간의 이동이 가능하다.
이는 리액트 라우터에서 제공하고 있다.
URL을 확인하고 해당하는 컴포넌트를 불러오는 과정이 브라우저에서 실행되고 있다.
그래서 react-router-dom 이라고 이름이 붙여진 것이다.
이는 클라이언트 측 패키지로, 서버에서 실행되는 것이 아니다.
이 프로젝트에는 서버에서 실행되는 코드가 없다.
네비게이션의 블로그를 클릭하면 블로그 컴포넌트로 넘어가는 로직은, 서버가 아닌 브라우저에서 실행된다.
링크를 클릭하는 대신 도메인 뒤에 /posts URL을 직접 입력해도 정상적으로 블로그 페이지로 넘어간다.
하지만 당연히 작동한다고 생각하면 안된다.
왜냐하면 실제로는, 앱을 서버에 배포하고나서, 사용자가 브라우저에 URL을 입력하면, 기술적으로 브라우저는 서버에 요청을 보내기 때문이다. 브라우저에 URL을 입력하면 언제나 웹사이트에 요청을 보내야 하므로 항상 일어나는 일이다.
서버는 요청에 응답해야 하는데, 리액트 애플리케이션을 호출하게 된다.
HTML, CSS, 앱을 구성하는 JS 코드를 응답으로 전송한다.
주 도메인이 아닌 /posts 등 경로를 추가해 요청을 보냈다면 요청에 따라 해당하는 경로가 서버에 전달된다.
그리고 기본적으로 서버는 요청한 경로에 해당하는 응답을 찾기 위해 노력한다.
하지만 당연히 서버 측에서 다양한 경로의 요청을 처리하는 로직이 없기 때문에, 해당하는 파일을 찾는데는 실패한다.
그 대신, 서버는 언제나 동일한 HTML 파일과 JS 코드를 반환하여 요청한 경로를, 클라이언트 측에서 요청한 JS 코드인 리액트 앱의 리액트 라우터로 처리하도록 만든다.
서버는 기본적으로 항상 동일한 파일을 반환하지 않는다. 기본적으로 서버는 폴더 내에서 요청에 해당하는 파일을 찾으려고 노력한다.
이런 이유 때문에, SPA로 설정할지 물어보는 질문이 있다.
따라서 현재 이 앱이 잘 작동되고는 있지만, SPA로 설정되어 있다는 사실을 꼭 기억해야 한다.
만약 다른 호스팅 제공자를 사용하게 될 경우, SPA로 설정하지 물어보지 않을 수도 있는데 그럴 경우 모든 요청에 대해 index.html을 반환하도록 직접 리디렉션 규칙을 설정해야 한다.
클라이언트 측 라우팅 및 서버 측 라우팅의 차이를 잘 알아두자~!