Back to list

#09. React: Bootstrap & 이미지

목차11

TL;DR

  • Bootstrap: react-bootstrap vs 순수 클래스 + CDN
  • 이미지: src/import vs public/ vs 외부 URL
  • 번들러를 통과하면 해시·최적화·빌드타임 검증을 얻고, 우회하면 그대로 로드되는 대신 그 이득이 없음

개요

이번에는 React에서 Bootstrap과 이미지를 넣는 방법을 정리해보았다.
두 내용 모두 “Bundler가 이 자원을 관리하느냐 아니냐”로 갈릴 수 있기에, 하나로 묶어서 정리하였다.


Bootstrap

Bootstrap은 웹 개발자가 편하게 HTML/CSS, JS 요소를 가져올 수 있도록 도와주는 라이브러리이다.
SaaS니 프레임워크니 엄밀한 정의는 따로 있는 것 같은데, 그냥 쉽게 가져다 쓸 수 있는 디자인 템플릿 모음이다.

본래는 순수 HTML/CSS, JS를 그대로 복사/붙여넣기 하는 것으로 알고 있었는데,
React에서는 별도로 react-bootstrap이 존재하여 컴포넌트 단위로 관리할 수 있도록 도와준다.

1. 순수 Bootstrap (클래스 방식)

react-bootstrap 없이, 그냥 className에 Bootstrap 클래스를 직접 박는 방식이다.

<button className="btn btn-primary">눌러</button>

여기서, Dropdown이나 Modal 등 interactive한 동작을 위해서는 JS 번들이 필요할 수 있다.
보통 index.html<body> 태그 안에 스크립트로 로드하는 방식을 쓴다.

2. react-bootstrap (컴포넌트 방식)

React를 쓰기로 했다면, 위의 방식보다는 react-bootstrap을 사용하는 것이 좋을 것이다.
<Button>, <Container>처럼 Bootstrap을 React 컴포넌트로 감싼 라이브러리를 가져다 쓰는 방식이다.

// main.jsx 상단에서 CSS 한 번만 로드
import 'bootstrap/dist/css/bootstrap.min.css';
// 이후 각 파일에서는 필요한 조각만 import
import Button from 'react-bootstrap/Button';

앞서 순수 Bootstrap을 가져올 때, Bootstrap의 JS 번들이 필요한 경우가 있다고 했었다.
그러나 react-bootstrap은 그 동작들을 React 방식으로 다시 구현해놨기 때문에 CSS만 import하면 된다 ← (장점1)

이러한 편리함 외로도, React의 동작 방식 때문에라도 react-bootstrap의 사용이 권장된다.

기존의 순수 Bootstrap를 사용할 때 로드하는 JS bundle은 결국 브라우저에서 DOM을 직접 조작한다.
예를들어 사용자가 Modal을 열면 JavaScript가 동작하여, DOM의 특정 위치에 Element를 직접 주입하고, 스타일을 꽂아버린다.

그러나 React는 별도로 가상 DOM을 관리하며, 이 가상 DOM이 사용자가 현재 보고있는 실제 화면이라고 믿는다.
따라서 JavaScript로 인해 편집된 브라우저의 DOM, 즉 사용자가 보게 되는 조작된 화면을 React는 모르므로,
나중에 가서 둘이 서로 다른 이야기를 해버릴 경우 불필요한 충돌이 발생할 수 밖에 없게 된다.

그래서 뭘 쓰나?

여러 이유로 react-bootstrap을 권장하는 말이 많았고, 나도 이에 수긍하였따:

  • Bootstrap JS를 안 쓰니 DOM 충돌 자체가 없다.
  • 필요한 컴포넌트만 import하니 번들에 안 쓰는 조각이 안 실린다.
  • props, state로 제어하니 React스럽게 다룰 수 있다.

https://react-bootstrap.netlify.app/docs/components/cards

예를들어, 위 링크에 접속하면 아래 이미지처럼 이름, 설명, 복붙할 코드가 순서대로 나온다.

Live Editor에서 이것저것 바꿔볼 수도 있다.
Live Editor에서 이것저것 바꿔볼 수도 있다.

여기서 “Live Editor”의 내용을 그대로 복사하고, BasicExample.jsx와 같은 파일을 만들고 붙여넣으면 끝.
귀찮지 말라고 export default까지 넣어준 모습이 인상적이다.

만약 props가 필요하다면 매개변수에 추가해주면 되고, state가 필요하면 늘 했듯이 선언하고 쓰면 된다.


이미지 넣기

이미지를 넣는 방식은 크게 세 개인데, 판단 기준은 딱 하나다.

이 이미지를 번들러가 관리하는가?

1. src/ 폴더에서 관리

src 안의 이미지는 번들러가 처리한다. import로 가져오거나, CSS url()로 참조한다.

import shoes from '../assets/shoes1.jpg';   // shoes = 해시된 URL 문자열
// import { shoes } from '../assets/shoes1.jpg';  // 중괄호 X

예전에 data.js 임포트할 때 named/default 엇갈려서 한참 헤맨 적이 있는데, 에셋도 똑같이 default다.
import shoes from '...'가 반환하는 건 이미지 객체가 아니라 번들러가 해석해준 최종 URL 문자열이라는 점도 알아두면 좋다.

CSS에서 쓸 때는 이렇게 한다:

background-image: url('./assets/shoes1.jpg');

여기서 경로의 기준은 이 파일의 현재 위치가 기준이다.

src에 두면 아래와 같은 이점이 있다:

  • Hash: 파일 명에 hash가 붙는다:
    • 이미지가 바뀌면 해시가 바뀌며, 파일명이 달라지니 브라우저는 이미지가 바뀌었음을 알고 새로 받아온다.
  • 빌드 타임 검증
    • 없는 이미지를 import하면 빌드가 안된다 ← 이런 사소한 배려(?)가 실수를 줄인다.
  • 최적화
    • 아주 작은 파일의 경우, base64 인라이닝을 통해 평문(data URI)으로 박아버린다.
    • 기준은 빌드 도구마다 다를 수 있는데, Vite의 경우에는 4KB 이내의, 아이콘처럼 작은 이미지에만 적용된다.

2. public/

public 폴더는 번들러가 건드리지 않으므로, 앞서 언급된 src/의 모든 이점을 포기해야한다.
그럼에도 public에 두는 이미지의 경우가 있는데, 외부에서 이미지 소스의 이름을 알아야 하는 경우이다.

src/에서 관리할 경우 파일 이름에 해시가 붙게 된다고 했는데, 아래의 경우에서는 이 해시가 정확한 파일의 탐색을 방해한다:

  • favicon.ico
    • 브라우저와의 약속. 탭에 띄울 조그만한 아이콘은 반드시 ‘favicon.ico’로 해줘
  • robotos.txt, sitemap.xml
    • 검색엔진은 루트의 정확한 경로에서 지정된 파일을 긁어간다.
  • OG 이미지
    • “카카오톡으로 공유” 기능 등을 사용할 때, 외부에서 어떤 이미지를 미리보기로 띄워줄지 크롤러가 알아야 한다.
  • 정적으로 배포되어야 하는 파일
    • 예를들어 온라인에서 띄워주는 PDF 등은 import 없이 콘텐츠가 순수하게 모든 정보를 담고 있어야 한다.

결과적으로, “내부적으로 우리만 원본을 알아도 상관 없는 것들”은 src에, “외부에서 누군가가 읽거나 가져가야 하는 이미지들”은 public에 둔다고 정리할 수 있겠다.

3. 외부 URL

물론 꼭 프로젝트에서 이미지를 관리할 필요 없이, 외부 저장소에 이미지를 올려놓고 필요할 때 fetch해도 된다.
바로 이 블로그가 대표적인 사례인데, github에 이미지까지 배포하기 좀 짜쳐서(?),
모든 첨부되는 이미지들은 일차적으로 Cloudflare R2 서버에 올라가고, 페이지 로드 시 해당 이미지를 가져오고 있다.

물론 Cloudflare가 해당 서비스를 종료하거나 그쪽 서버에 문제가 생기면 이미지를 가져올 수 없다고 뜨겠으나,
사실상 그럴 확률이 당분간은 0%이기에 이렇게 쓰고 있다.


결론

정리하면 이런 기준으로 고르면 된다.

상황선택
앱의 일부이고, 해시·최적화 이득을 보고 싶은 이미지src/ + import
파비콘처럼 경로가 고정돼야 하거나, index.html이 직접 참조하는 파일public/
CDN/유저 업로드처럼 내 번들 바깥의 이미지외부 URL

Bootstrap이든 이미지든, 결국 “번들러의 손을 타느냐 마느냐” 를 먼저 정하는 게 순서였다.
번들러에 포함시키면 해시/최적화/빌드타임 검증을 얻는 대신 import로 하나하나 불러와야하고,
우회하면 자유롭게 경로로 던지는 대신 나머지 모든 책임들은 본인이 지어야하는,
실로 개발자스러운 trade-off 관계가 아닌가 싶다.

링크가 복사되었습니다