Stable 버전으로 설치했습니다

C:\Users\계정이름 형식입니다.
C:\Users\young>netstat -ano | findstr :80
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 12976
C:\Users\young>taskkill /pid 12976 /f /t
성공: PID 13800인 프로세스(PID 12976인 자식 프로세스)가 종료되었습니다.
성공: PID 12976인 프로세스(PID 11572인 자식 프로세스)가 종료되었습니다.
C:\Users\young>cd nginx-1.26.2
C:\Users\young\nginx-1.26.2>nginx -t
nginx: the configuration file C:\Users\young\nginx-1.26.2/conf/nginx.conf syntax is ok
nginx: configuration file C:\Users\young\nginx-1.26.2/conf/nginx.conf test is successful
C:\Users\young\nginx-1.26.2>nginx
http://localhost 접속 시 이렇게 표시되면 성공입니다.
nginx.conf 파일을 수정해 줍니다.
기존 내용은 모두 지우고 아래의 내용으로 덮어 써 주세요.
nginx.conf
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 80;
charset utf-8;
location / {
root html;
index index.html;
}
location /frontend {
proxy_pass http://localhost:10001/;
}
location /backend {
proxy_pass http://localhost:20001/;
}
}
}
C:\Users\young\nginx-1.26.2>nginx -s reload
D:\react-spring 폴더 아래 리액트 프로젝트를 생성하겠습니다.D:\react-spring>npx create-react-app frontend
Need to install the following packages:
create-react-app@5.0.1
Ok to proceed? (y) y
npm start로 프로젝트를 실행합니다.D:\react-spring\frotend>npm start
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.0.11:3000
Note that the development build is not optimized.
To create a production build, use npm run build.
webpack compiled successfully

Ctrl + C를 눌러 실행 중인 보일러플레이트를 종료하고, 사진과 같이 최소의 파일만 남깁니다.build 폴더와 .env는 지금 당장 없는 게 맞습니다.
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1.0">
<title>헬로 리액트</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
App.css
.app {
text-align: center;
padding: 20px;
}
.title {
margin-bottom: 20px;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
.input-group {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.input-group label {
margin-bottom: 5px;
}
.input-group input {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
width: 200px; /* input 폭 2배 */
}
button {
margin-top: 10px;
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
App.js
import { useState } from "react";
import "./App.css";
function App() {
const [inputText, setInputText] = useState("");
const [outputText, setOutputText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
fetch("/backend", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ inputText: inputText }),
})
.then((response) => response.json())
.then((data) => setOutputText(data.outputText))
.catch((error) => {
console.error(error);
});
};
return (
<div className="app">
<h2 className="title">내용을 입력해 주세요</h2>
<form onSubmit={handleSubmit}>
<div className="input-group">
<label htmlFor="inputText">요청</label>
<input
name="inputText"
id="inputText"
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
</div>
<div className="input-group">
<label htmlFor="outputText">응답</label>
<input
name="outputText"
id="outputText"
type="text"
value={outputText}
readOnly
/>
</div>
<button type="submit">전송</button>
</form>
</div>
);
}
export default App;
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
.env 파일을 최상위에 추가하여 포트 번호와 기본 URL을 변경합니다..env
PORT=10001
PUBLIC_URL=/frontend
npm start 실행하여 화면이 정상적으로 표시되는지 확인합니다.
D:\react-spring\frontend>npm run build
> frontend@0.1.0 build
> react-scripts build
Creating an optimized production build...
(생략)
Compiled successfully.
D:\react-spring\frontend>npm install -g serve
added 90 packages in 6s
24 packages are looking for funding
run `npm fund` for details
D:\react-spring\frontend>serve -s build -l 10001
Serving!
- Local: http://localhost:10001
- Network: http://192.168.0.11:10001
Copied local address to clipboard!


Maven 프로젝트를 만들어 보겠습니다.

계속 엔터 엔터 누르면서 넘어가다가 Artifact Id 입력하는 곳에서 멈추고 기본값인 demo를 backend로 바꾸겠습니다. Artifact Id는 프로젝트 이름입니다.



Open 버튼을 눌러 새 창에서 프로젝트를 엽니다.

TextController.java
package com.example.backend.controller;
import java.util.HashMap;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TextController {
@PostMapping(value = "/", consumes = "application/json", produces = "application/json")
public ResponseEntity<HashMap<String, String>> postText(@RequestBody HashMap<String, String> textMap) {
System.out.println("리액트가 보냄: " + textMap.get("inputText"));
textMap.remove("inputText");
textMap.put("outputText", "스프링이 보냄: POST 받았음");
return ResponseEntity.ok().body(textMap);
}
}
application.properties
spring.application.name=backend
server.port=20001
D:\react-spring\backend>mvnw spring-boot:run
[INFO] Scanning for projects...
(생략)
2024-08-19T12:51:05.765+09:00 INFO 7348 --- [backend] [ restartedMain] com.example.backend.BackendApplication : Started BackendApplication in 1.922 seconds (process running for 2.372)

리액트가 보냄: 포스트 잘 되니?
Ctrl + C를 눌러 종료합니다.일괄 작업을 끝내시겠습니까 (Y/N)? y
D:\react-spring\backend>mvnw clean package
[INFO] Scanning for projects...
(생략)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 16.937 s
[INFO] Finished at: 2024-08-19T12:56:10+09:00
[INFO] ------------------------------------------------------------------------
D:\react-spring\backend>cd target
D:\react-spring\backend\target>java -jar backend-0.0.1-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
(생략)
2024-08-19T12:58:46.696+09:00 INFO 13104 --- [backend] [ main] com.example.backend.BackendApplication : Started BackendApplication in 3.362 seconds (process running for 4.1)
D:\react-spring\frontend>serve -s build -l 10001
Serving!
- Local: http://localhost:10001
- Network: http://192.168.0.11:10001
Copied local address to clipboard!

나 혼자 localhost에서 보는 것으로 끝내는 게 아니라, 다른 PC에서도 나의 프로젝트를 볼 수 있어야죠? 외부에서 내 페이지에 접근할 수 있도록 설정해 보겠습니다.
먼저 프로젝트가 실행 중인 PC의 IP 주소를 확인하겠습니다.
C:\Users\young>ipconfig
Windows IP 구성
이더넷 어댑터 이더넷:
연결별 DNS 접미사. . . . :
링크-로컬 IPv6 주소 . . . . : fe80::bc58:e41d:31c2:31d8%10
IPv4 주소 . . . . . . . . . : 192.168.0.11
서브넷 마스크 . . . . . . . : 255.255.255.0
기본 게이트웨이 . . . . . . : 192.168.0.1

TCP 80 포트가 개방되어 있지 않다 보니 접속이 안 됩니다.
방화벽 설정을 열어 줍니다.







ipconfig 명령어 입력의 결과물에서 기본 게이트웨이 항목이 있었습니다.C:\Users\young>ipconfig
Windows IP 구성
이더넷 어댑터 이더넷:
연결별 DNS 접미사. . . . :
링크-로컬 IPv6 주소 . . . . : fe80::bc58:e41d:31c2:31d8%10
IPv4 주소 . . . . . . . . . : 192.168.0.11
서브넷 마스크 . . . . . . . : 255.255.255.0
기본 게이트웨이 . . . . . . : 192.168.0.1
브라우저에서 기본 게이트웨이 주소를 입력해 보겠습니다. 이건 서버에서 해도 되고, 클라이언트에서 해도 됩니다. 동일 네트워크에 연결되어 있다면 무관합니다. 저는 서버에서 접속하기를 추천드립니다.
저는 SK브로드밴드의 모뎀을 공유기로 쓰고 있어서 관리자 페이지가 이렇게 나오고 있습니다.
이 장비 기준으로 설명하고 나서 추가로 iptime 공유기 기준으로도 설명해 드리겠습니다.



ipconfig -all 명령어로 확인 가능합니다.C:\Users\young>ipconfig -all
이더넷 어댑터 이더넷:
연결별 DNS 접미사. . . . :
설명. . . . . . . . . . . . : Realtek PCIe GbE Family Controller
물리적 주소 . . . . . . . . : 68-1D-EF-FF-FF-FF
DHCP 사용 . . . . . . . . . : 예
자동 구성 사용. . . . . . . : 예
IPv4 주소 . . . . . . . . . : 192.168.0.11(기본 설정)
서브넷 마스크 . . . . . . . : 255.255.255.0
(생략)





58.123.45.67이라면 입력해야 할 URL은 다음과 같습니다.
방금 모바일로 접속한 URL을 그대로 공유해도 되겠지만 멋있지는 않죠? 도메인을 연결해 더 멋지게 만들어 보겠습니다.
가비아에서 제 ID인 suyons를 입력하여 저렴한 도메인을 하나 구매했습니다.


도메인 연결 버튼을 누릅니다.

레코드를 저장한 후 즉시 반영되지는 않습니다. 저는 15분 후에 반영되었습니다.
똑같이 모바일에서 LTE로 인터넷에 연결하고 URL을 다시 입력합니다.




무료 SSL 인증서 검색하니 1순위로 나타나는 Let's Encrypt에서 발급받아 보겠습니다.
Let's Encrypt 웹페이지에서 인증서를 발급해 주는 것이 아니라, 별도의 프로그램을 써야 합니다.C:\Program Files 아래에 풀고 환경 변수 등록하여 어디서나 wacs.exe를 실행할 수 있도록 설정했습니다.
wacs.exe를 실행하고 M을 입력합니다.C:\Users\young>wacs
A simple Windows ACMEv2 client (WACS)
Software version 2.2.9.1701 (release, trimmed, standalone, 64-bit)
Connecting to https://acme-v02.api.letsencrypt.org/...
Connection OK!
Scheduled task points to different location for .exe and/or working directory
Scheduled task exists but does not look healthy
Please report issues at https://github.com/win-acme/win-acme
N: Create certificate (default settings)
M: Create certificate (full options)
R: Run renewals (0 currently due)
A: Manage renewals (0 total)
O: More options...
Q: Quit
Please choose from the menu: m
Running in mode: Interactive, Advanced
Source plugin IIS not available: No supported version of IIS detected.
Please specify how the list of domain names that will be included in the
certificate should be determined. If you choose for one of the "all bindings"
options, the list will automatically be updated for future renewals to
reflect the bindings at that time.
1: Read bindings from IIS
2: Manual input
3: CSR created by another program
C: Abort
How shall we determine the domain(s) to include in the certificate?: 2
abc.com이라면 abc.com,www.abc.com으로 입력합니다. Description: A host name to get a certificate for. This may be a
comma-separated list.
Host: suyons.site,www.suyons.site
Source generated using plugin Manual: suyons.site and 1 alternatives
Friendly name '[Manual] suyons.site'. <Enter> to accept or type desired name: suyons.site
By default your source identifiers are covered by a single certificate. But
if you want to avoid the 100 domain limit, want to prevent information
disclosure via the SAN list, and/or reduce the operational impact of a single
validation failure, you may choose to convert one source into multiple
certificates, using different strategies.
1: Separate certificate for each domain (e.g. *.example.com)
2: Separate certificate for each host (e.g. sub.example.com)
3: Separate certificate for each IIS site
4: Single certificate
C: Abort
Would you like to split this source into multiple certificates?: 1
The ACME server will need to verify that you are the owner of the domain
names that you are requesting the certificate for. This happens both during
initial setup *and* for every future renewal. There are two main methods of
doing so: answering specific http requests (http-01) or create specific dns
records (dns-01). For wildcard identifiers the latter is the only option.
Various additional plugins are available from
https://github.com/win-acme/win-acme/.
1: [http] Save verification files on (network) path
2: [http] Serve verification files from memory
3: [http] Upload verification files via FTP(S)
4: [http] Upload verification files via SSH-FTP
5: [http] Upload verification files via WebDav
6: [dns] Create verification records manually (auto-renew not possible)
7: [dns] Create verification records with acme-dns (https://github.com/joohoi/acme-dns)
8: [dns] Create verification records with your own script
9: [tls-alpn] Answer TLS verification request from win-acme
C: Abort
How would you like prove ownership for the domain(s)?: 1
C:\Users\사용자명\nginx-1.26.2\html 폴더 아래 index.html 파일이 있습니다.Description: Root path of the site that will serve the HTTP validation
requests.
Path: C:\Users\young\nginx-1.26.2\html

no 선택합니다.Description: Copy default web.config to the .well-known directory.
Default: False
Argument: False (press <Enter> to use this)
Copy default web.config before validation? (y/n*) - no
After ownership of the domain(s) has been proven, we will create a
Certificate Signing Request (CSR) to obtain the actual certificate. The CSR
determines properties of the certificate like which (type of) key to use. If
you are not sure what to pick here, RSA is the safe default.
1: Elliptic Curve key
2: RSA key
C: Abort
What kind of private key should be used for the certificate?: 2
conf 폴더 아래 cert 폴더를 만들었습니다.conf 폴더 내부에 만들어 주세요. 밖에 있는 파일은 nginx에서 위치를 못 찾습니다. Description: .pem files are exported to this folder.
File path: C:\Users\young\nginx-1.26.2\conf\cert
Description: Password to set for the private key .pem file.
1: None
2: Type/paste in console
3: Search in vault
Choose from the menu: 1
1: IIS Central Certificate Store (.pfx per host)
2: PEM encoded files (Apache, nginx, etc.)
3: PFX archive
4: Windows Certificate Store (Local Computer)
5: No (additional) store steps
Would you like to store it in another way too?: 5
Installation plugin IIS not available: No supported version of IIS detected.
With the certificate saved to the store(s) of your choice, you may choose one
or more steps to update your applications, e.g. to configure the new
thumbprint, or to update bindings.
1: Create or update bindings in IIS
2: Start external script or program
3: No (additional) installation steps
Which installation step should run first?: 3
no / 동의 할래? yesTerms of service: C:\ProgramData\win-acme\acme-v02.api.letsencrypt.org\LE-SA-v1.4-April-3-2024.pdf
Open in default application? (y/n*) - no
Do you agree with the terms? (y*/n) - yes
Enter email(s) for notifications about problems and abuse (comma-separated): su02ga@outlook.com
.pem 파일이 저장되면 인증서 발급에 성공한 것입니다. Plugin Manual generated source suyons.site with 2 identifiers
Plugin Domain created 1 order
[suyons.site] Authorizing...
[suyons.site] Authorizing using http-01 validation (FileSystem)
Answer should now be browsable at http://suyons.site/.well-known/acme-challenge/qaw_YZasJrkgHfCshZ-b3FQm66x8b84j5MhXgN5L3-s
Preliminary validation failed because 'An error occurred while sending the request.'
[suyons.site] Authorization result: valid
[www.suyons.site] Authorizing...
[www.suyons.site] Authorizing using http-01 validation (FileSystem)
Answer should now be browsable at http://www.suyons.site/.well-known/acme-challenge/LLeuPvMMkOzY8OlOqvoWPh08BGB3tjdEmv6JW4_jjwo
Preliminary validation failed because 'An error occurred while sending the request.'
[www.suyons.site] Authorization result: valid
Downloading certificate suyons.site [suyons.site]
Store with PemFiles...
Exporting .pem files to C:\Users\young\nginx-1.26.2\conf\cert
Scheduled task looks healthy
Adding renewal for suyons.site
Next renewal due after 2024-10-13
Certificate suyons.site created

win-acme가 자동으로 갱신해 줍니다.작업 스케줄러를 열어 확인해 보면 다음과 같이 win-acme의 작업이 매일 09시에 예약됨을 확인할 수 있습니다.

이제 nginx가 "나 SSL 인증서 있으니까 HTTPS(TCP 443) 연결만 받겠다." 하도록 설정할 차례입니다.
conf 파일을 수정합니다.

nginx.conf
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
server_name suyons.site www.suyons.site;
charset utf-8;
ssl_certificate cert/suyons.site-chain.pem;
ssl_certificate_key cert/suyons.site-key.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html;
}
location /frontend {
proxy_pass http://localhost:10001/;
}
location /backend {
proxy_pass http://localhost:20001/;
}
}
}
C:\Users\young\nginx-1.26.2>nginx -s reload




<!DOCTYPE html>
<html lang="ko">
<head>
<meta
charset="UTF-8"
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<link rel="stylesheet" href="https://unpkg.com/mvp.css" />
<style>
a {
text-decoration: none;
}
</style>
<title>Sooyoung</title>
</head>
<body>
<header>
<nav>
<a href="https://suyons.site"><h2>Sooyoung</h2></a>
<ul>
<li>
<a href="https://www.github.com/suyons" target="_blank">GitHub ↗</a>
</li>
</ul>
</nav>
<h1>👋 반갑습니다, 수영입니다</h1>
<p>찾아 주셔서 감사합니다.</p>
<br />
<p>
<a href="https://velog.io/@suyons" target="_blank">
<i>블로그</i>
</a>
<a href="mailto:su02ga@outlook.com" target="_blank">
<b>이메일</b>
</a>
</p>
</header>
<main>
<hr />
<section>
<header>
<h2>Projects</h2>
<p>제가 진행한 프로젝트를 소개합니다.</p>
</header>
<aside>
<h3>프로젝트 1</h3>
<p>심심해서 만들어 본 리액트 + 스프링 첫 번째 프로젝트입니다.</p>
<p>집에 있는 노트북에서 nginx를 실행하여 배포하고 있습니다.</p>
<p>
<a href="/project1" target="_blank"><em>이동하기</em></a>
</p>
</aside>
<aside>
<h3>프로젝트 2</h3>
<p>심심해서 만들어 본 리액트 + 스프링 두 번째 프로젝트입니다.</p>
<p>집에 있는 노트북에서 nginx를 실행하여 배포하고 있습니다.</p>
<p>
<a href="/project2" target="_blank"><em>이동하기</em></a>
</p>
</aside>
<aside>
<h3>프로젝트 3</h3>
<p>심심해서 만들어 본 리액트 + 스프링 세 번째 프로젝트입니다.</p>
<p>집에 있는 노트북에서 nginx를 실행하여 배포하고 있습니다.</p>
<p>
<a href="/project3" target="_blank"><em>이동하기</em></a>
</p>
</aside>
</section>
</main>
<footer>
<hr />
<p>
Made by
<a href="https://www.github.com/suyons" target="_blank">Sooyoung ↗</a
><br />
</p>
</footer>
</body>
</html>
첫 번째 프로젝트를 복제해서 두 번째, 세 번째 프로젝트까지 동시에 실행해 보겠습니다.
URL의 경로는 다음과 같이 설정하겠습니다.
# 프로젝트 1
프론트엔드: /project1, 포트 10001
백엔드: /api1, 포트 20001
# 프로젝트 2
프론트엔드: /project2, 포트 10002
백엔드: /api2, 포트 20002
# 프로젝트 3
프론트엔드: /project3, 포트 10003
백엔드: /api3, 포트 20003
nginx.conf
http {
server {
listen 443 ssl;
server_name suyons.site www.suyons.site;
charset utf-8;
ssl_certificate cert/suyons.site-chain.pem;
ssl_certificate_key cert/suyons.site-key.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html;
}
location /project1 {
proxy_pass http://localhost:10001/;
}
location /api1 {
proxy_pass http://localhost:20001/;
}
location /project2 {
proxy_pass http://localhost:10002/;
}
location /api2 {
proxy_pass http://localhost:20002/;
}
location /project3 {
proxy_pass http://localhost:10003/;
}
location /api3 {
proxy_pass http://localhost:20003/;
}
}
}
frontend\src\App.js
// 10행: /backend -> /api1, /api2, /api3
fetch("/api1", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ inputText: inputText }),
})
frontend\.env
# 1행: 포트 번호 (20002, 20003)
# 2행: URL 경로 (/project2, /project3)
PORT=10001
PUBLIC_URL=/project1
D:\react-spring\frontend>npm run build
build 폴더만 밖으로 옮깁니다.backend\src\main\resources\application.properties
# 2행: 포트 번호 (20002, 20003)
spring.application.name=backend
server.port=20001
D:\react-spring\backend>mvnw clean package
target 폴더 안의 backend-0.0.1-SNAPSHOT.jar 파일만 밖으로 옮깁니다.

