잘 조명되지 않은 사실로, 타입스크립트는 MSBuild CI/CD 파이프라인에 통합되는 기능도 제공합니다. 이 부분도 Visual Studio 중심의 에코시스템이다보니 파묻힌 느낌이 있는데, File-based App에서도 이 레거시를 그대로 가져올 수 있어 매우 매력적인 기능으로 프로모션할 수 있는 것 같습니다.
이 방식을 사용할 경우 장점은 node_modules 같은 빌드 아티팩트 폴더가 ASP .NET 소스 폴더와는 분리된 위치에 만들어지고 관리되며, 실제로 웹 애플리케이션이 사용할 asset만 따로 wwwroot 폴더에 최종적으로 만들어지도록관리할 수 있다는 점입니다. 그러면서도, 기존의 React/TypeScript 개발 워크플로우에 익숙한 프론트엔드 개발자가 닷넷의 존재를 신경쓰지 않아도 된다는 점이 (파일 하나로 끝나는 닷넷 템플릿이이니까요) 매우 좋습니다.
구성은 다음과 같습니다.
Program.cs
#!/usr/bin/env dotnet
#:sdk Microsoft.NET.Sdk.Web
#:property TargetFramework=NET_TFM
#:package Microsoft.TypeScript.MSBuild@5.9.2
#:property PublishAot=False
// Zero-config TypeScript + React fullstack development with single-file approach
// Run with `dotnet run --no-cache Program.cs`
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapGet("/api/hello", () => new { Message = "Hello from ASP.NET Core!" });
app.Run();
tsconfig.json
{
"compileOnSave": true,
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES5",
"outDir": "wwwroot/assets",
"jsx": "react",
"module": "umd",
"lib": ["DOM", "ES6"],
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": false
},
"include": [
"scripts/**/*.ts",
"scripts/**/*.tsx",
"scripts/**/*.js",
"scripts/**/*.jsx"
],
"exclude": [
"node_modules",
"wwwroot"
]
}
package.json
{
"name": "ts-react-fba",
"version": "1.0.0",
"description": "TypeScript React FBA Test Project",
"main": "wwwroot/assets/app.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"start": "dotnet run Program.cs"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
scripts/app.tsx
// React를 전역 객체로 사용
declare var React: any;
declare var ReactDOM: any;
interface Person {
firstName: string;
lastName: string;
}
class Student {
fullName: string;
constructor(public firstName: string, public middleInitial: string, public lastName: string) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}
function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}
const TSButton = () => {
const user = new Student("Fred", "M.", "Smith");
const [apiMessage, setApiMessage] = React.useState("");
const [isLoading, setIsLoading] = React.useState(false);
const handleClick = () => {
alert(greeter(user));
};
const handleApiCall = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/hello');
if (response.ok) {
const data = await response.text();
setApiMessage(data);
} else {
setApiMessage(`Error: ${response.status} ${response.statusText}`);
}
} catch (error) {
setApiMessage(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsLoading(false);
}
};
return React.createElement("div", null,
React.createElement("h2", null, "TypeScript + React Example"),
React.createElement("button", { onClick: handleClick },
"Click me to greet " + user.fullName
),
React.createElement("p", null, "Greeting: " + greeter(user)),
React.createElement("hr", null),
React.createElement("h3", null, "API Test"),
React.createElement("button", {
onClick: handleApiCall,
disabled: isLoading
}, isLoading ? "Loading..." : "Call /api/hello API"),
React.createElement("p", null, "API Response: ", apiMessage)
);
};
// DOM에 렌더링
window.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('ts-example');
if (container) {
ReactDOM.render(React.createElement(TSButton), container);
}
});
wwwroot/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>FBA React Demo</title>
</head>
<body>
<div id="root">
<div id="ts-example">
<!-- React component will be rendered here -->
</div>
</div>
<!-- React and ReactDOM from CDN -->
<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 src="js/app.js"></script>
</body>
</html>
여기서 눈여겨 볼 것은 Program.cs 파일에서 타입스크립트 컴파일러 버전과 일치하는 nuget 패키지를 사용한다는 점, 그리고 타입스크립트 빌드 관련 설정은 tsconfig.json 파일과 package.json 파일에서 관리한다는 점입니다. 그리고 타입스크립트 빌드가 항상 명시적으로 호출될 수 있게, FBA 캐시를 무시하고 dotnet run --no-cache Program.cs 를 호출해준다는 점만 잘 고려해주시면 되겠습니다.
최종적으로는 아래와 같이 풀 스택 지향성 애플리케이션을 만들어 볼 수 있습니다.
이상의 내용을 FBA 템플릿 팩으로도 만들었으며, dotnet-fba-templates/content/ts-react-fba at main · rkttu/dotnet-fba-templates · GitHub 에서 살펴보실 수 있습니다. ![]()
