나의 Naver Cloud 서버 배포기

🏁 개요
"서버 배포와 CI/CD 구축은 프로젝트 시작과 동시에 진행되어야 한다."
매번 프로젝트를 진행할 때마다 작업물을 서버에 배포하는 과정은 프로젝트의 '모든 기능을 구현한 후'에 진행하였다. 아무래도 프로젝트를 개발하는 기간동안은 서비스를 이용하는 사람이 없으니, 서버 과금을 방지하기 위해 항상 모든 작업이 마무리된 후에 서버를 배포하였던 것 같다.
하지만 그만큼 이미 많은 것들이 세팅된 로컬 환경과 동일하게 처음부터 배포 환경을 세팅하는 것 또한 번거로운 작업이다. 되돌아보면 항상 배포 서버 환경을 구축하는 경험은 항상 쉽지 않다고 느꼈었던 것 같다. 그래서 서버 배포라는 것 자체가 항상 낯설고 고된 작업이라 느껴진 것 같다.
이번에는 프로젝트를 시작하면서 동시에 서버를 배포하여 CI/CD와 함께 구축해보기로 결정하였다.
서버는 AWS, GCP(Google Cloud Platform), NCP(Naver Cloud Platform)를 고민하다가 NCP를 통해 서버를 생성하기로 결정하였다.
- 회원가입 후 결제 수단을 등록하면 micro서버 1년 무료, 3개월 간 사용 가능한 100,000 크레딧을 제공해준다.
- AWS Prettier를 모두 소진하였다.
- 추후 프로젝트 스프린트 기간에는 네이버 클라우드를 자주 사용하여 서버를 배포할 것 같아, 미리 연습해보고자 사용하였다.
🔗 FE - BE 연동
코드 생성
우선적으로 서버에 배포하기 전에 로컬에서 FE와 BE의 연동을 테스트해볼 수 있는 간단한 코드를 작성하였다. (AI 자동 생성)
FE는 Vite + React를 사용하였으며, BE는 Nest.js를 통해 초기 프로젝트를 설치하였다.
// fe/Main.tsx const handleFetch = async () => { setLoading(true);try { const response = await fetch("http://localhost:3000"); const result = await response.text(); await new Promise((resolve) => setTimeout(resolve, 1000)); setData(result); } catch (err: any) { setError(err.message); } finally { setLoading(false); }
};
간단하게 페이지에서 버튼을 클릭하면 :3000 포트로 요청을 보내서 데이터를 화면에 렌더링할 수 있는 핸들러를 AI로 생성하였다.
@Injectable()
export class AppService {
getHello(): string {
const random = Math.floor(Math.random() * 10);
return `Hello World! Random: ${random}`;
}
}
서버에서는 GET / 요청에 대해 랜덤한 숫자와 함께 간단히 Hello World 텍스트를 리턴할 수 있게 작업하였다.
CORS 문제
FE는 현재 5173 포트를 사용하고 있지만 BE는 3000 포트를 사용하고 있어, CORS(Cross-Origin Resource Sharing) 문제가 발생하였다.
CORS는 출처(Origin)을 기준으로 판단하는데, Origin은 URL의 Protocol, Host, Port로 조합되어 구분되며 동일한 localhost에서 요청을 보내더라도 현재 Port 번호가 상이하여 CORS 정책에 위반한 것이다.
해결하는 방법은 여러가지가 있겠지만 우선 빠르게 FE와 BE를 연결하여 서버에 배포하는 것이 목적이기 때문에 BE에서 5173 포트에 대한 Origin을 허용해주는 설정을 추가하여 해결하였다.
async function bootstrap() { const app = await NestFactory.create(AppModule); const PORT = process.env.PORT ?? 3000;app.enableCors({ origin: "http://localhost:5173", credentials: true, }); await app.listen(PORT, () => { console.log(`Server is listening on ${PORT}`); });
}
enableCors() 옵션뿐만 아니라 NestFactory.create()에 두번째 인자로 옵션을 전달하여 설정해줄 수도 있다. 자세한 내용은 Nest.js 공식문서를 참고하면 좋을 것 같다.

성공적으로 로컬에서 FE와 BE를 연동하여 데이터를 요청하고 응답할 수 있게 되었다.
💻 서버 생성
1️⃣ 회원가입 및 서버 이미지
Naver Cloud Platform에 접속하여 회원 가입 후 결제 수단을 등록하면 100_000 크레딧을 무료로 제공받는다.
이 후에 콘솔을 통해 Server 메뉴로 들어오면 서버를 생성할 수 있다.

참고로 1년간 무료로 사용이 가능한 Micro 서버는 Linux 계열 운영체제만 사용이 가능하다.
Rocky와 Ubuntu 중에 Ubuntu 명령어가 조금 더 익숙하여 해당 OS를 선택하여 서버를 생성하였다.
2️⃣ VPC & Subnet
사실 서버를 생성하기 전에 VPC(Virtual Private Cloud)와 Subnet을 먼저 생성해주어야 한다.
해당 개념에 대해 완전히 무지하였기 때문에 이를 이해하기 위해 IP와 Subnet 개념부터 학습을 시작하였다.
만약 개념을 처음 접한다면 이 동영상을 강추한다.
✅ IP(Internet Protocol)
IP는 네트워크에서 호스트를 식별하기 위한 주소이다.
- IPv4: 32bit 주소 체계 (2^32 = 43억개 정도)
- IPv6: 128bit 주소 체계
실제 나는 어떤 IP 주소를 갖고 있는지 확인하기 위해 터미널을 통해 ifconfig 명령어로 정보를 조회해보았다.
inet 192.168.0.38 netmask 0xffffff00 broadcast 192.168.0.255- inet(IPv4): 192.168.0.38
- netmask(서브넷 마스크): 0xffffff00 (255.255.255.0)
- broadcast: 192.168.0.255
IPv4는 "."으로 8비트씩 쪼개서(옥텟) 표시되며 0~255까지 표현이 가능하다. 실제 내 IP는 다음과 같이 해석될 수 있다.
192.168.0.38 → 11000000.10101000.00000000.00100110IP는 Network ID와 Host ID로 구별되며 Network ID의 길이를 나타내는 것이 Subnet Mask이다.
- Network ID: 네트워크를 식별하기 위한 아이디
- Host ID: 네트워크 내 개별 장치를 식별하기 위한 아이디
현재 192.168.0.38이라는 IP에서 서브넷 마스크가 255.255.255.0에 해당하기 때문에, Network ID는 192.168.0.0에 해당하며 38이 Host ID에 해당한다.
192.168.0.0 네트워크를 사용하는 많은 사람들 중 38번 식별자를 가진 사람이라는 뜻이다. (사실은 컴퓨터겠지만,,)
현재 32개의 비트(8비트 * 4) 중에 24비트가 Network ID를 표현하는데 사용하고 있으니, Host ID를 표현할 수 있는 비트는 8비트 밖에 존재하지 않는다.
이 말을 다시 표현해보자면 해당 네트워크는 2^8개의 호스트만 수용할 수 있다는 의미이다. 다시 한 번 더 표현해보자면 192.168.0.0 ~ 192.168.0.255의 호스트 범위를 갖는다.
- 사실 호스트 범위에서 0번 호스트는 네트워크 자기 자신,** 255번 호스트는 broadcast 주소**에 해당하기 때문에 더 정확하게는
192.168.0.1 ~ 192.168.0.254의 범위를 갖는다.
그렇다면 한 가지 의문이 들 수 있다. 하나의 네트워크 안에 사용할 수 있는 호스트 범위가 이렇게 제한된다면 전 지구의 모든 컴퓨터가 자신의 IP를 가지기에는 턱없이 부족하지 않을까?
IPv4는 32비트 주소 체계이기에 약 43억 개 정도의 IP를 생성할 수 있지만 2025년 기준 지구 인구는 약 82억 명이라 한다. 더군다나 한 명이 하나의 컴퓨터만을 가진다는 보장도 없다.
이렇게 한정된 IP 주소 자원을 효율적으로 나누고 관리하기 위해 등장한 기술이 Subnetting이다.
✅ Subnetting
Network ID 범위에 따라 A ~ E 클래스로 네트워크 주소 범위가 나누어져있다.
| 클래스 | Network ID 범위 (첫 옥텟) | 기본 서브넷 마스크 | 호스트 수 (이론상) | 용도 |
|---|---|---|---|---|
| A | 1 ~ 126 | 255.0.0.0 (/8) | 16,777,214 | 대규모 네트워크, ISP 또는 대기업 |
| B | 128 ~ 191 | 255.255.0.0 (/16) | 65,534 | 중형 네트워크, 대학, 중소기업 |
| C | 192 ~ 223 | 255.255.255.0 (/24) | 254 | 소규모 네트워크, 일반 사무실 |
| D | 224 ~ 239 | N/A | N/A | 멀티캐스트용 |
| E | 240 ~ 254 | N/A | N/A | 실험적/연구용, 예약 |
위에서 서브넷 마스크는 255.255.255.0과 같은 방식으로 표기하였는데, 간단하게 /24처럼 prefix로 비트 수를 작성하여 표기할 수 있는데 이를 CIDR(Classless Inter-Domain Routing) 이라고 한다.
IP 주소는 다음과 같이 A~C 클래스로 구분하며 규모에 따라 범위를 한정지어 사용하였지만, 위와 같은 방식은 IP 주소가 낭비되는 문제가 존재하였다.
예를 들어, 50개의 호스트만 필요한 네트워크를 사용하려고 할 때, 어떠한 클래스를 선택하더라도 200개 이상의 호스트가 사용되지 않고 낭비된다.
이러한 비효율성과 인터넷 보급에 따른 주소 고갈 문제를 해결하기 위해 IP 주소를 보다 효율적으로 분할할 수 있는 방식인 서브네팅(Subnetting) 이 등장하였다.
예를 들어, 192.168.10.0/24 네트워크에서는 총 256개의 호스트 주소를 갖는다.

호스트 ID에서 가장 왼쪽의 한 비트를 서브넷 구분 비트로 설정하였을 때, 서브넷 구분 비트가 0일 경우에는 호스트 ID는 0 ~ 127까지 가능할 것이고 1인 경우에는 128 ~ 255까지 가능하다.
이 때, 서브넷 구분 비트는 네트워크 ID로 설정되며 prefix는 /25로 판단할 수 있다.
만약 서브넷 구분 비트를 두 개의 비트로 설정할 경우에는 다음과 같이 분할이 가능하다.

이 또한 두 개의 비트는 네트워크 ID로 설정되기 때문에 prefix는 /26으로 판단된다.
이러한 방식으로 동일한 네트워크에서도 호스트를 서브넷 구분 비트를 통해 필요한만큼 분할하여 각각 용도에 따라 관리할 수 있다는 장점이 있다
✅ 실제 Subnetting 예제
만약 하나의 빌딩이 사설 IP주소를 192.168.10.0/24 IP주소가 주어질 때, 각 층별로, 그리고 그 층에 거주하는 회사 내 host별로 구분하여 subnetting을 해줄 수 있다.
| 구분 | 층 | 필요한 호스트 수 | 할당된 서브넷 | 실제 사용 가능한 IP 범위 |
|---|---|---|---|---|
| 1층 | 60명 | /26 (62호스트) | 192.168.10.0/26 | 192.168.10.1 ~ 192.168.10.62 |
| 2층 | 30명 | /27 (30호스트) | 192.168.10.64/27 | 192.168.10.65 ~ 192.168.10.94 |
| 3층 | 10명 | /28 (14호스트) | 192.168.10.96/28 | 192.168.10.97 ~ 192.168.10.110 |
| 4층 | 100명 | /25 (126호스트) | 192.168.10.128/25 | 192.168.10.129 ~ 192.168.10.254 |
그리고 만약 1층에 3개의 회사가 존재한다면 다시 192.168.10.0/26의 IP 주소를 다시 subnetting 해볼 수 있다.
| 회사 | 서브넷 | 네트워크 주소 | 브로드캐스트 주소 | 사용 가능한 호스트 범위 | 총 호스트 수 |
|---|---|---|---|---|---|
| A | 192.168.10.0/28 | 192.168.10.0 | 192.168.10.15 | 192.168.10.1 ~ 192.168.10.14 | 14 |
| B | 192.168.10.16/28 | 192.168.10.16 | 192.168.10.31 | 192.168.10.17 ~ 192.168.10.30 | 14 |
| C | 192.168.10.32/28 | 192.168.10.32 | 192.168.10.47 | 192.168.10.33 ~ 192.168.10.46 | 14 |
| (남는 공간) | 192.168.10.48/28 | 192.168.10.48 | 192.168.10.63 | 192.168.10.49 ~ 192.168.10.62 | 14 |
✅ VPC & Subnet
진짜 멀리 돌아왔다. 이제 진짜 VPC와 Subnet을 생성하여 서버를 생성하자.

이는 NCP 에서 제공하는 서버 인프라 구축 예제 이미지 중 하나이다.
제공받는 Region 내부에서 나만의 논리적으로 격리된 네트워크를 생성하여 다른 사용자의 네트워크와 분리된 인프라를 제공받을 수 있는 기술이 VPC(Virtual Private Cloud)이다.
VPC가 다른 사용자와의 분리된 네트워크 환경이라고 생각한다면 Subnet은 나의 VPC안에서 용도에 따라 분리된 네트워크 영역이라고 이해해볼 수 있다.
위 이미지에서도 하나의 VPC 안에 Public, Private Subnet으로 분리된다. Public Subnet은 Internet Gateway와 연결하여 외부 인터넷을 통해 접속할 수 있는 서버를 위치시키며 Private Subnet은 NAT Gateway를 통해 한정된 인터넷만 접근이 가능하며 보안적으로 중요한 서버들이 위치하게 인프라를 설계할 수 있다.
서버를 생성하기 전에 VPC를 먼저 Subnet을 생성하게 되는데, 나는 다음과 같이 VPC를 생성하였다.

VPC를 생성할 때 IP 범위를 설정하는데, 멋도 모르고 /16을 설정하였다. 내 VPC 안에는 2^16개(65,536개)의 호스트를 가질 수 있다. ㅎㅎ...
그 다음 Subnet을 생성하게 되는데, Subnetting에 의해 사전에 설정된 VPC IP 범위를 다시 분리하여 사용할 수 있다.

생성한 서브넷의 prefix는 /24로 설정하였는데, 나의 subnet 에서 생성할 수 있는 호스트는 2^8개(256개)에 해당한다.
IP를 효율적으로 분배하라고 Subnetting 기술이 생겼지만 Subnetting 기술을 통해 비효율적으로 분배해버리기.
3️⃣ 서버 생성
VPC와 Subnet을 생성한 후 추가적인 설정만 지어주면 서버를 생성할 수 있다. 특히, Micro로 서버 스펙을 지정해서 생성하는 것 주의해야 한다.

기본적으로 standard로 지정되어있기 때문에 micro로 지정해서 생성해줘야 한다. 이 후에 용량이나 Public IP 자동 생성을 체크해서 서버를 생성해주면 성공적으로 서버가 생성된다.

서버를 생성하면서 인증키(.pem)을 다운받을 수 있었던 것 같은데, 이를 로컬에 저장하여 해당 키를 통해 ssh로 배포 서버에 접속할 수 있다.
4️⃣ 서버 접속 및 환경 설정
✅ Nginx 설치 및 테스트
로컬 터미널에서 ssh -i <pem 키 경로> <계정>@<주소>를 통해 접속을 시도하였고 성공적으로 접속할 수 있었다.

정상적으로 서버를 통해 정적 파일이 서빙될 수 있는지 확인해보았다. 기본적으로 Nginx를 설치하면 root 경로는 /var/www/html로 설정이 되었고 해당 위치에 index.html을 생성하여 저장해주었다.

이 후, 브라우저를 통해 공인 IP로 접근해보았더니 정상적으로 index.html이 렌더링되었다.

✅ Node.js 설치
현재 Node.js 기반 프로젝트를 사용하고 있기 때문에 npm을 사용하기 위해 배포 서버에서도 노드를 설치해주었다. 노드는 NodeSource를 통해 22버전으로 설치해주었다.
sudo apt-get install -y curl curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs node -v
v22.21.0
✅ 도메인 구매
돈을 내면서 이쁜 도메인을 달 생각은 없었기에, 무료로 도메인을 제공해주는 사이트를 서치해보았다. 그 중 한 사이트가 눈에 띄었는데, 이름하여 내 도메인.한국이다.
간단하게 회원가입을 하고 사용하고 싶은 호스트 명를 검색하면 사용가능한 도메인 주소 리스트가 조회된다. 도메인 주소를 정하면 A 레코드에 연결할 Public IP를 지정하면 정상적으로 해당 도메인으로 접속이 가능해진다.
만약 www.{도메인}이나 api.{도메인} 처럼 서브 도메인을 달고 싶다면 CNAME 레코드를 등록하면 된다.
✅ Nginx 설정
서버를 배포하여 FE와 BE를 어떻게 연결할까 고민하다가 처음에는 다음과 같이 설계하였었다.
server { listen 80; server_name fakebnb.kro.kr; root /var/www/html; }
server { listen 80; server_name api.fakebnb.kro.kr; location / { proxy_pass http://127.0.0.1:3000; } }
기본적으로 브라우저에 접속하였을 때 화면에 렌더링하기 위해 요청되는 정적 파일들에 대해서는 fakebnb.kro.kr로 요청이 들어오고 FE에서 데이터를 요청하기 위해 API 요청을 보내는 BE 서버는 api.fakebnb.kro.kr로 관리하고자 위와 같이 설계하였다.
서브 도메인으로 두 가지 용도에 맞게 분리하여 관리한다면 서버의 역할이 명확해질 것이라 생각하여 설계하였지만 위와 같은 설계는 CORS 문제를 다시 야기하였다.
fakebnb와 api.fakebnb는 결국 출처(Origin)의 기준 중 하나인 Host가 다르기 때문에 CORS 문제가 발생하였다.
이를 해결하려면 위에서 해결하였던 것처럼 서버에서 fakebnb에 대해 CORS를 설정하거나 여러 방법들이 존재하겠지만 nginx reverse proxy를 사용하여 동일한 서버에서 클라이언트 요청에 대해 해당 서버의 다른 포트로 전달할 수 있도록 Nginx 설정 값을 변경해주었다.
server { listen 80; server_name fakebnb.kro.kr;root /home/project/fe/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } location / { try_files $uri $uri/ /index.html; }
}
클라이언트의 기본 요청은 fakebnb.kro.kr로 들어오면 root에 해당하는 정적파일 경로에 의해 파일이 서빙되겠지만, /api로 요청을 보내게되면 이는 내부적으로 3000 포트로 요청을 보내며 BE 애플리케이션에서 처리하게끔 수정해주었다.
한 가지 궁금했던 것은 "어쨌든 fakbnb.kro.kr 도메인에서 localhost:3000으로 요청을 보내니 CORS 문제가 여전히 존재하지 않는가?" 였다.
AI를 통해서 답변을 받아보면, CORS 입장에서는 브라우저가 직접적으로 다른 Origin에 요청을 보내고 받을 때 정책의 위반을 검사한다고 한다.
리버스 프록시를 사용하게되면, 브라우저 입장에서는 정적파일 서빙이나 API 요청 모두 fakebnb.kro로 요청을 보낸 것으로 파악되지만 내부적으로 Nginx에 의해 요청 경로에 의해 BE 요청은 3000포트로 보내지게되기 때문에 CORS 문제가 발생하지 않는다고 한다.
reverse proxy에 대해서는 학습이 부족하여 더 보충해야할 것 같다.
✅ FE 빌드 및 확인
위에서 설정한 nginx 설정 파일을 확인해보면 root 경로는 root /home/project/fe/dist;로 설정되어 있다. 기본적으로 /var/www/html/로 설정되지만 FE가 빌드된 경로로 수정하여 npm run build를 통해 빌드하자마자 추가적인 작업없이 해당 경로의 파일들을 nginx가 서빙할 수 있도록 설정하였다.
실제로 git clone을 통해 작업한 프로젝트의 repository를 가져와서 각각 의존성을 설치한 후에 FE는 npm run build로, BE는 npm run start를 통해 각각 스크립트를 실행시켜주었다.

이후, 브라우저를 통해 사전에 등록한 도메인으로 접속하여 데이터 요청을 보내보면 정상적으로 배포된 서버에서 정적 파일 서빙 및 BE 애플리케이션으로 데이터 요청이 문제없이 이루어진다는 것을 알 수 있다.
이 후에는,,
포스팅한 내용까지는 서버를 구축하고 간단한 테스트한 내용에 그쳤지만 메인은 이 후 작업하면서 발생한 내용들이다. Github Actions를 통해 CI/CD 구축, OOM(Out of Memory)로 인해 서버 폭발, 그리고 눈물을 머금고 서버 스펙 업그레이드까지 많은 일들이 있었는데 찬찬히 포스팅 해보도록 하겠다.