일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- React-Router-Dom
- CSS
- typescript
- Effective Typescript
- JUNCTION2023
- 개발자 원칙
- useState
- 코드트리
- 이펙티브타입스크립트
- 캐나다취준
- 개발자를 위한 글쓰기 가이드
- 타입스크립트
- 캐나다개발자
- 시스템디자인
- 테오의 스프린트
- CSS방법론
- Framer motion
- ASP.NET
- 글또 10기
- react
- TS
- 회고
- 글또
- SemVer
- VS Code
- framer-motion
- 알고리즘
- framer
- JSBridge
- Semantic Versioning
- Today
- Total
큰 꿈은 파편이 크다!!⚡️
ASP.NET으로 서버&리액트 프로젝트 서빙하기 본문
ASP.NET으로 서버&리액트 프로젝트 서빙하기.. 전에는?
지금까지 회사에서 CSR을 구현할 때 리액트 웹은 웹대로, ASP.NET 서버는 서버대로 따로 동작시키며 서비스를 제공했었다. 만약 같은 서버가 클라이언트+서버의 역할을 모두 할 때는 ASP.NET의 Razor page라는 문법을 사용해서 SSR 방식으로 웹페이지를 구현했고 나는 ASP.NET을 극혐하게 되었다(..)
그러던 중 같은 서버에서 포트를 다르게 해서 웹 클라이언트, 서버를 각각 돌리되 웹 부분은 리액트로 구현하자는 이야기가 나왔다. 아무래도 리액트 구현 방식에 익숙해져있기도 하고 유저 인터랙션에 강하다보니 리액트의 생산성을 Razor(따위)가 따라올 수 없다는 느낌에서였다.
결과적으로 웹 쪽은 vscode로 작업한 뒤 빌드하면, 빌드된 결과물인 클라이언트와 서버를 돌리는 건 asp.net쪽에서 할 수 있다. 배포할 때에는 vscode로 빌드 후 ASP.NET에서 Publish하면 된다.
여담으로, 사실 이런 시도가 처음은 아니다. 그때는 서버 코드의 어떤 컨트롤러에서 빌드된 채로 존재하는 파일을 html 형식으로 리턴하도록 구현하는 방식이었고, 웹 작업을 한 뒤에는 해당 폴더로 빌드 결과물을 복붙해서 동작을 확인했어야 했다.. 그래서 핫 리로드나 디버깅/테스트가 어려운 단점이 있어서 이거 못할짓이라고 생각했었는데, 여기 나름 최근에 나온 블로그 포스트를 참고하다보니 생각보다 간단했다.
How To
블로그에서 잘 설명해주고 있기 때문에 나는 정리하는 느낌으로만 작성해보려 한다.
1. Visual Studio에서 ASP.NET 웹 프로젝트(ReactAspNetCoreTemplate)를 만들고, 프로젝트 내부에 리액트 프로젝트(ClientApp)를 만든다.
2. Visual Studio에서는 ASP.NET+React 프로젝트도 제공해주는데, 그 프로젝트를 생성했을 때 나타나는 파일들이 필요하다고 블로그는 설명한다. (개인적으로 파일을 삭제해서 뭐가 안되는지 확인한다거나.. 해보지는 않았다.) 각 파일 내용은 아래와 같다.
aspnetcore-https.js
// This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate
const fs = require('fs');
const spawn = require('child_process').spawn;
const path = require('path');
const baseFolder =
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
? `${process.env.APPDATA}/ASP.NET/https`
: `${process.env.HOME}/.aspnet/https`;
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
if (!certificateName) {
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
process.exit(-1);
}
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
spawn('dotnet', [
'dev-certs',
'https',
'--export-path',
certFilePath,
'--format',
'Pem',
'--no-password',
], { stdio: 'inherit', })
.on('exit', (code) => process.exit(code));
}
aspnetcore-react.js
// This script configures the .env.development.local file with additional environment variables to configure HTTPS using the ASP.NET Core
// development certificate in the webpack development proxy.
const fs = require('fs');
const path = require('path');
const baseFolder =
process.env.APPDATA !== undefined && process.env.APPDATA !== ''
? `${process.env.APPDATA}/ASP.NET/https`
: `${process.env.HOME}/.aspnet/https`;
const certificateArg = process.argv.map(arg => arg.match(/--name=(?<value>.+)/i)).filter(Boolean)[0];
const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name;
if (!certificateName) {
console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<<app>> explicitly.')
process.exit(-1);
}
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
if (!fs.existsSync('.env.development.local')) {
fs.writeFileSync(
'.env.development.local',
`SSL_CRT_FILE=${certFilePath}
SSL_KEY_FILE=${keyFilePath}`
);
} else {
let lines = fs.readFileSync('.env.development.local')
.toString()
.split('\n');
let hasCert, hasCertKey = false;
for (const line of lines) {
if (/SSL_CRT_FILE=.*/i.test(line)) {
hasCert = true;
}
if (/SSL_KEY_FILE=.*/i.test(line)) {
hasCertKey = true;
}
}
if (!hasCert) {
fs.appendFileSync(
'.env.development.local',
`\nSSL_CRT_FILE=${certFilePath}`
);
}
if (!hasCertKey) {
fs.appendFileSync(
'.env.development.local',
`\nSSL_KEY_FILE=${keyFilePath}`
);
}
}
.env
BROWSER=none
.env.development (포트번호, public url은 예시이다)
PORT=3333
HTTPS=true
PUBLIC_URL=/spaApp
3. package.json에 prestart 설정을 추가한다.
"scripts": {
"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
4. Visual Studio에서 Microsoft.AspNetCore.SpaServices.Extensions 누겟을 설치한다.
5. Program.cs에서 SpaStaticFiles를 추가한다. clientapp build폴더 내의 파일들이 웹에 나타나게 될 것이며, 서버 소유(?)의 파일이므로 캐싱되지 않는다.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// ↓ Add the following lines: ↓
builder.Services.AddSpaStaticFiles(configuration => {
configuration.RootPath = "clientapp/build";
});
// ↑ these lines ↑
var app = builder.Build();
6. Startup.cs에서 우리가 리액트 프로젝트를 보여줄 경로를 추가한다. 또한 코드에서는, dev모드일 경우 클라이언트를 특정 url로 띄우도록 설정하고 있다. production모드일 때는 clientapp폴더 내의 코드들을 사용함을 명시한다.
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// ↓ Add the following lines: ↓
var spaPath = "/spaApp";
if (app.Environment.IsDevelopment())
{
app.MapWhen(y => y.Request.Path.StartsWithSegments(spaPath), client =>
{
client.UseSpa(spa =>
{
spa.UseProxyToSpaDevelopmentServer("<https://localhost:3333>");
});
});
}
else
{
app.Map(new PathString(spaPath), client =>
{
client.UseSpaStaticFiles();
client.UseSpa(spa => {
spa.Options.SourcePath = "clientapp";
// adds no-store header to index page to prevent deployment issues (prevent linking to old .js files)
// .js and other static resources are still cached by the browser
spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ResponseHeaders headers = ctx.Context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue
{
NoCache = true,
NoStore = true,
MustRevalidate = true
};
}
};
});
});
}
// ↑ these lines ↑
app.Run();
7. .csproj 파일에 아래 내용을 추가한다. 그러면 ASP.NET 프로젝트를 Publish했을 때 우리가 작업하던 리액트 프로젝트를 같이 빌드하도록 한다.
<PropertyGroup>
<SpaRoot>clientapp\\</SpaRoot>
</PropertyGroup>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)build\\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath> <!-- Changed! -->
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
트러블슛: 배포 후 접속 안되는 문제
위 내용대로 하면 "http://www.example.com/spaApp"으로 접속하면 거기서부터 리액트 페이지다. 그런데 접속하면 www.example.com/static/css/main.c7f02381.css 파일을 찾을 수 없다는 오류가 나타난다. 이러한 경우, index.html에서 해당 파일 경로를 <link href=".**/spaApp**/static/css/main.c7f02381.css" rel="stylesheet"> 으로 변경하면 정상동작한다.
하지만 이걸 빌드할때마다 해야하는건 문제니까 빌드 설정을 바꿔야 하겠지만, 나는 "www.example.com/spaApp"이 아닌, " www.example.com/"부터 리액트 페이지로 시작할것이기 때문에 설정을 바꾸지는 않았다.
루트 경로부터 리액트 프로젝트를 시작하게 하려면? 아래 내용에 이어진다.
응용편: 리액트 페이지 라우팅 변경하기
“www.example.com”, 즉 루트부터 리액트 페이지를 뜨게 할려고 spaPath를 바꿨다. 경로의 마지막에 "/"가 오면 안되어서 “/”가 아닌 그냥 “”으로 작성한다.
var spaPath = "";
.env.development 에서도 동일하게 변경해줘야 한다.
PUBLIC_URL=/
여기에서 문제가 있다. 나는 같은 url에 /api를 붙인 경로로 api호출을 하고 있는데, "/경로부터 리액트 페이지다", 라고 선언해버리니 api호출을 할 수가 없게 되어 404오류가 생겼다.
그래서 /api로 시작하는 api path가 아닌 경우에만 리액트로 연결되도록 변경했다.
var spaPath = "";
var apiPath = "/api";
app.MapWhen(y => (!y.Request.Path.StartsWithSegments(apiPath)), client =>
{
//...
});
결과는?
이렇게 설정하면 우리는 어떤 경험을 하게 될까?
개발 시 vscode로 리액트 프로젝트를 작업/디버깅하고, Visual studio로 띄운 로컬 서버와 api통신을 제대로 하는지 실시간으로 확인할 수 있다.
해당 프로젝트를 배포하면, 사용자는 www.example.com으로 접속했을 때 리액트로 만들어진 CSR 웹사이트를 누빌 수 있다. 해당 리액트 프로젝트에서의 api 호출 경로는 www.example.com/api이다.
Reference
🐍 사족
너무 오랜만에 기술 글을 작성하기도 하고 이 글의 초안이 2023년 7월에 작성되었던 만큼, 기억 속에서 잘 끄집어내서 쓸 수 있을까..? 걱정되기도 하고 글쓰기 너무 귀찮기도 했다.
그런데 결국 끝낸 것을 보니 글또와 예치금의 힘은 위대하고 자극적이다.
글 쓰기 시작하고 30~40분 정도는 몸을 뒤틀면서 썼는데 결말에 가까워지니 날개돋친듯 쓰고 있는 나를 발견했다. 앞으로는 고통스러워도 30분은 참으면서 해보자..
'기타 CS' 카테고리의 다른 글
시스템 디자인 인터뷰 준비 (1) - 개념 정리 (1) | 2025.01.05 |
---|---|
[시스템 디자인 학습] Load balancer (1) | 2024.11.10 |
Microsoft.IdentityModel 인증 + 리액트 (0) | 2023.07.30 |
“런타임”이라는 단어를 이제는 사용할거야 (0) | 2023.07.02 |
내가 정착한 VSCode 환경 설정 💞 소개 (0) | 2023.05.20 |