이번 프로젝트는 꽤나 재밌었습니다. 도메인도 좋고 IoT 기기와 연동하는 웹사이트는 처음이라 스마트팜과 여러 프로토콜을 열심히 학습하면서 개발했던 기억이나네요. 기능 개발도 좋았지만 시스템을 어떻게 구축할지 고민했던 것이 흥미로워 재밌게 개발했습니다. 

당시 어떤 환경을 구축했었는지, 어떤 고민을 했었는지 정리해서 공유해보려고 합니다.

 

1. 구성 요소

1) IoT 기기 : 식물재배기입니다. 카메라와 환경센서가 부착되어있습니다.

2) FTP 서버: IoT 기기로부터 이미지 파일을 수신하고 저장합니다.

3) Python 스크립트 : 특정 폴더의 변경사항을 감지하고 추론 알고리즘으로 이미지를 분석해 데이터베이스에 저장합니다.

4) Node.js 웹 서버 : 데이터베이스의 변경사항을 감지하고 SSE를 통해 클라이언트에 알림

 

2. 데이터 흐름

1)  IoT 기기가 이미지를 캡처하고  <DeviceId-날짜> 형식의 파일명으로 FTP 서버에 전송합니다.

2) Python 스크립트로 폴더 변경을 감지하여 전송된 이미지를 추론하여 데이터를 추출합니다.

3) 추출된 데이터를 데이터베이스에 저장합니다.

4) 파일명을 바탕으로 파일을 이동시켜줍니다. (폴더명: DeviceId/날짜/example.jpg)

5) 데이터베이스 업데이트 발생 시 웹 서버가 SSE를 통해 클라이언트에 알립니다.

6) 웹 클라이언트가 실시간으로 업데이트된 정보를 표시합니다.

 

3. 구현사항

1) FTP 서버인 FileZilla 서버 연결 설정

IoT 기기에 설정한 FTP 설정, 호스트 / 사용자명 / 비밀번호 / 포트를 일치시킵니다

 - 호스트 : example.com

 - 사용자명: user

 - 비밀번호: root

 - 포트: 21 (기본 FTP 포트)

 

2) 자동화 설정 

FileZilla 인터페이스를 통해 FTP 서버에서 파일을 자동으로 다운로드 합니다.

filezilla -c "open ftp://user:root@example.com; cd /uploads; lcd C:/local/image/folder; get *.jpg; close; exit"

 

 

3) Python 스크립트 (파일 감지 및 추론 알고리즘 실행)

Python 스크립트의 추론 알고리즘을 만드는 부분은 외부 기관과 협력했습니다. 식물의 생육 데이터를 구축하고 적절한 모델을 정의하여 학습시키고 현재 프로젝트에 알맞은 모델을 만드는 과정이 흥미로웠습니다. 

 

약간의 파라미터 변화로 결과물이 완전히 달라지는 모습에 뜬금없이 영화 나비효과가 생각나기도 했습니다.

(아직도 어린 에반이 칼 들고 서있는 장면보면 놀랍니다, 개인적으로 감독판 엔딩은 너무하다 생각해요..)

 

처음엔 Python 스크립트에 따른 결과물을 DB에 별도로 저장하고 웹 서버에서 DB 내용을 한번 가공해서 다른 collection 에 저장하도록 구현했는데 프로젝트가 수정되면서 추론 데이터를 사용하는 일이 없어졌습니다. 파일명을 기준으로 파일을 이동시키는 기능도 웹서버에서 진행하다가 해당 과정은 Python 스크립트에게 맡겨 중간 과정을 하나 생략했습니다.

 

큰 차이는 아니라고 생각했는데 중간 단계를 하나 줄이니 소요 시간이 3s -> 2.5s로 단축됐습니다. 

 

 

4) 웹 서버 설정 (Node.js)

웹 서버에서는 다음과 같은 기능을 합니다.

- MongoDB 감지

- MongoDB 업데이트 시 SSE를 통해 클라이언트에 알림

- 저장된 이미지 파일 제공 (아래 예시에선 다루지 않음)

 

4.1)(최적화 x)

let clients = []; // 연결된 SSE 클라이언트 리스트

// SSE 엔드포인트
app.get('/events', (req, res) => {
  // SSE 헤더 설정
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  res.write(`data: ${JSON.stringify({ message: 'Connected to event stream' })}\n\n`);

  clients.push(res); // 클라이언트 연결 추가

  // 연결 종료 시 clients 리스트에서 제거
  req.on('close', () => {
    clients = clients.filter((clientRes) => clientRes !== res);
  });
});

// MongoDB Change Streams
async function startChangeStream() {
  const changeStream = collection.watch();

  changeStream.on('change', (change) => {
    if (change.operationType === 'update') {
      // 연결된 client 모두에게 이벤트 전송
      clients.forEach((res) => res.write(`data: rendering\n\n`));
    }
  });
}

 

시연을 목표로 프로젝트를 진행하니 유저가 많은 경우를 생각하지 못했습니다.

DB가 업데이트되면 모든 클라이언트에게 알림을 준다는 걸 나중에야 알게되었습니다🫢 

 

4.2) (최적화 o)

클라이언트를 Id 별로 접속하고 서버는 해당 userId 에 대한 데이터만 전송하게 변경했습니다.

// 유저별 연결 저장
const clients = new Map();

// SSE 연결 엔드포인트
app.get('/events/:userId', (req, res) => {
  const userId = req.params.userId;

  // 연결 저장
  clients.set(userId, res);

  // 연결 종료 처리
  req.on('close', () => {
    clients.delete(userId);
  });
});

// MongoDB Change Streams 설정
async function watchMongoChanges() {
  const changeStream = collection.watch();

  changeStream.on('change', (change) => {
    const userId = change.fullDocument?.userId; // event에 해당하는 user 추출
    if (!userId) return;

    const userClient = clients.get(userId);
    if (userClient) {
      userClient.write('data: rendering\n\n');
    }
  });
}

 

 

5) 클라이언트에서 SSE 설정 (Next.js)

// 클라이언트 측 SSE 연결 설정
const eventSource = new EventSource('/events');

// 이벤트 수신 시 화면 업데이트
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data === 'rendering') {
    // UI 업데이트 로직
    updateUI(data);
  }
};

eventSource.onerror = (error) => {
  console.error('SSE connection error:', error);
  eventSource.close();
};

 

 

4. FTP, MQTT, SSE 프로토콜 선택 이유

 FTP: 이미지 파일을 전송할 때 먼저 HTTP를 생각했습니다. 그런데 사용하기로 했던 IoT 기기가 비용 이슈로 다운그레이드 되면서 성능 이슈가 있었고 HTTP는 기본적으로 Header에 데이터가 많이 들어가기 때문에 보다 오버헤드가 적고 안정적인 FTP를 사용했습니다. MQTT는 경량화된 메시징 전송에 특화된 프로토콜로 대용량 파일 전송에는 적합하지 않았습니다.

 

MQTT: FTP와 동일한 이슈로 HTTP는 사용하지 않았습니다.

 

SSE: WebSocket을 사용하려고 했는데 서버 pc가 이미 하는 일이 많다고 생각했고 특히, 양방향이 아닌 단방향 통신이 필요해 서버 리소스도 덜 사용하는 SSE를 사용했습니다.

 

5. 후기

위 과정을 통해 IoT 에서 이미지를 취득 -> 추출한 데이터를 클라이언트에게 실시간으로 전달하는 환경을 구축할 수 있었습니다.

환경데이터를 주고받은 내용은 MQTT 프로토콜을 이용했는데 내용을 추가하고 싶었지만 본문 구성이 많이 달라져서 제외했습니다

(이럴때마다 글 잘쓰는 분들이 부럽네요..)

 

FTP, MQTT, SSE 프로토콜을 다뤄보면서 네트워크 통신의 전반적인 흐름을 이해할 수 있었습니다. 그리고 생각보다 재밌어서 TCP/IP 랑 기본적인 네트워크도 같이 스터디했습니다. 아주 좋은 현상이네요 

 

AI가 점점 세상을 가속화하고 있는걸 체감하는 요즈음입니다. 그런데 AI 도구를 열심히 이용하고 있는것과 별개로 저는 책에 손이 가네요. 인스타 릴스, 유튜브 쇼츠 등 대기업에서 점점 짧고 강력한 유혹으로 시선을 뺐어가는데, 제 나름대로 도파민을 컨트롤할 방법을 찾다 보니 책 만한게 없더라구요.

김영하 작가님을 아시나요? 저는 검은꽃을 시작으로 작가님 책을 찾아보기 시작했는데 최근 읽은 빛의 제국은 나름 생각할 화두를 던져줬습니다. 주인공은 북에서 온 간첩이지만 인생의 절반을 평범한 한국인인척 연기합니다.

그 모습을 보며 나도 누군가에게 연기를 하며 지내고 있구나 싶었어요. 적당한 회사 동료들에게는 사무적인 모습으로, 교회 아이들에게는 상냥한 선생님의 모습으로, 부모님에게는 그래도 노력하는 아들로.

 

다들 저마다의 방법으로 적당히 연기를 하며 지내고 계시나요? 저는 뭐가됐든 제 모습이 다른 사람에게 긍정적인 영향을 줄수 있으면 좋겠네요.

'On-Premise' 카테고리의 다른 글

On-Premise 환경에 React, Express 프로젝트 배포하기  (0) 2025.04.30

1. 들어가며

클라우드 환경이 대세인건 맞지만 보안이나 여러가지 이유로 On-premise 환경에서 애플리케이션을 운영하는 경우도 많습니다.

저의 경우 회사에 유휴 pc가 있어 On-premise에 배포하게 되었고 그 과정을 정리해보려합니다.

 

2. 환경 구성

기본 환경을 정리하면 다음과 같습니다.

OS(윈도우)

Node.js

PM2

Nginx

SSL 인증서

방화벽

포트포워딩

 

3. 사전준비

1) 환경 설정 

  • Node.js 설치 : 공식 웹사이트 다운로드
  • PM2 설치 : npm install -g pm2
  • Nginx 설치 : 공식 웹사이트 다운로

2) SSL 인증서 구입 (가비아 발급)

무료인 Let's Encrypt도 고려해봤으나 다음의 문제로 가비아로 결정했습니다

  • 인증서 문제 발생에 따른 보증이 확실하다
  • Let's Encrypt는 90일마다 갱신해야하는 번거로움이 있다.
  • 도메인을 가비아에서 구매해서 SSL 연동하기가 더 간편했다.

 

4. 배포 과정

1) PC에 React, Express 프로젝트 설치

  • git clone
  • npm install

2) React 빌드

  • npm run build

3) Nginx 설정

server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    
    # HTTP를 HTTPS로 리다이렉트
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name your-domain.com www.your-domain.com;
    
    # SSL 인증서 설정
    ssl_certificate /etc/ssl/certs/your-domain/fullchain.crt;
    ssl_certificate_key /etc/ssl/private/your-domain.key;
    
    # SSL 프로토콜 설정
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
    
    # SSL 세션 캐시 설정
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    
    # HSTS 설정 (선택사항)
    add_header Strict-Transport-Security "max-age=63072000" always;
    
    # React 앱 서빙 (정적 파일)
    location / {
        root /path/to/your-react-app/build;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    # Express API 요청 프록시
    location /api {
        proxy_pass http://localhost:5000;
        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;
    }
}

 

4) Nginx 재시작

  • nginx -s reload

 

5) PM2 시작

  • pm2 start server.js --name my-server

6) 방화벽에서 Nginx 포트 열기

 

  • 제어판 → 시스템 및 보안 → Windows Defender 방화벽 → 고급 설정 클릭
  • 왼쪽 메뉴에서 인바운드 규칙 클릭
  • 오른쪽에서 새 규칙(New Rule) 선택
  • 포트(Port) 선택 후 다음(Next)
  • TCP, 특정 로컬 포트: 80, 443 입력
  • 연결 허용(Allow the connection) 선택
  • 규칙이 적용될 네트워크(도메인, 개인, 공용) 선택
  • 규칙 이름: 예) Nginx HTTP/HTTPS

 

7) 포트포워딩

포트포워딩으로 라우팅을 해줘야 외부에서 On-premise 환경에 배포한 프로젝트에 접근할 수 있습니다.

ipconfig // # 내부 ip 확인 (ex: 192.168.0.10)

 

  • 공유기 / 라우터 관리자 페이지 접속
  • 포트포워딩 설정 메뉴에서 아래 내용 설정
더보기

- 외부포트 80(HTTP), 443(HTTPS)

- 내부 IP (192.168.0.10)

- 내부 포트 80

- 프로토콜 TCP

 

5. 유지보수

1) 로그 관리

로그는 PM2 자체적인 로그관리 시스템을 활용했습니다

# 로그 위치 확인
pm2 logs       # 실시간 로그 보기
pm2 logs my-server  # 특정 앱 로그

# 로그 파일 경로
~/.pm2/logs/my-server-out.log      # 일반 로그
~/.pm2/logs/my-server-error.log    # 에러 로그

 

2) 자동 재시작

OS가 윈도우인 PC다보니 한번은 윈도우 업데이트로 서버가 내려간적이 있었습니다

다행히 테스트 단계인 프로젝트여서 큰 문제는 없었지만 실제 상용화 중인 프로젝트에서 서버가 내려갔다고 생각하니까 문제가 심각했습니다

PC의 윈도우 업데이트를 수동으로 관리하게 변경하였고 불가피하게 PC가 재시작되어도 PM2가 자동 재시작되게 설정해줬습니다

pm2 start server.js --name my-server # 서버 실행 상태에서
pm2 save                      # 현재 상태 저장
pm2 startup                   # 부팅 시 자동 시작 등록

 

6. 마치며

AWS, Vercel, FileZilla, Heroku, Firebase 등등 많은 클라우드에 배포해봤지만 On-Premise 배포는 안해봤는데 하나하나 환경 설정해주는 맛이 있었습니다. 특히 환경설정 명령어가 리눅스 명령어가 대부분이라 순순히 설치되어주지 않더군요..

Nginx도 꽤 흥미로웠습니다. 그동안 정적 파일 서빙만 한다고 생각했는데 리버스 프록시와 SSL 설정을 하면서 이것저것 시도해볼 수 있었습니다. Next.js 프로젝트도 배포해봤는데 React랑은 config 파일 작성이 많이 달라 삽질을 많이 한 기억이 나네요.

개발에서 서버/인프라 중요성이 큰 만큼 다뤄볼 수 있는 기회가 왔을 때 해보는건 참 좋은 것 같습니다.

그래도 아직은 프론트엔드 개발에 집중하고 싶네요..😊

+ Recent posts