React App Deploy(Firebase Hosting)

김동현·2022년 2월 19일
1

React

목록 보기
23/27
post-thumbnail

배포 단계

React App을 배포할 때마다 거쳐야할 단계가 존재합니다.

  1. 작성한 코드 테스팅
    : 우리는 애플리케이션을 배포하기 이전에 철저하게 테스트를 진행해야 합니다. 배포할 애플리케이션이 사용할 준비가 되었는지 확실히 해두어야 합니다.

  2. 코드 최적화
    : 우리가 작성한 코드에서 최적화할 수 있는 것들을 최적화 해줍니다.

  3. 코드를 배포환경(Production)에 맞게 빌드
    : 빌드라는 것은 작성한 코드를 최대한 작게 축소하고, 자동으로 배포 환경에 맞게 최적화를 실행합니다.

  4. 번들링한 결과물인 번들을 서버에 업로드

  5. 서버(호스트 제공자)를 설정


참고로 "호스트(Host)"란 네트워크에 연결되어 있고 애플리케이션을 실행할 수 있는 컴퓨팅 시스템을 갖춘 모든 장비들을 말합니다. 예를 들어, 컴퓨터, 스마트폰, 태블릿 등을 말하며, 라우터나 공유기는 호스트에 속하지 않습니다(컴퓨팅 시스템을 갖추고 있지 않음).

이러한 호스트 중에서 서비스를 클라이언트(브라우저)에게 제공하는 호스트를 바로 "서버(Server)"라고 합니다. 즉, 서버 또한 장치이며 네트워크와 연결되어 있으면서 서비스를 제공하는 장치들을 바로 서버라고 부릅니다.
예를 들어, 사용자가 브라우저 주소창에 URL을 입력하여 서버측에 요청을 보내면 서버는 업로드한 파일을 브라우저에게 응답으로 보냅니다.

"호스팅(hosting)"이란 서버 컴퓨터의 전체 혹은 일부를 이용할 수 있도록 임대해주는 것을 호스팅이라고 합니다. 즉, 우리는 호스팅을 이용하여 서버에 파일을 업로드할 수 있습니다.

Lazy Loading

코드 최적화를 위해서 우리는 React.memo, useCallback, useMemo 등을 사용하여 불필요한 컴포넌트 렌더링을 해결하여 성능을 향상시킬 수 있었습니다. 이것 말고 최적화를 위한 중요한 개념으로 "Lazy Loading"이 있습니다.

"Lazy Loading"이란 해당 코드가 필요한 경우에만 로딩하는 것입니다. SPA 를 사용하면 결국에는 하나의 큰 자바스크립트 번들 파일을 얻게 되고, 해당 사이트를 사용하려면 웹 사이트의 모든 사용자가 하나의 큰 번들 파일을 다운로드해야 합니다.

이는 모든 코드가 다운로드된 이후에 React App이 작동하기 때문에 웹 사이트를 사용하는 사용자는 코드가 모두 다운로드될 때까지 기다려야합니다. 따라서 우리는 다운로드되는 최초의 코드 번들을 가능한 한 작게 만들어야 합니다. 그리고 특정 코드는 필요할 경우에만 다운로드되도록 하여 사용자가 처음 페이지 로드시 기다리는 시간을 줄이도록 최적화를 진행해야 합니다.

React Route를 사용한 React App의 경우 초기 화면에보이지 않는, 즉 다른 URL 경로일 때 렌더링되는 컴포넌트들은 해당 URL 경로일 때 해당 코드를 다운로드 하도록 만들어주는 것입니다. 이것이 바로 Lazy Loading의 개념입니다. 하나의 큰 코드를 여러 덩어리로, 여러 번들로 나누고 필요한 경우에만 그에 맞는 번들을 다운로드하는 것입니다.

Route를 사용하는 경우 Lazy Loading을 구현하기 쉽습니다. Route 별로 코드를 하나의 번들로 분할해서 특정 Route에 대한 코드가 활성화될 때만 그에 맞는 번들이 다운로드되도록 만들어주면 됩니다.


// App,js
import { Routes, Route } from 'react-router-dom';

import AllQuotes from './pages/AllQuotes';
import QuoteDetail from './pages/QuoteDetail';
import NewQuote from './pages/NewQuote';
import NotFound from './pages/NotFound';

const App = () => {
    return (
        <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/quotes" element={<AllQuotes />} />
            <Route path="/quotes/:quoteId" element={<QuoteDetail />} />
            <Route path="/new-quote" element={<NewQuote />} />
            <Route path="/*" element={<NotFound />} />
        </Routes>
    );
};

export default App;

App.js 파일을 보면 우리는 Route를 통해서 해당 URL 경로일 때 렌더링되도록 했지만 import 구문으로 각 파일을 가져왔기 때문에 각 파일의 모든 코드가 미리 다운로드됩니다. 이는 코드를 번들링하는 과정(빌드)에서 import된 파일을 모두 가져와서 하나의 큰 번들 파일로 만들고 이 번들 파일을 로드해서 사용하기 때문입니다. 따라서 현재 URL 경로에서 사용되는 컴포넌트와 다른 URL일 때 사용되는 모든 코드를 다운로드하게 됩니다.

React Route를 사용하는 경우 각 Route마다 코드를 분할할 수 있습니다. react 라이브러리에서 "React.lazy 함수"를 가져옵니다. React.lazy 함수는 인수로 콜백함수를 전달받으며 반환값으로는 import 함수 호출문을 작성합니다. 그리고 import 함수를 호출할 때 인수로 가져올 파일의 경로를 전달해줍니다.
이후 해당 파일이 필요할 때 React가 lazy 함수를 호출하여 파일을 다운로드하게 됩니다.


예를 들어, AllQuote라는 컴포넌트를 불러오는 import 문을 아래와 같은 코드로 대체해줍니다.

import React from 'react';

const AllQuotes = React.lazy(() => import('./pages/AllQuotes'));

이후 URL의 경로가 /quotes일 때 해당 Route가 활성화되고 이때 React가 lazy 함수를 호출하여 import 함수의 인수로 전달한 경로의 파일을 로드한 뒤에 AllQuotes 컴포넌트를 가져오게됩니다. 하지만 이렇게만 작업을 하면 제대로 동작하지 않습니다. 그 이유는 React.lazy로 파일을 로드하는데 몇 밀리초 혹은 몇 초가 걸릴 수 있기 때문입니다.

이를 해결하기 위해서 다운로드하는 동안 표시될 대체 콘텐츠가 필요합니다. 대체 콘텐츠를 표시하기 위해서 react에서 "Suspense 컴포넌트"를 가져옵니다. Suspense 컴포넌트는 react 라이브러리 자체에서 제공되는 특수한 컴포넌트입니다.

우리는 Suspense 컴포넌트로 React.lazy가 다운로드하는 파일의 컴포넌트를 사용중인 컴포넌트들을 감싸주어야 합니다. 즉, lazy loading을 사용하는 Routes 컴포넌트를 Suspense 컴포넌트로 감싸주어야 합니다.

그리고 Suspense 컴포넌트의 fallback prop에 대체 콘텐츠로 표시될 JSX 문법을 작성해줍니다.

<Suspense fallback={<p>Loading...</p>}>
    <Routes>
    ,,,,
    </Routes>
</Suspense>

이후 Route가 활성화되고 컴포넌트가 필요할 때 React.lazy 함수가 실행되고 파일을 다운로드하는데 다운로드하는 동안에 Suspense 컴포넌트의 fallback prop에 작성한 리액트 엘리먼트가 표시되고, 파일의 다운로드가 끝나게 되면 그에 맞는 컴포넌트가 다시 화면에 렌더링됩니다.

// App,js
import React, { Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const AllQuotes = React.lazy(() => { import('./pages/AllQuotes') });
const QuoteDetail = React.lazy(() => { import('./pages/QuoteDetail') });
const NewQuote = React.lazy(() => { import('./pages/NewQuote') });
const NotFound = React.lazy(() => { import('./pages/NotFound') });

const App = () => {
    return (
        <Suspense fallback={<p>Loading...</p>}>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/quotes" element={<AllQuotes />} />
                <Route path="/quotes/:quoteId" element={<QuoteDetail />} />
                <Route path="/new-quote" element={<NewQuote />} />
                <Route path="/*" element={<NotFound />} />
            </Routes>
        </Suspense>
    );
};

export default App;

Lazy Loading을 사용하여 초기 코드 번들을 더 작게 만들어 애플리케이션의 초기 로딩을 단축시켜줍니다. 이는 React App의 모든 코드가 한 번에 로딩되지 않고 필요한 코드만을 로딩하기 때문입니다.

build scripts

우리는 배포 환경에 대한 최적화를 하기 위해서 scripts를 추가해주어야 합니다.

package.json 파일의 scripts 부분에 다음 명령어를 추가해줍니다.

// package.json
{
    ,,,
    scripts: {
        ,,,
        "build": "react-scripts build"
    }
}

참고로 react-create-app으로 React 프로젝트를 생성한 경우 기본적으로 해당 스크립트가 추가되어 있습니다.

react-scripts build는 배포를 위해 코드를 최대한 최적화하고 축소합니다. 하지만 npm start처럼 서버를 자동으로 실행해주지는 않습니다. 단지 배포를 위해 코드를 최적화하고 축소하는 변환만을 실행합니다.

우리는 npm run build명령어를 터미널에 입력하여 실행할 수 있으며 그 결과물은 프로젝트의 build 폴더에서 찾을 수 있습니다. 즉, build 폴더에 배포하는데 필요한 모든 코드가 존재하며, build 폴더의 모든 파일은 추후에 서버에 업로드될 파일들입니다.


위 그림처럼 터미널에 npm run build 명령어를 실행하게 되면 배포를 위해 최적화된 파일들이 생성된 것을 확인할 수 있고, 생성된 파일들은 아래 그림처럼 프로젝트 내 build 폴더에 모두 존재합니다.

Upload to Server

React를 포함한 모든 SPA는 "정적 웹사이트(Static Website)"입니다. 정적 웹사이트란 HTML, CSS, 브라우저 사이드 JavaScript 코드로만 구성되어 있는 웹사이트를 의미합니다. 즉, 다른 서버 사이드 코드가 존재하지 않습니다.

build 폴더에 존재하는 자바스크립트 코드들은 서버 사이드 자바스크립트 코드가 아닙니다. 클라이언트 사이드, 브라우저 사이드 자바스크립트 코드입니다. 이 코드들은 브라우저에서 실행됩니다. 따라서 전체 build 폴더에는 서버에서 실행되기 위한 코드(Node.js, PHP 등)가 전혀 없습니다.

따라서 SPA를 배포하기 위해서 정적 웹사이트를 제공하는 "정적 웹사이트 호스팅 제공자(서버)"가 있어야 합니다.


FireBase를 이용하여 서버에 업로드해보겠습니다.

firebase 프로젝트로젝트를 생성하고 Hosting을 선택합니다.

그러면 다음과 같이 호스팅 설정을 하는 방법을 알려주는 페이지로 이동하게 됩니다.

그리고 페이지에 나와있는 순서대로 npm install -g firebase-tools를 입력하여 firebase-tools를 설치하고, firebase login을 입력하여 로그인을 해줍니다. 그리고 firebase init 명령어를 입력하여 호스팅을 설정합니다.

  1. 첫 번째 질문은 어떤 firebase 특징을 사용할 것인지를 묻고 있습니다. 우리는 호스팅을 하기 위해서 4번째 옵션을 선택해줍니다. 참고로 선택을 하기 위해서는 스페이스바를 눌러야 합니다. 그리고 엔터키를 눌러 다음 설정으로 넘어갑니다.

  2. 두 번째 질문은 새 Firebase 프로젝트를 만들 것인지 기존의 것을 사용할 것인지를 선택합니다.

  3. 세 번째 질문은 배포할 파일의 위치를 설정합니다. 기본값은 public 폴더로 되어있습니다. 우리는 build 폴더에 최종적으로 업로드할 번들파일이 존재하기 때문에 우리는 build로 설정해줍니다.

  4. 네 번째 질문은 이 애플리케이션을 SPA로 설정할 것인지에 대한 질문을 합니다. 이 질문은 요청되는 모든 URL에 대한 응답으로 index.html로 사용할 것인지에 대한 질문입니다. 이 질문에 대한 자세한 설명은 아래에서 설명하겠습니다.

  5. 다섯번째 질문은 자동 빌드와 자동 배포에 대한 질문입니다.

  6. 여섯번째 질문을 index.html 파일이 다시 작성되어야 하는지에 대한 질문입니다.

이렇게 모든 질문에 대답하여 호스팅을 설정한 뒤에 터미널에 firebase deploy라는 명령어를 입력하여 서버에 업로드해줍니다. 터미널에 다음과 같은 호스팅 URL 경로가 제공됩니다.

만약 호스팅을 중단하고싶다면 firebase hosting:disable을 터미널에 입력해줍니다.

Server-side Routing vs Client-side Routing

React Router를 사용한 React App의 모든 코드는 브라우저가 실행합니다. react-router-dom 또한 브라우저 사이드 패키지이며 이는 브라우저에서 실행됩니다. React App이 브라우저에 로드된 이후 React Router가 URL을 확인하고 화면에 그에맞는 컴포넌트를 렌더링합니다.

여기서 중요한 점은 Reac Router는 React App이 로드된 뒤에 URL의 경로를 확인한 이후에 그에 맞는 컴포넌트를 렌더링한다는 점입니다.
또한 실제로 Route는 다른 페이지를 화면에 렌더링하는 것이 아닌, 하나의 페이지(index.html)에서 URL의 경로에 맞는 컴포넌트를 조건부 렌더링하는 것입니다. 즉, SPA는 하나의 페이지(index.html)만을 사용하기 때문에 다른 페이지를 요청하게 된다면 이는 제대로 동작하지 않습니다.

브라우저 주소창에 경로가 포함된 전체 URL을 입력하고 엔터를 누르면 서버로 요청이 전송되고 서버가 응답을 보냅니다. 이때 특정 경로가 포함된 전체 URL로 요청을 보내게 되면 응답으로 우리가 원하는 React App(index.html)이 전달되지 않습니다. 우리는 경로가 포함된 전체 URL로 요청을 전송하더라고 서버측에서는 URL의 경로값을 무시하도록 설정하여 어떤 URL 요청을 받더라도 그에 대한 응답으로 항상 index.html을 브라우저에게 제공하도록 해야 합니다. 그리고 브라우저가 index.html을 응답을 받은 뒤에 React Router가 경로를 확인하고 그에 맞는 컴포넌트를 렌더링하도록 해야합니다.

즉, URL의 도메인 뒷 부분인 경로가 어떤 값을 갖든 서버는 항상 같은 응답을 반환하도록 합니다. 즉, 사용자가 어떤 경로를 작성하여 요청하던지 항상 같은 HTML 파일이 응답되도록 해야 합니다.
응답을 받은 브라우저는 React App을 실행하고 React Router가 URL의 경로를 확인하고 그에 맞는 컴포넌트를 렌더링하도록 합니다.

우리는 서버는 클라이언트가 요청한 URL의 특정 경로를 무시하도록 서버를 설정해주저야 합니다.

만약 our-domain/some-route를 요청하더라고 서버측에서는 our-domain/으로 요청을 받아 언제나 동일한 응답을 브라우저에게 전달해주도록 만들어야 합니다.

참고로 루트 경로(/)의 요청에대한 응답은 index.html을 의미합니다. 즉, our-domain/our-domain/index.html과 동일합니다.

이러한 설정이 위에서 호스팅 설정하는 네번째 질문에 대한 답으로 설정할 수 있습니다. y로 입력하게 되면 모든 URL 요청에 대한 응답으로 index.html을 브라우저에게 전달해주도록 설정하는 것입니다.

profile
Frontend Dev

0개의 댓글