본문 바로가기
웹/24-StudyWithPnP

[React.js] 8주차 - MPA, SPA, 이미지 최적화

by 보먀 2024. 12. 2.
728x90
반응형

1. 페이지 라우팅

경로에 따라 알맞은 페이지를 렌더링 하는 과정을 말한다. 

 

1.1. MPA

 

전통적인 웹 서버는 사용자에게 보여줄 웹 페이지들을 다 가지고 있었다. 브라우저가 특정 주소를 갖는 페이지를 요청하면 해당 페이지를 찾아 xxx.html 페이지를 브라우저로 보내주고, 브라우저는 받은 html 파일을 그대로 화면에 렌더링하는 방식으로 페이지를 라우팅하였다.

 

이렇게 서버가 여러 개의 페이지를 가지고 있는 것Multi Page Application(MPA)브라우저가 페이지를 요청했을 때 서버 측에서 미리 완성해놓은 html 파일을 보내서 브라우저가 그대로 렌더링하는 것서버 사이드 렌더링(Server Side Rendering) 방식이라고 한다.

(서버 측에서 페이지를 미리 렌더링해준다는 의미) 

 

하지만, MPA 방식에는 몇가지 단점들이 존재한다. 

 

- 단점1. 페이지의 이동이 매끄럽지 못하고 비효율적

페이지를 이동할 때마다 원래 렌더링 해두었던 페이지는 지우고 서버에게 새롭게 받은 페이지 처음부터 다시 렌더링하기 때문에 페이지가 새로고침되는 것처럼 한 번 깜박이게 된다. 

또 화면 이동시에 공통된 부분이 있더라고, 매번 다 지우고 새롭게 그려야 하기 때문에 효율적이지 못하다.

 

- 단점2. 서버의 부하가 심해짐

모든 사용자들이 페이지를 이동할 때마다 서버에게 새로운 페이지를 달라고 요청하게 되는데, 동시에 많은 사용자가 이용할 경우 서버의 부하가 심해지게 된다. 

 

 

1.2. SPA

 

그래서 리액트는 페이지의 이동을 매끄럽고 효율적으로 처리하고, 다수의 사용자가 접속해도 문제 없는 Single Page Application(SPA) 이라는 방식을 사용한다. 

 

SPA 방식은 이름에서도 알 수 있는 것처럼 페이지를 단 한 개만 가지고 있다. 그래서 브라우저가 페이지를 요청을 하면 어떤 경로로 요청을 하던 무조건 index.html 이라는 한 개의 페이지만을 반환해준다. 

 

index.html 은 빈 껍데기 같은 파일이므로 브라우저가 렌더링해도 아무것도 렌더링되지 않는다. 이때 리액트는 해당 페이지에 필요한 리액트 컴포넌트들을 한 번에 묶어서 다시 브라우저에게 추가로 전달한다. 브라우저는 전달 받은 번들 파일을 직접 실행하여, main.jsx 에 있는 render 메서드를 호출하여 App 컴포넌트를 실행하여 렌더링한다. 추가로 전달되는 리액트 컴포넌트와 필요한 js 파일들을 묶는 것을 Bundling 이라고 하며, 묶은 파일을 Bundle JS 파일이라고 하는데, 결국 이 파일에 모든 정보가 담겨있기 때문에 React App 이라고 부를 수 있다. 그리고 이때 Bundling 은 Vite 가 담당한다.

 

SPA 방식은 MPA 방식과 다르게 서버에 아무런 요청을 보내지 않는다. 처음 접속할 때 서버로부터 받았던 React App 을 이용해서 자체적으로 브라우저 내에서 필요한 컴포넌트들을 교체하고, 페이지를 이동시킨다. (React App 에는 모든 페이지, 컴포넌트의 정보가 다 포함되어 있기 때문) 이 방식은 페이지를 이동시킬 때 이전 페이지와 공통 컴포넌트가 존재한다면, 그 부분은 놔두고 필요한 부분만 컴포넌트를 갈아끼우기 때문에 효율적으로 페이지를 업데이트 할 수 있다. 이렇게 브라우저가 직접 자바스크립트를 실행해서 화면을 렌더링하는 방식을 클라이언트 측인 브라우저에서 직접 렌더링을 실행한다고 하여 클라이언트 사이드 렌더링(Client Side Rendering) 이라고 한다. 

 

 

1.3. 페이지 라우팅하기

  • Routes, Route 를 사용해서 각 경로에서 렌더링될 페이지(컴포넌트)를 연결해 놓음
  • useNavigate 라는 리액트 훅으로 페이지를 이동
  • Link 컴포넌트는 클릭 시 지정된 경로로 이동, 브라우저의 기본 새로고침 없이 변경하는 리액트 라우터의 SPA 방식으로 동작
import './App.css'
import { Routes, Route, Link, useNavigate } from 'react-router-dom'
import Home from './pages/Home'
import Diary from './pages/Diary'
import New from './pages/New'
import NotFound from './pages/NotFound'

function App() {

  const nav = useNavigate(); // useNavigate를 사용하면 그 자리에 실제 페이지를 이동시키는 함수 남음

  const onClickButton = () => {
    nav('/new');
  }

  return (
    <>
      <div>
        <Link to={'/'}>Home</Link>
        <Link to={'/new'}>New</Link>
        <Link to={'/diary'}>Diary</Link>
      </div>
      <button onClick={onClickButton}>New 페이지로 이동</button>
      <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/new' element={<New />} />
          <Route path='/diary' element={<Diary />} />
          <Route path='*' element={<NotFound/>} />
      </Routes>
    </>
    
  )
}

export default App

 

 

 

2. 동적 경로 (Dynamic Segements)

 

2.1. URL Parameter

 

/ 뒤에 아이템의 id 등 변경되지 않는 값을 주소로 명시하기 위해 사용됨

~ /product/1
~ /product/2
~ /product/3

 

Route 의 path에 ~ /:id 를 붙여 동적 url 사용을 명시

<Route path='/diary/:id' element={<Diary />} />

 

useParams 라는 리액트 훅으로 params 를 가지고 올 수 있다. 

import { useParams } from 'react-router-dom'

const Diary = () => {

  const params = useParams();

  return (
    <div>
      {params.id} 번 일기입니다.
    </div>
  )
}

export default Diary

 

 

2.2. Query String

 

검색어 등의 자주 변경되는 값을 주소로 명시하기 위해 사용됨

~ /search?q=검색어

 

쿼리 스트링은 Route 에 별도로 설정해 줄 것은 없고, 필요한 곳에서 useSearchParams 를 사용해서 가져오기만 하면 된다.

import React, { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom'

const Home = () => {

  const [params, setParams] = useSearchParams();

  return (
    <div>
     query string: {params}
    </div>
  )
}

export default Home

 

만약 아래와 같이 쿼리 스트링 여러 개가 나열되어 있는 경우, get 메서드를 통해서 원하는 쿼리 스트링의 값만 가져올 수도 있다.

~ ?limit=8&current=5
const [params, setParams] = useSearchParams();

const limit = params.get('limit')); // 8
const current = params.get('current')); // 5

 

 

 

3. 이미지 최적화

 

3.1. 이미지를 src/assets 폴더에 넣기 vs 이미지를 public 폴더에 넣기

 

src/assets 폴더에 이미지를 넣으면 Vite 가 내부적으로 이미지 최적화를 진행한다. 이미지를 최적화할 것이 아니라면, public 에 이미지를 넣어줘도 상관없다.

 

- src/assets 폴더에 이미지 넣기

 

src/assets 폴더에 넣은 경우 import 문을 통해 불러와서 사용할 수 있다. 

import emotion1 from './assets/emotion1.png';
import emotion2 from './assets/emotion2.png';
import emotion3 from './assets/emotion3.png';
import emotion4 from './assets/emotion4.png';
import emotion5 from './assets/emotion5.png';

<div>
    <img src={emotion1} />
    <img src={emotion2} />
    <img src={emotion3} />
    <img src={emotion4} />
    <img src={emotion5} />
</div>

 

 

- public 폴더에 이미지 넣기

 

public 폴더에 넣은 경우 경로를 통해 불러와서 사용할 수 있다. 경로를 통해 이미지를 불러오면 이미지 최적화가 이루어지지 않는다. 

<div>
    <img src={'/emotion1.png'} />
    <img src={'/emotion2.png'} />
    <img src={'/emotion3.png'} />
    <img src={'/emotion4.png'} />
    <img src={'/emotion5.png'} />
</div>

 

 

 

빌드를 하고 개발자 도구를 켜서 이미지를 확인해보자. 

($ npm run build -> $ npm run preview)

 

public 폴더에서 불러온 이미지들의 주소는 불러온 경로 그대로 들어가 있고,

src/assets 폴더에서 불러온 이미지는 암호문 같은 포멧으로 설정되어 있는 것을 확인할 수 있다. 

 

이런 암호문 같은 포멧을 DATA URI 라고 하는데, 이미지와 같은 외부데이터들을 문자열 형태로 브라우저의 메모리에 캐싱하기 위해 사용하는 포멧이다. 이렇게 불러온 이미지들은 자동으로 브라우저의 메모리에 캐싱(저장)되어서 새로고침해도 다시 불러오지 않도록 최적화 된다. 

 

반면 public 폴더에서 일반적인 주소로 불러온 이미지들은 새로고침할 때마다 매번 새롭게 불러온다. (최적화 측면에서 불리)

 

network 탭에서 확인해보면,

public 폴더에 넣어 놓은 이미지(경로로 부른 이미지)는 새로고침할 때마다 새롭게 불러오기 때문에 Size 열에는 불러온 이미지의 사이즈가 Time 열의 불러오는데 걸리는 시간이 들어가 있지만, 

src/assets 폴더에 넣어 놓은 이미지(DATA URI 를 사용한 이미지) 는 브라우저에 캐싱되어 있기 때문에 Size 에는 (memory cache) 가 Time 은 0 ms 라는 값이 들어가 있다. 

 

그렇다면, 이미지는 늘 브라우저에 캐싱하는 것이 좋을까? X

-> 만약 불러와야 하는 이미지가 어마무시하게 많다면, 브라우저 메모리에 캐싱을 했을 때 브라우저 메모리에 과부하가 일어난다.

그렇기 때문에 소수의 이미지는 캐싱이 좋고, 이미지의 수가 많다면 public 폴더에 이미지를 넣고 경로로 불러오는 것이 좋다. 

 

 

추가로 이미지를 여러 장 불러와야 하는 경우 깔끔한 코드를 위해 아래의 코드처럼 util 폴더에 이미지를 불러오는 함수를 작성해서 사용하는 것도 좋다. 

// util/get-emotion-image.js

import emotion1 from '../assets/emotion1.png';
import emotion2 from '../assets/emotion2.png';
import emotion3 from '../assets/emotion3.png';
import emotion4 from '../assets/emotion4.png';
import emotion5 from '../assets/emotion5.png';

export function getEmotionImage(emotionId) {
    switch(emotionId) {
        case 1: return emotion1;
        case 2: return emotion2;
        case 3: return emotion3;
        case 4: return emotion4;
        case 5: return emotion5;
        default: return null;
    }
}
<div>
    <img src={getEmotionImage(1)} />
    <img src={getEmotionImage(2)} />
    <img src={getEmotionImage(3)} />
    <img src={getEmotionImage(4)} />
    <img src={getEmotionImage(5)} />
</div>

 

 

 

728x90
반응형