제목 : 점진적인 리액트 프로젝트 도입 과정과 문제 해결 과정
이 글을 작성하는 이유는?
빠른 프로젝트 생성과 웹 페이지 생성에 초점이 맞춰진 현대 프로그래밍 트렌드는 AI 로 인해
더욱 중요해진 요소로 꼽히고 있다.
현재 프로그래밍 세상에서, 오히려 해당 분야에 대한 깊은 공부는 공감받지 못하는 사회라는 것을 느낀다.
오히려, 빠른 개발을 요하는 실제 프로덕션 세상에서, 깊은 이해를 위한 시간 소비는 사치라고 생각할 지 모른다.
아니, 그것이 당연할 지도 모른다.
그러나, 나는 역발상으로, 리액트라는 단어가 가지는 수많은 의미를 탐색해 왔다.
리액트는 무엇인지, 어떤 것으로 이루어져 있는지, 어떤 방법론을 사용하는지 등등..
심지어는 JavaScript 의 es6 이상 버전이 어떻게 es5 로 폴리필 되는지 확인할 정도였다.
이 과정에서 사람 친화적, 웹 친화적인 자바스크립트가 되기 위해 어떤 변화와 방법이 적용되었는지 조사했다.
예를 들면,
- JS 에 클래스는 Functional 함수의 Sugar Syntax 버전이다.
- 즉, 클래스는 개발자 친화적인 지원일 뿐, 실제로는 JS 의 함수로 컴파일 된다.
- NPM 은 JS 의 커뮤니티를 거대화 시켰으며, 매우 간편한 명령어들을 지원한다.
- JavaScript 는 Web, WAS 개발 시장을 동시에 잡은 프로그래밍 언어이다.
- 최적화를 위해서라면 저 레벨 언어를 사용하는 것이 좋겠지만,
자바스크립트 스스로의 최적화와 수많은 템플릿은 개발 시장에서 굳건하게 자리를 차지하게 해 주었다.
특히 메타프로그래밍이 매우 중요한 시대에서 자바스크립트는 간편한 메타프로그래밍을 지원한다.
- 최적화를 위해서라면 저 레벨 언어를 사용하는 것이 좋겠지만,
- JavaScript 의 디버깅과 타입 지원을 위한 SuperSet 인 타입스크립트는 JS 의 자리를 지켜준다.
- 느린 TS -> JS 컴파일을 위해 최신 Microsoft 가 Go 를 이용한 컴파일 지원을 준비하고 있다.
- JavaScript 는 C 와 같은 저레벨 언어로 제작된 수많은 라이브러리를 가지고 있다.
- 이를 이용하여 JS 에서 부족한 성능 최적화를 어느정도 보완할 수 있다.
- JavaScript 언어 자체의 성능을 극복하기 위해서
wasm모듈을 도입할 수 있다. - 멀티스레드 시스템을 지원한다. - 단, 스레드들은 독립적인 메모리 공간을 가진다.
- 등등...
웹 시장에서 거대한 도구로 자리매김하고 있는 React 는 더욱이 최신 JS 스펙을 적용하며 최적화했으며,
특히 타입스크립트와 발전된 IDE, Language Server 덕분에 리액트 제작은 더욱 간결하고 편리해 졌다.
그러나, 나는 이전에 React 의 Fiber, Lane, EventPriority, executionContext 등
리액트 개발 과정에서 볼 수 없던 수많은 내부 아키텍쳐 변수와 메서드를 살펴보며 이러한 궁금증이 들었다.
"리액트 개발 템플릿 생성을 위한 명령어를 입력하지 않고, 리액트 + 타입스크립트 프로젝트를 어떻게 만들 수 있을까?"
이를 위해 진행하게 될 절차는 다음과 같다.
index.html생성 및 루트 DOM id 등록- 해당 프로젝트 내부에 npm, tsconfig 설정을 이용하여
dist이용 예정 - 컴파일된 파일을 HTML 파일 내부에서 실행하여 DOM 을 렌더링한다.
아주 추상적으로 계획을 잡았는데, 이는 중간에 어떤 과제가 더 존재할지 모르기 때문이다.
(사실, 체계적으로 계획을 잡았다가 해당 방법이 안 될 경우를 고려한 계획.)
한번 날것으로 리액트를 만들어 보고 싶었는데, 오늘이 그 날인가 싶다!
기본적인 프로젝트 세팅하기
우선, 마음가짐을 처음으로 되돌렸다.
나의 프로젝트는 index.html, index.js 에서 출발하기로 마음먹었다.
간단한 웹 서버 실행
파일 형식으로 웹 파일을 열게 된다면, file 과 localhost 자체의 CORS 가 성립되지 않아
모듈 형식의 파일이 로드되지 않는 것을 확인했다.
따라서,
$ npx http-server -c-1
이 명령어로 현재 명령어 위치에서 index.html 을 찾아 실행한다.
파일의 로드 현황과 로드 에러를 실시간으로 CLI 로 볼 수 있어 편하다.
그리고 -c-1 인 이유는, 파일의 캐싱 때문에 넣은 옵션이다.
우리가 웹을 로드할 때, 아주 최근에 동일한 리소스를 요청한 적이 있었다면,
304 라는 네트워크 신호와 함께, 리소스가 다시 로드되지 않는다.
즉, 브라우저가 스스로 저장하고 있던 기존의 파일을 다시 되돌려준다.
이러한 기능은 네트워크를 매우 절약해 주는 브라우저만의 특성이지만, 개발시에는 필요없다.
따라서, -c-1 을 지정하여, 캐싱 파일 따위 저장하지 않겠다는 개발자의 면모를 보여주자..
index.html 기본 형태
<!DOCTYPE html>
<html>
<head>
<title>처음부터 만드는 TS 리액트 프로젝트</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script defer src="./index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
우선, bundler 가 없다는 것을 전제로 간단한 React 웹 렌더링을 수행하기로 마음 먹었다.
index.js 기본 형태
const e = React.createElement;
const domRoot = document.getElementById("root");
const root = ReactDOM.createRoot(domRoot);
root.render(
e(
"h2",
null,
"rendering react for h2"
)
);
결과물은,

이와 같이 정상적으로 나타나는 것을 확인 할 수 있다.
우리가 제작 한 것은 매우 간단한 RootDOM(FiberRoot) 로부터 시작되는 렌더 예제이다.
이제 간단한 컴포넌트를 제작하여 직접 렌더링 해 보자.
index.js 기본적인 컴포넌트 예제
const { useState, createElement } = React;
const e = createElement;
const domRoot = document.getElementById("root");
const root = ReactDOM.createRoot(domRoot);
const Count = () => {
const [count, setCount] = useState(0);
return e(
"div",
null,
e(
"h2",
null,
count
),
e(
"button",
{ onClick : () => {setCount(count + 1)}},
"+ 1"
)
)
}
root.render(
e(
Count,
null,
)
);
결과물 :

index.html 은 건들 것이 아직 없기에 변경하지 않았고,
index.js 에서 변경점이 많다.
우선,
- 함수 컴포넌트를 이용하여 렌더링을 했다.
- 함수 컴포넌트 내부에 React 기본 Hook 인
useState를 사용했다. button엘리먼트를 생성 할 때, JSX 처럼 속성을 주입했다.- 번들러를 위해
import구문을 사용한 것이 아니라, 추출 형식을 채택했다.
React, ReactDOM 2 가지 라이브러리 자체는 현재 윈도우의 전역 변수로 등록되었다.
우리는, defer 이라는 속성 덕분에, DOMContentLoaded 이벤트 직접에 이 파일을 실행하는 것이다.
즉, 렌더링 직전 모든 DOM 이 구성되었지만, 렌더링하기 직전에 이 코드를 실행한다.
우리가 JSX 혹은 TSX 를 이용하지 못하여 createElement 를 불편하게 사용하는 것 외에는,
크게 코드 작성 기법이 다르다는 생각을 하진 않는다.
참고로, createElement 메서드는 현재 e 라는 값으로 "간소화" 시켰으며,
이 메서드의 인자는 각각 이러한 것을 의미한다.
- 사용자 정의 컴포넌트, 혹은 기본 태그를 렌더링 할 경우 문자열로 전달
- 해당 컴포넌트에 전달할 props 혹은 내부 예약 속성
- 해당 컴포넌트의
children에 해당될 인자를 전달한다. (선택 인자)
즉, 우리가 JSX 기법을 이용하지 못하는 것을 제외하면, 대부분 동일한 형식을 가진다.
간단한 정보 입력 및 리스트 구축하기 - Just JavaScript 버전
위의 방식은 이러했다.
- cdn 으로 리액트 라이브러리 파일을 가져와
window객체에 속성으로 매핑한다. defer속성을 이용하여 나의 코드를 마지막에 실행시킨다.
그러나, 위의 방식은 모두 UMD 방식으로, 컴포넌트 별 파일로 분리시키기에는 한계가 명확했다.
이유는, window.ButtonComponent 이런 식으로 매핑해야 되었기 때문.
혹은, 리액트 프로젝트의 Chunk 파일처럼, 하나의 파일에 모든 자바스크립트 코드를 넣는 경우는 상관없다.
그러나, 나는 파일 별로 대표 컴포넌트를 나눠, 관심사를 분리하고 싶었다.
따라서, 나의 자바스크립트 파일은 type : "module" 로서, import 스코프에 해당하도록 만들고,
cdn 방식을 import 스코프, 즉, 모듈 스코프(ESM) 으로 만들기 위해 다른 스크립트 태그를 사용했다.
index.html (ESM 의존성 방식)
<!DOCTYPE html>
<html>
<head>
<title>처음부터 만드는 TS 리액트 프로젝트</title>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client"
}
}
</script>
<script type="module" src="./index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
위의 방식으로, react, react-dom/client 라이브러리는
이전의 cdn 처럼 .development 로서 개발용이 아니라, 실제 프로덕션에서도 사용되는 라이브러리이다.
즉, import .. from ... 을 빌드 전에 개발하는 것처럼 가져올 수 있게 해 주는 기능이다.
MDN 공식문서 확인 결과, 모든 브라우저에서 지원된다.
그러나, 멀티 "imports" 선언은 극소수의 브라우저에서 지원되지 않는다.
굳이 Multi Imports 를 선언 할 필요가 있는가..?
또한, 리액트 렌더링의 진입점을 명확히 작성했다.
즉, ./index.js 경로를 통해 모든 DOM 이 체계화 되는 것이다.
index.js
import { createElement } from "react";
import {createRoot} from "react-dom/client"
import InputInformation from "./components/InputInformation.js";
const e = createElement;
const domRoot = document.getElementById("root");
const root = createRoot(domRoot);
root.render(
e(
InputInformation,
null,
),
);
InputInformation.js - 입력 및 리스트 포함 컴포넌트
import { useState, createElement } from "react";
import InputComponent from "./InputComponent.js";
import ListInformation from "./ListInformation.js";
export default function InputInformation() {
const [text, setText] = useState('');
const [list, setList] = useState([]);
const onClickInsert = () => {
const newList = list.concat(text);
setList(newList);
}
return createElement(
"div",
null,
createElement(
InputComponent,
{text : text, setText : setText, onClickInsert : onClickInsert}
),
createElement(
ListInformation,
{list : list}
)
)
}
InputComponent.js - 입력 컴포넌트
import { createElement } from "react";
export default function InputComponent ({text, setText, onClickInsert}) {
function onClickIns() {
onClickInsert(text);
setText("");
}
return createElement(
"div",
null,
createElement(
"input",
{ onChange: (e) => { setText(e.currentTarget.value); }, value : text},
),
createElement(
"button",
{onClick : () => onClickIns()},
"리스트에 텍스트를 추가한다"
)
)
}
ListInformation.js - 입력된 리스트 컴포넌트
import { createElement, useEffect } from "react"
export default function ListInformation ({list}) {
useEffect(() => {
listing(list);
}, [list]);
const listing = (list) => {
const arr = list.map((text, idx) => {
return createElement(
"li",
{id : idx},
text
)
})
return arr;
}
return createElement(
"ul",
null,
listing(list)
)
}
결과물은,

이를 그래프로 표현 해 보자면,
flowchart TB
subgraph index.html
html-1("웹을 표현하기 위한 리소스를 포함하는 루트 리소스")
html-2("React 에서 RootDOM 이 될 기본 DOM 이 최소 1개는 있어야 한다.")
end
subgraph index.js
index-1("리액트를 사용하는 첫 번째 파일이자, 의존성을 관리")
index-2("루트 돔을 안착시키는 부분이자, 전역 컨텍스트를 관리하도록 만들 수 있는 중요한 부분")
end
subgraph InputInformation.js
input-info-1("입력과 리스트를 포함")
input-info-2("입력에 대한 데이터를 관리하며, 버튼 클릭 시 리스트를 변경하는 메서드를 생성한다.")
input-info-3("이러한 함수와 변수를 props 속성 형식으로 내려주어 하단 컴포넌트에서 실행한다.")
end
subgraph InputComponent.js
input-component-1("텍스트 입력 칸과 입력 버튼이 존재")
input-component-2("상단에서 내려준 state 참조, 설정 값을 매칭시켜, 리스트 데이터 추가")
end
subgraph ListInformation.js
list-info-1("InputComponent.js 에서 입력된 텍스트를 리스팅한다.")
list-info-2("입력된 데이터는 리스트의 변경을 말하며, 이를 감지하여 컴포넌트를 재 렌더링 한다.")
end
index.html --> index.js
index.js --> InputInformation.js
InputInformation.js -- 핸들러 주입 --> InputComponent.js
InputInformation.js -- 리스트 데이터 주입 --> ListInformation.js
ListInformation.js 파일에는 useEffect Hook 을 통하여
list 의 변화 시 재렌더링을 수행하게 만들며, 콘솔에 렌더링 마크를 찍었다.
그런데, 이상한 것이, ListInformation.js 는 리스트의 변화 시에만 재렌더링시 실시되어야 하는데,
InputComponent.js 의 input 태그에서 글자 입력 시 마다
오히려 ListInformation.js 의 컴포넌트 re-render 가 일어났다.
왜 그런가 하니,
InputInformation.js 컴포넌트는 입력 칸에 들어갈 정보를 useState Hook 으로 관리 중이다.
여기에 최적화 방법을 적용하지 않기도 했고, 컴포넌트 내부의 state 변경 시, 해당 선언
컴포넌트는 리렌더링이 일어나야 하기 때문에, 하위 컴포넌트인 ListInformation.js 에서
지속적으로 re-render 과정이 일어나는 것이다.
그런데, 절대적으로 알아야 할 사실이 있다.
사실, 컴포넌트 자체는 재 렌더링 과정으로 갔다는 것이 사실이다.
그러나, React 는 정말 고도의 diffing 기술을 가지며,
특정 diffing 값에만 Commit 과정을 거친다.
즉, 컴포넌트 상태 변화로 인해 Fiber Architecture 가 실행하더라도,
실질적으로 대부분의 컴포넌트는 이전과 별 다를 바가 없기 때문에, 리스트에 추가되는 것만 보일 뿐이다.
그러나, 상태 변화로 인한 버블링 과정과 계산은 컴포넌트가 복잡 해 질 수록 최적화를 요구하게 될 것이다.
즉, 데모 프로젝트에서 동작이 잘 된다 하더라도, useEffect, useCallback 혹은
커스텀 훅을 이용하여 특정 상황에서만 Fiber Architecture 논리로 들어갈 수 있도록 노력해야 한다.
참고로, 바로 직전 주제에서 setState 로 인한 컴포넌트 재렌더링 논리를 대부분 훑어보았다가,
Fiber Architecture 에 대한 내용을 알게 되었다.
따라서, 나는 어찌하여 React 논리 재실행과 실질 렌더링의 차이가 이질적으로 느껴질 수 밖에 없는지 조금은 알 것 같다.
스타일을 추가해볼까?
스타일 또한 createElement 의 요소로 들어갈 수 있다.
이전에 리액트를 할 때, 간헐적 스타일의 경우 직접 컴포넌트에 style={...} 로 넣었었다.
그리고 프로그래머스에서 ThemeContext 에 대해서 다룬 적이 있는데,
나는 CSS 부문에 있어 이해력이 굉장히 낮기 때문에, 허겁지겁 따라 치기에 바빴었다.
즉, createElement == JSX 는, 스타일 요소를 객체로 받아 적용한다는 것이다.
이에 대한 경험으로, 스타일 컨텍스트를 분리한다는 느낌으로 디렉토리를 만들고,
내부 파일을 작성했다.
현재 디렉토리 상황 :
.
├── components
│ ├── InputComponent.js
│ ├── InputInformation.js
│ └── ListInformation.js
├── index.html
├── index.js
└── styles
├── InputComponent.css.js
└── ListInformation.css.js
이렇게 styles 로 일단은 분리시켜 놓았다.
참고로 나는 CSS 를 정말 못한다. 즉, 스타일링 기법을 거의 모른다.
디자인이 정말 나빠도 예시로 봐주길 바랍니다.

JavaScript 에서 CSS 적용 객체 만들기
index.js 는 그대로
InputComponent.css.js
export const InputComponentCss = {
display : "flex",
width : "40rem",
padding: "2rem",
border : "2px solid blue",
borderRadius : "2rem",
}
ListInformation.css.js
export const ListInformationCss = {
width : "40rem",
backgroundColor : "#eee",
fontSize : "1.5rem",
}
CSS 객체를 적용한 파일들의 모습
InputInformationjs
import { useState, createElement } from "react";
import InputComponent from "./InputComponent.js";
import ListInformation from "./ListInformation.js";
import { InputComponentCss } from "../styles/InputComponent.css.js";
import { ListInformationCss } from "../styles/ListInformation.css.js";
export default function InputInformation() {
const [text, setText] = useState('');
const [list, setList] = useState([]);
const onClickInsert = () => {
const newList = list.concat(text);
console.log(newList);
setList(newList);
}
return createElement(
"div",
null,
createElement(
InputComponent,
{
text : text,
setText : setText,
onClickInsert : onClickInsert,
style : InputComponentCss
},
),
createElement(
ListInformation,
{
list : list,
style : ListInformationCss
}
)
)
}
InputComponent.js
import { createElement } from "react";
export default function InputComponent ({text, setText, onClickInsert, style}) {
function onClickIns() {
onClickInsert(text);
setText("");
}
return createElement(
"div",
{style : style},
createElement(
"input",
{
onChange: (e) => { setText(e.currentTarget.value); },
value : text,
style : {
width : "80%",
height : "3rem",
fontSize : "2rem",
border : "3px solid gray",
borderRadius : "0.5rem",
}
},
),
createElement(
"button",
{
onClick : () => onClickIns(),
style : {
width : "20%",
height : "3rem",
fontSize : "2rem",
marginLeft : "1rem"
}
},
"+"
)
)
}
ListInformation.js
import { createElement, useEffect } from "react"
export default function ListInformation ({list, style}) {
useEffect(() => {
listing(list);
}, [list]);
const listing = (list) => {
console.log("listing start");
const arr = list.map((text, idx) => {
return createElement(
"li",
{
id : idx,
style : {
margin : "1rem",
padding : "1rem",
background : "#789",
boxShadow : "10px 5px 5px gray",
borderRadius : "0.5rem"
}
},
text
)
})
return arr;
}
return createElement(
"ul",
{
style : style,
},
listing(list)
)
}
지금와서 보면, 오히려 상위 컴포넌트의 css 객체를 만들 것이 아니라,
하위 객체들에 적용할 css 객체를 만들어야 했다는 생각이 든다.
이는 마치 CSS-in-JS 방식으로,
React 내부의 ReactCssProperties 를 사용한 것과 비슷하다.
즉, 나중에 React 의 Context 기능을 이용하여,
컨텍스트 컴포넌트 하위에 존재하는 컴포넌트들이 원할 때 적용할 수 있는 객체를 만들 생각이다.
물론, 이 글은 React 와 TypeScript 를 적용해 가는 과정에 있으므로, 그건 나중에 다루자.
JSX 적용하기
나의 리액트 프로젝트는 아주 간단하고 "조악한" ㅠㅠ 예제를 보여주었다.
즉, JSX 없이, 프로젝트 세팅 없이도, 기본적인 조합으로 리액트 기능을 이용 할 수 있다는 것이다.
그러나, 리액트의 가장 큰 장점은, JavaScript 코드와 HTML 태그를 동시에 사용하는
"JSX" 기능에 있다.
내가 위에서 createElement 를 react 라이브러리에서 가져왔던 것 처럼,
JSX 방식의 표현은 createElement 를 대신 해 주는 것이다.
따라서, JSX 파일을 적용하는 방식에 대해서 구상해 봐야 한다.
현재 내가 드는 생각은,
1. 사용자 브라우저에 JSX 파싱 기능 추가
이건 정말 개발용이라는 생각에 가깝고, JSX 파싱 연산을 클라이언트에게 추가하는 것과 동일하다.
보통 우리가 리액트 프로젝트를 빌드하고, index.html 을 본다면,
단순한 Chunk(청크) 파일 xxx.js, xxx.css 로 입력 된 것을 볼 수 있다.
그러니까, 브라우저 사용자는 JSX 파일을 컴파일(파싱) 하지 않고, 곧바로 실행하여 인터랙션 하는 것이다.
그러나, 만약 브라우저에서 JSX 파싱을 실행 할 경우, 사용자는 컴포넌트의 렌더링 시 마다,
컴파일 과정을 거쳐야 된다는 이야기이다. (매우 무서운 사실)
따라서, 이는 테스팅이나 매우 간단한 미니 프로젝트가 아닌 이상, 이 방식은 채택되어선 안된다고 생각했다.
2. 이제 npm 을 이용하여 프로젝트를 만들고, Babel 을 이용하여 파싱 결과물을 이용하자
JSX 은 XML 기반의 마크업 문서를 의미한다. (JS + XML)
JSX 는 리액트를 원활히 사용하기 위해, Facebook 이 만든 확장자 파일이다.
참조 문서
https://en.wikipedia.org/wiki/JSX_(JavaScript)
참고로, 리액트 기본 프로젝트 세팅 시, React 전용 Babel 을 따로 내부적으로 설치한다.
일단 Babel 말고도 다른 폴리필 프로그램이 있을지는 모르나,
Babel 은 가장 대표적으로 널리 사용되는 폴리필 프로그램이라고 할 수 있다.
즉, 2015 년 부터 적용된 JS 스펙 - (es2015 버전) 이후에 도입된 기능들은 당연히
현대적인 브라우저에서 지원한다.
그러나, Babel 은 그 이전 버전 또한 호환하기 위해 버전을 낮추는 기능을 제공하며,
추가적인 기능으로 리액트 JSX 를 JavaScript 로 변환시켜주는 기능 또한 탑재했다.
물론, 이는 따로 모듈로 설치해야 할 부분이기는 하다.
따라서, 현재 프로젝트 내부의 js 부분을 모두 삭제한다.
그리고, 프로젝트를 npm 프로젝트로 변화시킨다.
프로젝트 상황 :
.
└── index.html
1 directory, 1 file
기존의 index.html 은 놔둔다.
그렇다면, 기존의 <script type="importmap">...</script> 을 놔두는 이유는,
웹팩과 같은 번들러가 사용될 의존성을 나의 코드와 함께 청크로 내보내지 않기 때문이다.
이제 프로젝트 루트에서 이러한 명령어를 실행한다.
$ npm init -y
Wrote to /../<프로젝트 루트>/package.json:
{
"name": "5-react-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
이 프로젝트에서 개발 시 필요한 의존성들을 기억하고 설정할 기본 파일 package.json 이
생성 및 초기화 되었다.
이제 필요한 기본 의존성을 다운로드 하자 :
$ npm i react react-dom
package.json :
{
...,
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
...
}
그리고, package-lock.json 이라는 의존성 세부정보 파일 또한 생겨났을 것이다.
명심해야 할 것은, 아직 index.html 에서 index.js 만을 진입점으로 두고 있다는 점이다.
index.jsx 생성하기
폴더 src 를 생성 한 후, 내부에 새로운 파일 index.jsx 도 생성하자.
import { createRoot } from "react-dom/client";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(
<div>Start With NPM With React and Babel!!!!!</div>
)
.jsx 확장자 파일을 .js 로 변환시키기 위해서는 Babel 플러그인이 필요하다.
Babel 플러그인 설치하기
기본적인 JSX 파일을 변환하기 위해서는 Babel 플러그인이 필요하다.
그래서 babel 은 무엇을 하는 것일까?
npm 에는 정말 상상 못할 정도의 라이브러리를 지녔으며, 또 각각의 목적을 가지고 있다.
단순히 프로젝트 어플리케이션이 사용하는 의존성들을 가졌다고 생각한다면 오산이다.
babel 은, 현재 우리가 생성한 프로젝트 코드베이스 EX - src 와 같은 폴더를
통째로 가져다가 컴파일한다.
즉, 2015년 직후 새로워진 JavaScript 와 생성된 기능들을 그 이전으로 폴리필(호환) 하도록
만들 필요가 있었다.
따라서, 화살표 함수라던지, Async Await 조합 등등..
이러한 것들은 2015년 이후에 생성된 문법과 기능이다.
따라서, Babel 은 "기초적으로는" 이러한 기능들이 옛날 버전과도 호환되도록,
"Polyfill" 이라는 기능을 가지고 있는 것이다.
그런데, 가장 대표적인 기능으로 React 의 JSX 를 컴파일하는 기능이 있다.
그 뿐만 아니라, StyledComponent 와 같은 독특한 문법을 필요로 하는 경우,
바벨의 preset 을 이용하여 문법 하이라이팅을 "보조" 할 수도 있다.
일단, 우리가 사용하게 될 Babel 의 기능은,
@babel/preset-react 이다.
JSX 를 위한 문법 하이라이팅부터, JSX to JS 파싱까지 도맡아 하는 모듈이다.
그리고, 바벨 사용 시 꼭 필요한 모듈 @babel/core 과 @babel/cli 가 자동으로 설치된다.
뿐만 아니라, React 를 위한 다양한 하위 모듈들이 추가로 설치된다.
확인하고 싶다면, package-lock.json 을 보면 된다.
# 개발용으로 설치 옵션 -D or --save-dev
$ npm i -D @babel/core @babel/cli @babel/preset-env @babel/preset-react,
이제, 추후 사용할 Babel CLI(모듈 내부를 사용) 를 위해,
바벨 설정 파일 babel.config.json 을 루트에 생성하자.
<root>.config.json :
{
"presets" : [
"@babel/preset-react",
],
}
Babel 명령어로 브라우저 실행 가능 파일 만들기
현재 우리의 디렉토리 현황은,
<Project Root>
├── babel.config.json
├── index.html
├── node_modules
├── package-lock.json
├── package.json
└── src
└── index.jsx
이렇게 구성되어 있는 상황이다.
Babel 로 src 디렉토리 내부를 파싱하려면,
2 가지 방식이 존재하는데, 모두 알아야 할 만큼 중요하다
./node_modules/.bin/babel위치를 이용하여 바벨 실행하기
주로 명령어를 이용하여 특정 파일을 리딩하거나, 파싱하는 프로그램의 특성상,
모듈 어디엔가 CLI 파일이 존재한다.
이러한 CLI 파일은, 해당 모듈을 대표하는 사이트에서 제공하므로, 잘 찾아봐야 한다.
즉, 바벨의 명령어 실행을 위해서, @babel/cli 는 필요한 모듈이라는 의미이다.
아니면 코드로 직접 바벨을 불러와서 파싱하는 방법도 있음 --> 드문 경우
현재 디렉토리 루트에서 명령어 탭을 열고,
이러한 명령어를 입력한다.
$ ./node_modules/.bin/babel src --out-dir dist
Successfully compiled 1 file with Babel (149ms).
npx를 이용하여 바벨 실행하기
NPX 는 NPM 모듈을 CLI 로 쉽게 실행하거나, 특정 명령어의 모음을 쉽게 실행하게 해 주는
NPM 의 프로그램이다.
개발 장소인 로컬 컴퓨터에서는 물론, npx 로 바벨을 실행 할 수 있지만,
만약 바벨 실행 장소가 클라우드 서버와 같은 경우, npx 실행을 위해 npm 을 설치 해 주어야 한다.
훨씬 더 압축된 형태로 명령어 실행이 가능하다.
$ npx babel src --out-dir dist
Successfully compiled 1 file with Babel (143ms).
이제 바벨로 컴파일 된 파일을 까보자!
<root>/dist/index.js
import { createRoot } from "react-dom/client";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/React.createElement("div", null, "Start With NPM With React and Babel!!!!!"));
초창기에 이 글에서 createElement 를 이용하여 리액트 컴포넌트를 제작했던 것과 완전 동일하게,
Babel 은 createElement 로 JSX 파일을 컴파일한다.
JSX --> JavaScript 브라우저 실행 가능 확인하기
먼저, index.html 에서 한 줄을 변경해야 한다.
우리의 진입 파일은 ./index.js 가 아니라, ./dist/index.js 이다.
<!DOCTYPE html>
<html>
<head>
<title>처음부터 만드는 TS 리액트 프로젝트</title>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client"
}
}
</script>
<!-- src 속성을 바꿈 -->
<script type="module" src="./dist/index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
결과

렌더링 되지 않은 상태를 보여주었다.
그렇다면 어디가 문제였을까?
index.js 파일을 다시 보자.
import { createRoot } from "react-dom/client";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/React.createElement("div", null, "Start With NPM With React and Babel!!!!!"));
위의 코드에서, React 가 문제가 난 것이다.
즉, 이 파일에서 React 를 정의한 적도, 가져 온 적도 없다는 의미이다.
따라서, index.jsx 에 이러한 코드를 맨 위에 추가해야 한다.
index.jsx :
import * as React from "react";
import { createRoot } from "react-dom/client";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/React.createElement("div", null, "Start With NPM With React and Babel!!!!!"));
그리고, 바벨을 실행 한 후, 서버를 다시 열어 보면,

이제서야 정상적으로 렌더링이 되는 것을 볼 수 있다.
바벨 명령어 통합을 위한 자그마한 작업
이 글이 누구한테 어떻게 언제 읽히게 될지 모르기에, npx 를 통해 바벨을 실행하는 것이 아니라,
직접 명령어 스크립트를 작성하여 개발 세팅을 통합하는 것이 중요하다는 생각이 들었다.
먼저, ./node_modules/.bin/babel src --out-dir dist 라는 명령어를 기억하자.
그걸 그대로, package.json 파일의 "scripts" 부분에 추가한다.
즉,
package.json :
{
// ...
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
// 추가 된 부분
"build": "./node_modules/.bin/babel src --out-dir dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
// ...
},
"devDependencies": {
// ...
}
}
위 처럼 package.json 을 적용 해 주고 나서,
npm run build 를 실행 해 보자 :
$ npm run build
> 5-react-project@1.0.0 build
> ./node_modules/.bin/babel src --out-dir dist
Successfully compiled 1 file with Babel (141ms).
이러한 방식으로 우리는
npx babel src --out-dir dist
에서,
npm run build
만 입력하면 바벨이 src 폴더를 파싱하여 dist 로 만들어 준다.
더욱 간편해 졌다!
현재까지의 진행 요약과 아직 부족한 점.
현재 디렉토리는
.
├── babel.config.json
├── dist
│ └── index.js
├── index.html
├── node_modules
│ └──... 수많은 모듈들
├── package-lock.json
├── package.json
└── src
└── index.jsx
79 directories, 6 files
이러한 상황이다.
즉, 디렉토리를 읽고, 이렇게 해석 할 수 있다.
1. npm 을 이용한 웹 앱 프로젝트이다.
index.html 의 존재와, package.json 의 동시 존재는
npm 의 모듈 의존성을 이용하여 현재 웹 앱을 개발중이라는 것을 파악할 수 있게 해 준다.
2. Babel 을 이용하여 프로젝트를 빌드하고 있다.
프로젝트 내부에는 babel.config.json 이 있다.
이는, Babel 이 어떠한 방식이던 실행되어 컴파일을 실행한다면,
babel.config.json 옵션에 따라 컴파일을 수행한다.
3. src 폴더의 컴파일 결과는 dist 에 해당한다.
이건 개발 컨벤션으로서, 특정 라이브러리를 결과물로 만들고자 했다면, lib 가 될 것이고,
특정 상황에서의 결과물 폴더는 각자 다른 이름을 가진다.
그러나, 보통 대부분 결과물 파일의 이름은 dist 를 가진다.
4. 이 웹 앱은 프로덕션 or 그냥 실행 시 dist 폴더의 소스를 통해 실행한다.
index.html 과, dist 폴더가 공존 할 때 우리는
웹 앱의 결과물(dist) 가, index.html 에서 실행된다고 판단한다.
모든 웹 앱, 즉, 브라우저는 html 파일을 통해 리소스를 로드하고 순서에 맞게 실행한다.
우리가 웹 개발의 편의성으로 인해 잊을 수 있지만, 항상 html 을 통해
브라우저 내에서 클라이언트에게 서비스를 제공할 수 있다는 점을 기억해야 한다.
부족한 점은 무엇일까?
나는 프로젝트를 Babel CLI 로 빌드하고, index.html 에서
index.js 를 실행하는 과정에서 1 가지 문제점이 발생했다.
바로, React 는 현재 프로그램에 존재하지 않는다는 것이었다.
나는 이를 index.jsx 맨 위쪽에 import * as React from "react"
를 선언하는 것으로 해결했다.
그러나, "모든 jsx 파일" 상단에 이러한 React 를 가져오는 구문은 옛날 방식이다.
실제로, 몇 년 전에는 항상 import * as React from "react" 이런 식으로
상단에 리액트를 가져와야만 에러가 나지 않았었다.
이제야 그 이유를 알것 같다..
먼저, 설정한 Babel 의 옵션을 다시 보자
babel.config.json :
{
"presets": [
[
"@babel/preset-react"
]
]
}
추가 옵션을 위해 배열을 내부에 하나 더 넣은 상황. 곧 알게됨.
현재 설정은 @babel/preset-react 로, 기본 옵션을 채택하고 있다.
만약에 Babel 8 이상의 버전이라면, default 는 "runtime" : "automatic" 으로 바뀐다.
그 이전이라면, "runtime" : "classic" 이다.
클래식의 경우, 리액트 메서드 "그대로" React.createElement 로 JSX 를 파싱한다.
따라서, React 를 항상 파일마다 import 해 주어야 하는 것이다.
하지만, 나는 현재 Babel 8 이상의 버전이 아니다.
따라서, 이를 명시적으로 추가 옵션에 적어주어야 한다.
babel.config.json :
{
"presets": [
[
"@babel/preset-react",
{
"runtime" : "automatic"
}
]
]
}
이후, 다시 바벨로 빌드한다.
$ npm run build
> 5-react-project@1.0.0 build
> ./node_modules/.bin/babel src --out-dir dist
Successfully compiled 1 file with Babel (151ms).
index.js :
import { createRoot } from "react-dom/client";
import { jsx as _jsx } from "react/jsx-runtime";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/_jsx("div", {
children: "Start With NPM With React and Babel!!!!!"
}));
기존과 다르게, createElement 메서드가 아니라, _jsx 로 대체하고 있는 것을 볼 수 있다.
이것이 바로 현재 최신 리액트 프로젝트에서 jsx or tsx 파일 작성 시,
상단에 항상 React 를 가져올 필요가 없는 이유이다.
그러나, 하나 더 추가해야 한다.
저기 jsx 메서드를 가져오는 "react/jsx-runtime" 에 해당하는 라이브러리를,
index.html 파일의 importmap 에 추가해야 한다.
간단히 불러오는 방법이 있는데, 바로 경로를 애매하게 두는 것이다.
애매한 경로로 하위 모듈을 모두 불러오기
<!DOCTYPE html>
<html>
<head>
<title>처음부터 만드는 TS 리액트 프로젝트</title>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
// 애매하게 절대경로 "/" 를 걸쳐놓은 것을 볼 수 있다.
"react/": "https://esm.sh/react@18/"
"react-dom/client": "https://esm.sh/react-dom@18/client"
}
}
</script>
<script type="module" src="./dist/index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
저 애매한 경로의 하위 모듈에는 jsx 를 위한 메서드를 제공할 뿐만 아니라,
jsx 개발 런타임, 버전, 메타데이터를 코드로 불러올 수 있다.
하지만, 현재 가장 중요한 모듈은 jsx 이다.
따라서, 추가 경로를 통해 JSX + Babel (runtime : automatic) 으로,
굳이 파일 상단에 import * as React from "react" 할 필요 없이,
곧바로 작성하기만 하면 끝이다!
결과는?

index.jsx 에서 렌더링하는 태그를 "h2" 로 바꾸고, 내용을 좀 바꿔봤다!
import { createRoot } from "react-dom/client";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(
<h2>Babel Options (runtime : automatic) and importsmap in jsx runtime!!!!!!</h2>
)
컴파일링 된 index.js :
import { createRoot } from "react-dom/client";
import { jsx as _jsx } from "react/jsx-runtime";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/_jsx("h2", {
children: "Babel Options (runtime : automatic) and importsmap in jsx runtime!!!!!!"
}));
이러한 식으로, NPM + React + Babel + JSX 조합으로,
index.html 에서 성공적으로 프로젝트를 빌드할 수 있음을 증명했다!!
타입스크립트 추가
현재까지 정말로 읽으신 분이 계시다면,
타입스크립트의 설정 파일인 tsconfig.json 와 연관이 깊어진다는 말씀을 먼저 드리고 시작합니다.
타입스크립트란 무엇일까? - 구체적으로 생각해보기
"아주 정확하게" 말하는 타입스크립트란, "자바스크립트의 SuperSet" 이다.
슈퍼셋이 뭘까? 곰곰이 생각해보면, 자바스크립트를 포함하고 있는 거대한 타입스크립트의 모습을 상상할 수 있다.
그렇다면, 타입스크립트는 스스로 구동하나? --> 당연히 절대 아니다.
그렇다면, 타입스크립트는 프로그래밍 언어이냐? --> 자바스크립트의 약점인 "타입" 을 강화 해 주는 언어다.
또한, 타입스크립트는 타입을 "먼저 검사" 후, 자바스크립트로 컴파일하는 역할만 수행 하지 않고,
데코레이터라고 부르는(EX - @ClassDecorator(...)) 메타데이터 전용 기능을 사용할 수 있게 해 준다.
이는 현재 NestJS 라고 부르는 Node.js 백엔드 프레임워크의 근간을 이루는 매우 중요한 기능이다.
하지만, 아직 자바스크립트 정식 최신 스펙에 추가되지 않아, 이를 트랜스파일 해 주는 것이 타입스크립트이다.
그래서, 사전적 의미와 공식 문서를 통틀어서, 현재 진행되고 있는 이 글 자체에 어떤 의미를 가지고 있을까?
바로, 발전을 거듭하며 복잡해진 모듈을 가진 React 라는 아주아주 거대한 라이브러리를,
개발자에게 있어 편리한 기능을 "유도" 하는 역할을 한다.
이게 뭔 말인가? 싶긴 하지만, 생각해 보면, 리액트는 이러한 템플릿으로 주로 움직인다.
(물론 내가 응용은 잘 못하지만, React Fiber Architecture 를 까보면서 알게 되었다.)
리액트가 가지고 있는 흔한 패턴이란?
위에서 잠깐 TypeScript 가 가지고 있는 일반적 정의와,
아주 일부의 편리성 기능만을 언급했다.
그리고, 나는 "타입을 강화시켜주는" 타입스크립트가,
오히려 리액트의 편리한 기능으로 "유도" 한다고 언급했다.
리액트에는 react, react-dom, jsx 라는 JavaScript React 기본 모듈이 존재한다.
그러나, 혹시라도 리액트를 사용 해 본 분이라면, 아마, 타입스크립트로 템플릿이 이루어진
리액트 초기 프로젝트보다 훨씬 더 고려해야 할 사항이 많다는 것을 느꼈을 것이다.
그 이유는, 타입스크립트를 적용하면, "타입 유도 기능" 이 엄청난 역할을 한다.
만약에 자바스크립트로 작성한다면, 사용자가 원하는 유도 메서드를 알려주는 기능이 별로 없다.
하지만, 타입스크립트와 그 설정 파일 tsconfig.json 의 옵션들을 사용한다면,
사용자가
useState--> 컴포넌트가 책임지는 스스로의 stateuseEffect--> 컴포넌트가 re-render 되려면, 어떤 state 가 바뀌었을 때인지.useContext--> 컨텍스트 컴포넌트를 지정하고, 사용할 "지역적 전역 state" 를 지정한다.- [v] 여기서 말하는 지역적 컨텍스트란,
리액트 프로젝트 전체에서 관리하는 전역 상태가 아니라,
컨텍스트를 관리하는 컴포넌트 내부의children이 곧바로 접근할 수 있게 만드는 것이다.
- [v] 여기서 말하는 지역적 컨텍스트란,
타입스크립트는 이 세 가지 패턴을 가지고도, 아주 복잡한 프로젝트를 생성할 수 있다.
그러나, 타입스크립트는 "자동 완성 기능" 뿐만 아니라, 사용된 메서드의 사용법까지 유도한다. (경고문)
따라서, 현대 세상에서 빠른 제작과 구현이 필수 덕목이라면, 타입스크립트와 리액트의 조합은
꼭 필요한 조합이 되었다고 생각한다.
뿐만 아니라, 리액트의 외부 모듈 호환성 덕분에, 리액트의 모듈은 아니지만, 연관 모듈의 import 가
매우 간편해졌다.
이제 기존 프로젝트에서 타입스크립트를 적용 해 보자.
먼저, npm 개발 의존성으로 @babel/preset-typescript 를 추가한다.
$ npm i -D @babel/preset-typescript
babel.config.json :
{
"presets": [
[
"@babel/preset-react",
{
"runtime" : "automatic"
}
],
[
"@babel/preset-typescript",
{
"isTSX" : true,
"allExtensions" : true
}
]
]
}
지금 바로, index.tsx 로 변환해서 트랜스파일링 할 수 있지만,
타입스크립트 파일을 사용할 수 있는 것은 아니다.
바벨은 트랜스파일링을 위한 도구이며, 타입스크립트는 프로젝트에서 .ts, .d.ts, tsx 등등
타입스크립트와 관련된 파일을 사용하기 위한 기초적인 개발 프로그램이다.
타입스크립트를 위한 준비
먼저, typescript 로 전역 명령어로 준비한 사람과,
그렇지 않고 개발 파일로 타입스크립트를 컴파일 하는 사람이 존재 할 것이다.
그렇다면, tsconfig.json 을 만들어야 하는 것은 동일한데, 어떻게 초기화 해야 할까?
1. typescript 가 전역 CLI 로 설치되어 있을 경우.
이럴 때는 쉽게 tsc --init 으로 생성하면 된다.
생성된 tsconfig.json 파일 :
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016",/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true,/* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
명령어로 생성된 tsconfig.json 에는 정말 기본적인 내용들로만 이루어져 있으며,
대부분, 혹은 모든 옵션들에 대한 내용과 함께, 주석 처리가 되어 있다.
위의 json 파일은 주석 처리 옵션을 전부 삭제한 상황이다.
이 CLI 명령으로 생성된 tsconfig.json 옵션의 주석들을 확인하며,
현재 리액트 프로젝트에 필요한 옵션들을 고르면 된다.
혹은, 타입스크립트 설정 참조 위치
2. 타입스크립트 전역 명령어를 따로 설치하지 않고, 개발 의존성으로 처리하는 경우
이럴 때는, 개발 의존성으로 tsconfig.json 을 생성하고,
이후 typescript, @types/react, @types/react-dom 을 설치해야 한다.
프로젝트 루트에 생성한 아주 간단한 (아직 완성 x) tsconfig.json 예시 :
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"jsx": "preserve",
"lib": ["DOM"],
"skipLibCheck": true
}
}
정확히, 이 프로젝트에서 .tsx 파일이 인식이 되며,
정상적으로 브라우저 웹 앱을 제작하기 위한 "최소한의" 기능만을 추가했다.
정확히 현재 프로젝트에서 오류가 나지 않으며,
바벨로 트랜스파일이 가능한 상태는 위와 같은 옵션으로 지정할 수 있다.
(추후 기능을 추가하면 다른 옵션이 필요함.)
이러한 옵션들의 의미는 이러하다 :
target: 출력물이 컴파일 되어 나올 때 이 버전으로 지정module: 코드 작성 시, 최신 문법을 적용할 수 있음. (리액트는 최신 문법을 적용하기 때문에 필요한편)jsx: "jsx" 혹은 "tsx" 파일을 인식하기 위한 타입스크립트 옵션lib: 브라우저 웹 앱을 만들기 위해 필요한 내부 모듈skipLibCheck: 타입 체킹 시, 모듈로 다운로드 한 파일까지 타입 체크를 하지 않는다.- 이 옵션에
true를 설정 한 이유는, 같은 이름을 가진 정의파일이 서로 다른 타입을 가질 수 있기 때문이다.
- 이 옵션에
이렇게 설정하게 되면,
리액트 내부에서 타입스크립트를 통한 타입 체킹과 디버깅, 편의성 기능 추가가 용이해지며,
바벨을 통한 정상적인 트랜스파일링이 가능해진다.
지금 상태로 간단한 컴포넌트 작성 해 보기
현재 index.tsx 컴포넌트를 간단하게 npm run build 명령어를 통해서 트랜스파일링 해 보았는데,
전혀 tsx 가 적용되지 않고, 이전의 JSX 의 결과물이 보이고 있는 상황이다.
그런데, 타입스크립트를 적용하기 위해서는 package.json 의 명령어를 약간 바꿔야 한다.
기존 :
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "./node_modules/.bin/babel src --out-dir dist"
},
이후 :
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "./node_modules/.bin/babel src --out-dir dist --extensions \".ts,.tsx\""
},
정해질 데이터 인터페이스와 TSX 파일을 직접 지정하여 트랜스파일 하도록 명령해야 한다.
문제는 여기서 끝나지 않는다.
타입스크립트 프로젝트를 진행하게 된다면,
같은 타입스크립트 과의 파일을 import 할 때, 확장자를 생략한다.
Babel 은, 물론 이 과정에서 파일을 인식하여 오류 없이 TSX 를 JavaScript 로 변경한다.
그러나, 브라우저는 인식하지 못한다.
예시로, index.tsx 와, Main.tsx 가 존재한다고 가정한다.
index.tsx :
import { createRoot } from "react-dom/client";
import Main from "./components/Main";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(
<Main />
)
그리고 이를 등록해둔 명령어를 사용하기 위해 npm run build 를 실행하면,
index.js :
import { createRoot } from "react-dom/client";
// 여기가 문제이다.
import Main from "./components/Main";
import { jsx as _jsx } from "react/jsx-runtime";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/_jsx(Main, {}));
만약에 "Bundler"(번들러) 를 통해 트랜스파일링을 수행한다면,
알아서 각 파일의 의존성을 파악하여 필요한 확장자를 붙여준다.
그러나, 우리의 트랜스파일링 도구는 Babel 이다.
위의 파일은 브라우저에서 인식하지 못하기 때문에, (.js 가 붙지 않아서)
렌더링이 불가하다.
확장자가 없어서 렌더링이 되지 않는다?
말 그대로 확장자가 붙지 않기 때문에, 브라우저는 ./components/Main 에 요청을 해도,
해당 파일을 찾지 못해 에러가 난다. (404 NOT FOUND 에러)
왜 이러한 에러가 날까?
바벨, 그리고 typescript 개발 상황에서는, 이러한 경로로 작성한다면,
자신과 동일한 확장자를 가진 파일에 의존성을 두고 있음을 자동으로 인식한다.
그러나, 문제는 타입스크립트 개발 상황과 Babel 은 경로를 resolve 하여 알고 있지만,
브라우저는 ./components/Main 을 그대로 서버에 요청한다.
서버는 ./components/Main.js 를 가지고 있지만, 확장자가 없기 때문에 NOT FOUND 404
에러를 돌려준다.
확장자 문제를 해결하는 방법은?
조금 아쉽지만, 바벨 + TypeScript 상황에서 이러한 확장자 문제를 종식시킬 수 있는 방법은,
TSX 확장자나, TS 확장자 파일 내부에서 import 하는 특정 모듈의 경로에
xxx.tsx or xxx.ts 를 붙여주는 것이다.
물론, 타입스크립트는 타입스크립트 파일에 확장자를 직접 붙이는 것에 기본적으로 경고를 날리므로,
tsconfig.json 설정을 약간 추가해주고, babel.config.json 설정을 바꿔준다.
즉,
import Main from "./components/Main" 이라는 형식으로 컴포넌트를 가져왔다면,
import Main from "./components/Main.tsx 이라는 정확한 형식으로 컴포넌트를 가져와야 한다.
tsconfig.json :
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"jsx": "preserve",
"lib": ["DOM"],
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
}
}
allowImportingTsExtension 옵션을 통해 정확한 확장자명을 지정할 수 있다.
그러나, 이는 noEmit 또한 true 일 때 설정이 가능하다.
noEmit 옵션이란, 컴파일러가 자바스크립트 소스 코드, 소스 맵, 정의 파일들과 같은
출력물 파일들을 내보내지 않는다는 의미이다.
왜 이런 옵션이 있냐면, Babel, swc 와 같은 TypeScript to JavaScript 컴파일러와 같은
도구에서 직접적으로 이러한 데이터를 건드리거나, 만들 수 있게 해 주는 옵션이라고 한다.
따라서, tsconfig.json 은 단순히 해당 프로젝트의 타입을 검사해 주는 기능만을 사용하게 되고,
직접 컴파일(트랜스파일링) 하는 역할은 Babel, SWC 에게 맡기게 되는 것이다.
babel.config.json :
{
"presets": [
[
"@babel/preset-react",
{
"runtime" : "automatic"
}
],
[
"@babel/preset-typescript",
{
"rewriteImportExtensions" : true
}
]
]
}
rewriteImportExtensions 옵션 공식 설명서
여기서 @babel/preset-typescript 의 옵션 rewriteImportExtensions 를 다루게 되는데,
이 옵션이 활성화 되었을 경우,
import xxx from "./xxx.ts" 라는 문구는,
import xxx from "./xxx.js" 라는 문구로 바뀌게 된다.
그러나, 우리가 타입스크립트 프로젝트에서 "명확한" 확장자인 .ts 를 작성하기 위해서는,
allowImportingTsExtensions : true 를 설정 해 주어야 하며,
이 옵션은 noEmit : true 를 필요로 한다.
바뀐 index.tsx 와 그 출력물
그렇다면, 이제 한번 트랜스파일 전후를 비교 해 보자.
index.tsx
import { createRoot } from "react-dom/client";
import Main from "./components/Main.tsx";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(
<Main />
)
import { createRoot } from "react-dom/client";
// 원래 없던 확장자 ".js" 가 뒤에 붙은 것을 볼 수 있다.
import Main from "./components/Main.js";
import { jsx as _jsx } from "react/jsx-runtime";
const root = document.getElementById("root");
const RootReact = createRoot(root);
RootReact.render(/*#__PURE__*/_jsx(Main, {}));
결과물

이제야 확장자 문제로 인해 브라우저에서 404 NOT FOUND 가 뜨지 않고,
정상적으로 Main.js 를 지정하여 가져오는 것을 볼 수 있다.
왜 현재 바벨 프로젝트에는 확장자 지정이 꼭 필요한 것이지?
나도 이 문제에 대해서 StackOverFlow 나, DEV 블로거 사이트 등등..
해외 사이트를 많이 뒤져봤는데, 이에 대한 정답은 나오지 않았다.
대부분의 경우, 이 확장자 문제를 WebPack 과 같은 번들러 프로그램을 이용하여 해결했다.
번들러 프로그램은, 우리가 리액트나, 뷰 등등 정말 많은 라이브러리와 프레임워크들의 의존성 관계를
쉽게 해소해 주기 위한 프로그램이다.
번들러는 그 내부가 복잡하고 편의성 기능이 매우 많아서, WebPack 사이트에 들어가
직접 무엇을 할 수 있는지 직접 리스팅을 보는 것이 가장 적합한 행동이다.
즉, 우리는 번들러 프로그램을 사용하지 않았기 때문에, "확장자" 를 명시해야 했다.
이 말은, "모듈의 의존성" 을 풀지 못하기 때문에 확장자 명시가 꼭 필요하단 의미가 된다.
즉, Babel 이란, 우리가 타겟으로 하는 src 폴더의 파일 메타데이터를 저장하고 이를 읽으며,
내부 파일을 읽고, 이에 매칭되는 특정 표현식을 알맞는 JavaScript 표현으로 바꿔준다는 것이다.
이러한 과정을 JavaScript 코드로 쉽게 읽을 수 있게 해 주는 프로그램이며,
이러한 과정 덕분에 공식 모듈이 아닌 여러가지 모듈이 존재한다.
Webpack 과 같은 번들러도 Babel을 사용한다.
리액트는 기본 템플릿 상 Webpack 을 사용한다.
즉, 우리는 React --> Webpack --> Babel 로서 이를 인식할 수 있다.
요약
아주 간단한 리액트 프로젝트의 예시부터,
TypeScript + JSX + Babel + tsconfig + npm 의 예제까지 모두 도달했다.
이 글에 작성된 프로젝트의 과정을 요약하자면 다음과 같다.
1. index.html 에 리액트 cdn 2개로 리액트 프로젝트 실행해보기
CDN(Content Delivery Network) 를 통해, 웹 앱 실행 브라우저 전역 변수로
React, ReactDOM 을 추가한다.
이후 간단한 JavaScript 파일을 추가하여 리액트 컴포넌트를 렌더링하였다.
JSX 를 사용하지 않았으며, 단순히 createElement 로 JSX 를 대체했다.
2. index.html 의 리액트 프로젝트를 모듈 형식의 프로젝트로 변경
기존의 CDN 경로로 가져오는 리액트 프로그램은, window.React, window.ReactDOM 처럼
전역 변수에 할당된다. 내가 브라우저에 주입하는 모든 자바스크립트 파일에서 이를 손쉽게 뽑아 쓸 수 있지만,
모듈 형식의 파일이 아닌, 전역 JS 파일 형식으로 프로그램을 작성하기 때문에
컴포넌트 계층 형식으로 디렉토리와 파일을 제작 할 수 없다.
더군다나 모듈 형식으로 컴포넌트 계층을 확보한다고 한들,
모듈 코드 내부에서는 import 형식으로 프로그램을 가져와야 하는데,
React 는 전역 변수 형식으로 가져와야 하기 때문에,(모듈에서는 사용 불가)
React 프로그램을 import 할 수 있도록 내부 Map 에 넣어주는 스크립트로 변경.
(이후, React 기본 메서드들은 개발 프로젝트처럼 쉽게 가져오고 사용할 수 있었다.)
3. 간단한 형식으로 기본 React Hook 들을 사용한 컴포넌트 조합 예제를 생성
아직 JSX 가 적용되지는 않았지만, createElement 는 JSX 의 JS 메서드 표현 방식이므로,
이 메서드를 사용하여 간단한 리스트 추가 예제를 만들었다.
InputComponent, ListComponent, InputInformation, index 파일로
입력 리스트 예제를 생성했다.
4. 이 상태에서 조악한 형식의 스타일 객체를 만들어 컴포넌트에 적용
createElement 의 2 번째 인자에는 props 로 넘겨줄 정보와,
React Component 가 가질 수 있는 기본 DOM 의 속성을 React 변수 형식으로 넘겨서
해당 컴포넌트 파일에서 props 로 스타일 객체를 넘겨받아 적용했다.
비효율적인 방식이지만, 추후 적용 가능성이 매우매우 높은 StyleContext 의 이해를 위해서
직접 만들어서 적용 해 보았다.
5. JSX 적용하기
편한 리액트 웹 앱 프로젝트 개발에서 빠질 수 없는 꽃, JSX 확장자를 적용하기 개발하기 단계이다.
.jsx 확장자는 브라우저가 바로 이해할 수 있는 파일 형식이 아니다.
즉, React.createElement 의 복잡한 컴포넌트 개발 방식을,
JavaScript + HTML(XML) 형식으로 쉽게 복잡한 컴포넌트를 개발하기 위한 파일이다.
이 단계부터는 NPM 이 사용되는데, Babel 의 도구를 설치하고, 적용하기 위함이다.
(그리고 TypeScript 를 적용하기 위해서는, npm 모듈 관리가 필수적이기도 했다.)
이 단계는 옵션 설정이 복잡할 수 밖에 없는 단계이기도 했다.
NPM 프로젝트 생성(package.json 초기화), Babel 필요 모듈 설치,
package.json 에 Babel CLI 사용을 위한 명령어 스크립트 작성,
babel.config.json 을 루트에 생성하여 Babel CLI 에 응답하는 설정 파일 작성.
이러한 과정을 통해 Babel CLI 생성 시, JSX 를 트랜스파일 하여 웹 페이지에서 렌더링을 했다.
6. TypeScript 적용하기
.tsx 파일을 사용한 단계이다.
TypeScript 자체가 JavaScript 로 다시 컴파일 하기 위한 특정한 도구
(타입 검사, 편의성 도구 추가, 문법 검사 등등 정말 수많은 기능이 추가된다)
이기 때문에, 까다로운 단계였다.
타입스크립트 적용을 위해서 tsconfig.json 을 생성 및 옵션 설정을 마쳤으며,
@types/react, @types/react-dom 설치를 진행했다.
이를 통해서 실질적으로 개발 상황에서 리액트를 "검사" 하기 위한 과정을 모조리 마쳤다.
그리고, 바벨이 타입스크립트 대신 컴파일(트랜스파일) 하는 역할을 맡았기 때문에,
새로운 타입스크립트 플러그인을 설치하고, 적용했다.
이를 통해, .tsx --> .ts --> .js 변환이 바벨을 통해 이루어 졌다.
그러나, 개발 환경에서는 같은 확장자 .ts or .tsx 를 import 해서 적용한다는 것을
암묵적 규칙처럼 검사했지만,
실질 컴파일 결과에서는 의존 파일의 경로에서 확장자가 무시되어 브라우저가 인식하지 못하는 상황이 나왔다.
7. 확장자 추가하기
개발 환경에서 import xxx from "./xxx" 선언 시,
이것이 import xxx from "./xxx.ts or ./xxx.tsx" 라는 것이 암묵적 규칙으로서 인정되었지만,
결과물은 확장자가 무시되면 안되었기에, babel.config.json 에 새로운 규칙을 추가하고,
tsconfig.json 에도 새로운 옵션 규칙을 생성하여 최종 결과물이 브라우저에서 렌더링 되도록 만들었다.
babel.config.json 에는 기본 타입스크립트 프리셋 개발 모듈을 설치하고 이를 명시했으며,
옵션으로 rewriteImportExtensions 를 활성화 하여,
타입스크립트 프로젝트에서 직접적으로 의존 파일 경로에 타입스크립트 관련 확장자가 작성되어 있을 경우,
이것을 최종 결과물 파일인 .js 로 변환하도록 설정했다.
그런데, 기본 타입스크립트 프로젝트에서 .ts, .tsx 와 같은 동일한 확장자를 명시하는 것은
에러 및 경고를 발생시킨다. 이를 에러와 경로를 일으키지 않고, 하나의 규칙으로 인식하도록
tsconfig.json 에 옵션을 추가했다.
이후, 변경된 규칙과 트랜스파일로 브라우저에서 바로 적용 가능한 결과물이 나왔으며,
간단한 카운터 컴포넌트 렌더링을 통해 React 와 TypeScript 프로젝트를 적용시키는 것을 성공했다.
마무리
아주 간단한 index.html 파일 하나에서부터,
index.html + ESM CDN 의존성 + NPM + Babel + TypeScript + JSX 를 완수했다.
React + TypeScript 프로젝트를 만든다는 주요한 목적 말고,
이 글을 어떻게 치환할 수 있을까? 생각을 해 보니까,
"레거시 시스템 혹은 다른 웹 프로그램 기반 도구에서 리액트로 마이그레이션 하기" 가 적당하지 않을까 싶다.
그러나, 이 과정은 완벽하지 않고, 부족한 점이 많다.
이 글을 끝까지 index.html 에 필요 의존성을 esm 으로서 importmap 에 집어넣고 있다.
즉, 바벨이라는 도구는 의존성까지 파악하여 하나의 코드 덩어리(Chunk) 로 내보낼 수는 없다.
만약에 필요 라이브러리인 react, react-dom 까지 한번에 내보내고 싶다면,
Webpack 과 같은 번들러 프로그램을 이용하여 청크 파일을 적용해야 한다.
의존성 파악을 위한 로직은 매우 복잡하기 때문에, 따로 주제를 잡아 글을 작성해야겠다는 생각이 든다.
참조 사이트
React 공식 사이트 - TypeScript 사용하기
https://ko.react.dev/learn/typescript
React 공식 사이트 - 기존 프로젝트에 React 추가하기
https://ko.react.dev/learn/add-react-to-an-existing-project
React 이전 문서 - CDN 링크
https://ko.legacy.reactjs.org/docs/cdn-links.html
MDN 문서 (JavaScript Modules)
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Modules
위키백과 (JSX)
https://en.wikipedia.org/wiki/JSX_(JavaScript)
Babel 공식 사이트 - (@babel/preset-react)
https://babeljs.io/docs/babel-preset-react
TypeScript 공식 홈페이지 - (Using Babel with TypeScript)
https://www.typescriptlang.org/docs/handbook/babel-with-typescript.html
리액트 공식 홈페이지 문서 - (TypeScript 사용하기)
https://ko.react.dev/learn/typescript
Babel 문서 - (rewriteImportExtensions)
https://babeljs.io/docs/babel-preset-typescript#rewriteimportextensions
TypeScript 공식 문서 - (tsconfig - noEmit 옵션)
https://www.typescriptlang.org/tsconfig/#noEmit
'Web-Server > React' 카테고리의 다른 글
| create-react-app 은 레거시화 되었다. - 빠르게 변화하는 웹 진영 템플릿에 대한 생각 (0) | 2025.08.20 |
|---|---|
| React 런타임 코드 해부 노트 : Fiber와 Scheduler를 따라간 21일 (2) | 2025.07.02 |
| HTML 파일에서 React 사용하는 법 - (부제 : 클래스, 함수 컴포넌트 적용) (5) | 2025.06.16 |
| React, 기반 뿌리부터 살펴보자 - (부제 : 다양한 방법론의 조합) (4) | 2025.06.10 |