매번 서버 설치할때마다 찾아가면서 해서

이번에 작업 절차 정리한다.

 


라이트 세일은 이분 블로그가 잘되어있다

https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-Amazon-Lightsail-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%9A%9D%EB%94%B1-%EA%B5%AC%EC%B6%95%ED%95%98%EC%9E%90

 

[AWS] 📚 Lightsail 사용법 총정리 - 웹서비스를 뚝딱 구축하자

AWS Lightsail 서비스 아마존 Lightsail은 AWS에서 만든 가상 프라이빗 서버 (VPS) 이다. 프로젝트를 빠르게 시작하는 데 필요한 가상머신(compute), SSD기반 스토리지, Networking, 로드밸런서, DNS관리, 고정IP, O

inpa.tistory.com

 

#특별한 일이 아니면 리눅스로 설정하고 내부ip만 사용할 was 서버는 ip6로만 하면 $4달러를 적용할 수 있다.

(회사망에서는 못쓴다.. 괜히 만들었다가 다시 원복..)

https://test-ipv6.com/index.html.ko_KR

출처: https://satisfactoryplace.tistory.com/413?category=704093 [만족:티스토리]

 

#특정 ip제외하고 접근을 막는다.

# 본인의 SSH 접속 IP를 허용 (예: 203.0.113.4가 본인 IP인 경우)
sudo iptables -A INPUT -p tcp --dport 22 -s 203.0.113.4 -j ACCEPT

#github action 관련 ip 허용

sudo iptables -A INPUT -s 185.199.108.0/22 -j ACCEPT
sudo iptables -A INPUT -s 140.82.112.0/20 -j ACCEPT
sudo iptables -A INPUT -s 143.55.64.0/20 -j ACCEPT

# 현재 내 그룹의 아이피는 허용

sudo iptables -A INPUT -s  203.0.113.0/24 -j ACCEPT

# 루프백 설정

sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

#그외 전부 막음

sudo iptables -P INPUT DROP

# aws 인스턴스 내부 ip 허용

sudo iptables -A INPUT -s 172.26.0.0/16 -j ACCEPT
sudo iptables-save | sudo tee /etc/sysconfig/iptables > /dev/null
sudo systemctl restart iptables

 

#서버시간을 한국 시간으로 변경

sudo timedatectl set-timezone Asia/Seoul

# 확인

timedatectl

 

# 어플리케이션 설치할 data 폴더 생성

sudo mkdir /data

sudo chown ec2-user:ec2-user /data

 

# 도커 설치

sudo  yum install docker

 

# 권한 부여

sudo usermod -a -G docker ec2-user

 

# auto-start에 docker 등록

sudo chkconfig docker on

 

# 도커 버전 확인

docker -v

 

# 도커 컴포즈 설치

최신 docker compose를 해당 링크에서 받을 수 있음

sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
// 권한 부여

sudo chmod +x /usr/local/bin/docker-compose

 

// 설치 확인

docker-compose version

 

어플리케이션을 설치 할 건데 웹서버 - was서버  구성

 

웹서버는 다른 어플리케이션이 들어와야해서

사용자 -> 프록시 서버 -> app1(front) -> app1(back)

                                  -> app2(back)

 

각각의 앱은 프록시를 통해 was에 붙거나 바로 다이렉트로 접근한다.

 

 

 

완성작

📮 대망의 마지막 – 리포트까지 자동화하자!

드디어 마지막 이야기입니다. 이제 자동화는 잘~ 돌아가고, Illustrator도 말을 듣고, Gmail도 착착 메일을 보내줍니다. 하지만 뭔가 허전했죠.

“파일만 보내니까 너무 밋밋해…”

그래서 생각했습니다.
“메일에다가 무슨 데이터를 받아서 어떤 작업이 이뤄졌고, 얼마나 걸렸는지도 같이 정리해서 넣으면 더 멋지지 않을까?”

이제 등장해야죠, 우리의 생성형 AI 친구 – Gemini!


📊 작업 리포트는 LLM에게 맡기자

n8n 흐름 중간에 Gemini를 호출해서, 사용자 입력 + 처리 시간 등을 요약 리포트로 정리하게 만들었습니다.
그리고 이 결과를 메일 본문에 파일과 함께 넣는 게 목표였어요.

하지만 여기서부터 또 한참을 삽질하게 됩니다…


😵 n8n 초보의 삽질 다섯 가지

1. 중간 노드 접근하기까지 반나절

처음엔 "순차적으로만 흐른다"고 생각해서, LLM을 거친 뒤 파일을 다시 참조하는 방법을 몰랐습니다.
나중에야 깨달았죠 – n8n에서는 중간 노드도 직접 접근 가능하다는 걸요!

{{ $("파일생성노드이름").binary["data"] }}

…이걸 몰라서 얼마나 헤맸는지 😇


2. 바이너리 다루기… 미궁 속으로

n8n의 기본 데이터 흐름은 JSON이라 그런지, 바이너리 데이터를 노드에 넣는 방법을 몰랐습니다.
LLM에게 물어봤지만 버전이 안 맞았는지 뻔한 헛소리만 들었고, 결국 직접 Base64를 활용하여로 해결.


3. base64로 string 만들기 → 다시 바이너리? 실패의 연속

"그래, base64로 변환한 다음 string을 바이너리로 다시 만들면 되지 않을까?"
→ 그렇게 간단하지 않았습니다.

item이 여러 개인데 하나의 string만 처리해서는 안 됐고,
라인마다 따로 처리해야 했는데 그걸 몰라서 계속 실패… 또 시간 줄줄.


4. Google Sheet 업데이트 안 되는 문제

메일을 보낸 다음, Google Sheet에 ‘완료’로 태그 업데이트를 하려 했는데 첫 번째 줄만 반영됨.
row_number를 이용해서 각 행을 정확히 지정해주는 방식으로 해결!


5. 파일은 하나만 만들어야 하는데…

파일과 데이터는 1:N 관계인데 나중에 바이너리 파일로 변경하는 시점에

같은 파일이 각각 두 개씩 생기는 기이한 현상.
결국 Code 노드로 데이터를 강제로 하나의 item으로 묶어서 해결.

 

 

🧠 n8n을 다뤄 보면서 깨달은 세 가지

✅ 1. 노드 접근 방식은 자유롭다!(이걸 몰라서... 보낸 시간이...)

  • 이전 노드 직접 접근
    {{ $("파일노드").item.json.title }}
  • 현재 item의 데이터
    {{ $json['상태'] }}

🛠️ 2. Set 노드는 데이터를 정리하는 데 최적

  • 필드 이름 변경
  • key 정리
  • 고정값 추가 등등

🔧 3. Code 노드는 만능 도구

  • 복수 아이템 합치기
  • JSON ↔ Binary 변환
  • 복잡한 조건 처리

🎉 끝맺음

프로젝트 초반엔 그까짓 것 "자동화 좀 해보자~"였는데,
끝나고 보니 Illustrator부터 메일 전송, 요약 리포트 작성까지 모두 자동으로 돌아가는 작은 인하우스 플랫폼이 완성됐습니다.

물론 더 예쁘게, 더 잘 만들 수 있는 여지는 많겠지만,
이번 과제를 통해 진짜 많은 걸 배웠습니다.
무엇보다 n8n에 대해 아무것도 몰랐던 무지렁이에서 초보 수준까진 올라온 느낌이랄까요?  😎


다음에 또 삽질기가 있다면 돌아오겠습니다.
읽어주셔서 감사합니다!

 

안녕하세요!
오늘도 학교 과제 덕분에 머리를 싸매며 삽질한 일러스트레이터 자동화 프로젝트 이야기를 이어가겠습니다.
지난 번에는 ExtendScript Toolkit을 통해 Illustrator가 실제로 스크립트로 제어될 수 있다는 걸 확인했었죠.

이제 본격적으로 달려보죠.

🔧 자동화 프로세스의 큰 줄기

우리가 만들고자 한 건 단순했습니다.
사용자의 데이터 입력 → 디자인 작업 자동화 → 결과물 생성 및 전송 → 정리까지, 모든 게 자동으로 처리되는 시스템.

프로세스는 이렇게 짰어요:

  1. 데이터 받기 : 사용자가 Google Sheet에 정보 입력
  2. 작업 지시 : Adobe ExtendScript Toolkit 실행하여 Illustrator 제어
  3. 파일 생성 : AI + JPG 파일 저장
  4. 메일 발송 : Gmail로 첨부파일 전송
  5. 정리 : 작업 완료 후 파일 삭제 및 기록 정리

이 모든 동작이 하나의 플랫폼 아래서 통합되고 관리되어야 했죠.
그 주역은 바로… n8n 이었습니다.


📦 자동화 도구들, 이제는 본격적인 조합 시작

각각의 기능을 담당할 도구들은 이미 정해져 있었습니다:

  • Google Sheet : 사용자로부터 데이터를 받아올 창구
  • Adobe ExtendScript Toolkit + PyAutoGUI : Illustrator 자동 수정 담당
  • Illustrator : 자동으로 텍스트 변경하고 파일 저장
  • Gmail : 결과물을 첨부해서 보내주는 메일봇
  • Gemini : 사용자 데이터를 정리하고 설명문 작성까지

이 모든 걸 n8n 이라는 워크플로우 엔진 하나로 연결하려는 야심찬 계획이었죠.
(굳이 n8n 없어도 되긴 되는데.....)


⚙️ 사전 작업, NAS에 n8n 설치하기

자동화를 하려면 일단 서버가 필요합니다.
그래서 저희는 집에 있는 Synology NAS n8n 을 설치했습니다.
항상 켜져 있고 외부 접속도 가능하다는 점에서 정말 좋은 선택이었어요.

설치 방법은 크게 두 가지가 있었는데,
PostgreSQL과 연동하는 방식도 있었지만, 결국 N8N 자체 서버 + Web Station을 활용하는 간단한 방식으로 성공했고,
이후엔 웹브라우저로 n8n.XXXX.synology.me 접속만으로도 작업이 가능하게 되었죠.

(참고로 설치 가이드는 이곳 을 참고했어요!)


🔑 Google OAuth 인증, 처음 만드는 API 계정

복수 계정을 만들려고 PC에서 만들려고 했지만 계속 인증 오류가 나서 결국 모바일로 계정을 새로 만들었네요... 😅

 

n8n에서 Google Sheet를 접근하려면 당연히 OAuth 인증이 필요했습니다.
OAuth 설정이 완료된 이후, n8n 에서 인증키를 등록한 뒤

Google Sheet의 데이터를 읽어오는 건 n8n의 내장 노드로 금방 해결할 수 있었어요.
이렇게 데이터를 얻으면, 다음 단계로 넘어갈 준비가 끝났다는 뜻입니다.


💻 "웹"에서 온 데이터, 어떻게든 "로컬"에 심어보자 – n8n과의 사투

자동화 프로젝트가 어느 정도 윤곽을 갖추고 나니, 실제로 작동하는 흐름도 눈으로 확인할 수 있게 되었어요.
Google Sheet에 데이터를 입력하면 Illustrator 파일이 자동으로 생성되고, JPG까지 변환되어 Gmail으로 전송되는 일련의 과정이 말이죠.

 

하지만 여기엔 한 가지 큰 문제가 있었어요.

(Google Sheet)에서 온 데이터를, 어떻게 로컬 (일러스트레이터가 돌아가는 PC)로 가져갈 것인가?”


📥 웹에서 로컬로 데이터를 내리는 방법, 이게 이렇게 복잡할 줄이야…

처음엔 단순하게 생각했어요.
“n8n에서 Google Sheet API로 바로 읽어서 스크립트에 던져주면 끝 아냐?”
그런데 문제는 ExtendScript Toolkit 로컬 환경에서만 동작 한다는 거였죠.

그래서 n8n에서 웹훅(webhook) 을 활용해 이런 방식을 시도했습니다:

  1. Google Sheet 데이터를 n8n에서 받아옴
  2. JSON 형식으로 저장
  3. 웹훅 URL을 통해 로컬 서버에서 이 데이터를 GET 요청 으로 다운로드
  4. ExtendScript가 이 파일을 읽고 작업 시작

이렇게 하면 n8n이 중간 다리 역할을 해주면서,
로컬에서도 웹에서 받은 데이터를 사용할 수 있게 되더라고요.


⚠️ 그런데 사실… 더 쉬운 방법도 있었어요.

사실 Google Sheet 자체를 pyAutoGUI 시점에 직접 접근할 수도 있고,
OAuth 인증을 걸치면 스크립트 단독으로도 데이터를 읽을 수 있긴 합니다.
하지만 저희는 이미 n8n 위에 모든 로직을 얹어놨고,
이걸 또 처음부터 다시 짜기엔 그냥 너무 싫어서...…

꾸역 꾸역 기존 구조에 맞춰서라도 해보자…”

 

결국 웹훅으로 대기하고, 파일을 만들어 로컬로 다운받는 방식을 택하게 되었죠.
쉽진 않았지만, 일단 돌아가기 시작하면 그만~~~~~~


📤 반대로, 로컬에서 웹(n8n)으로 결과물을 보내는 건?

AI 파일과 JPG 파일이 생성된 후에는,
이걸 다시 n8n에게 넘겨줘서 Gmail로 발송해야 했습니다.
이번에도 같은 방식을 썼어요: 로컬에서 웹훅으로 파일 업로드

  1. Illustrator 작업 완료 → AI & JPG 파일 저장
  2. PyAutoGUI로 ExtendScript 종료 후, Python 스크립트 실행
  3. 생성된 파일들을 POST 요청으로 n8n 웹훅 주소에 업로드
  4. n8n에서 Gmail 노드 호출하여 메일 전송

여기서도 역시 “왜 그냥 로컬에서 구글 인증 받고 바로 메일 보내면 안 돼?”라는 질문이 떠올랐지만,
이미 n8n 위에 전체 흐름을 얹어놓은 상태라서…

n8n에서 다 처리되도록 만들고 싶다!
(명색이 자동화 프로젝트인데, 분산된 시스템은 싫단 말이에요…)


🧠 명색이 AI 과목 과제인데, LLM 없이는 좀 그렇잖아?

메일 발송까지는 잘 됐는데,
막상 메일을 보면 뭔가 허전하더라구요.
그래서 이번엔 Gemini 에게 요청 데이터와 변경 내용을 넘겨서 요약본을 만들어달라고 했습니다.

이 자동화 과정 중에 AI도 넣고 싶었습니다.

"이거 다 스크립트랑 매크로로만 해결했네. 그래도 명색에 AI 과목 과제인데 첨가향이라도 뿌려야 하나"

 

그래서 등장한 주인공이 바로 LLM , 이번엔 Google Gemini 였습니다.
AI가 직접 일러스트를 그리는 건 아니지만, 사용자가 입력한 데이터와 그에 따른 텍스트 변경 내역을 정리 해서 보여주는 역할을 맡겼죠.

 

이제부터 비극이 시작되었지요..

 

안녕하세요.

오늘은 학교 과제 덕분에 시작하게 된 일러스트레이터 자동화 프로젝트 이야기 를 준비했습니다.
처음엔 ‘자동화’라는 단어부터 낯설고 어렵게 느껴졌지만, 막상 시작해보니 그저 삽질의 연속....

 

과제지만 의미있는 주제를 삼아보자

우리 팀에서 이 자동화 프로젝트가 본격적으로 움직인 건, 팀원 중 한 분이 “일러스트레이터 작업을 자동화해 보고 싶다.” 라고

하셔서 현업에서 실제로 어떤 작업인지 들어보고, 저는 바로 반응했죠.

 

그냥 배워서 직접 하시는 게 더 빠르지 않을까요?

 

윈도우 기반 앱인 Illustrator를 스크립트로 조작하는 건 불가능할 거고, 매크로나 RPA 같은 걸 도입해야 하는데…
그걸 누가 짜겠어요..
결국 다른 아이디어를 구상하려던 찰나, 운명 같은 검색 결과 를 마주하게 됩니다.

 

과연, 자동화는 가능할까?

웹 서핑 도중 문득 이런 궁금증이 들었어요.

"혹시 우리처럼 Illustrator 작업을 자동화해보고 싶어 하는 사람들이 있을까?"

 

검색 결과, 세상은 역시 넓고 깊었습니다.
Adobe ExtendScript Toolkit 이라는 도구를 발견하고는 눈앞이 확 밝아졌죠. 💡
이걸 계기로 저희는 자동화 여정에 발을 내딛게 되었답니다.

 

오오, 이런 신세계가 있다니!

 

사용한 도구들: 여정의 동료들.

프로젝트를 진행하면서 함께했던 도구들을 소개할께요.

  • Adobe Illustrator : 텍스트 레이어 수정 (이번 과제의 핵심)
  • Adobe ExtendScript Toolkit : 일러스트레이터 제어용 확장 도구
  • PyAutoGUI : ExtendScript Toolkit을 자동 조작하는 자동화 도우미
  • n8n : 전체 프로세스를 관리하는 자동화 플랫폼 (자동화 위에 자동화!)
  • Gmail, Google Sheet, Gemini : 데이터 수집과 활용을 지원하는 든든한 동료들

첫 목표: 일러스트레이터가 정말 스크립트로 제어될까?

우선 확인해야 할 건 하나뿐이었어요.

"일러스트레이터가 ExtendScript로 진짜 조작이 가능한가?"

 

다행히 요구하는 작업은 단순했습니다. 템플릿 파일을 열고 텍스트만 바꾸면 됐으니까요!

다만 그 양이 만만치 않았다는 거… 😅

 

그래서 다음과 같은 단계를 세웠지요..

  1. 스크립트가 찾기 쉬운 레이어 이름 규칙 만들기
  2. ExtendScript로 원하는 레이어 탐색
  3. 찾은 레이어의 텍스트 변경

처음 다뤄보는 도구라 걱정이 많았지만, ExtendScript는 JavaScript 기반 이라 그런지 의외로 접근성이 좋았어요.

“이 정도면 해볼 만하겠는데?”


실제로 코드를 짜보니 오! 신기하게도 잘 작동했습니다. ✨
이거 그냥 날로 낼름 먹겠는데 

그런데… 자동화는 그렇게 끝나지 않아요 😅

처음엔 단순한 스크립트 하나로 시작했는데, 어느새 PyAutoGUI, n8n 등등…
점점 더 많은 자동화를 위한 자동화를 하고 그 자동화를 위해 또 다른 자동화를......

헐...

 

정말 그때는 몰랐어요. 앞으로 얼마나 더 많은 도구들과 머리 싸매야 할 지를 말이에요.

도구들 간 만보다가 예전 셀레니움도 로그인 때문에 역시 엄청 애먹은적이 있어

네이버 카페처럼 "사람만 들어오세요" 방패를 세운 곳에선 말이죠.

 

쉽고 직관적인 그래서 결국...
PyAutoGUI라는 눈물의 자동화 도구에 손을 댔습니다.


1. PyAutoGUI: 사람 흉내는 장인, 하지만…

처음엔 기분 좋았습니다.

"이걸로 브라우저 열고, 로그인도 하고, 이미지도 클릭하고 다 하겠군!"

 

✅ 진짜 마우스가 움직인다
✅ 진짜 키보드가 타이핑된다
✅ 진짜 브라우저로 로그인해서 들어간다!

🟢 장점은 단 하나,

 

"사람처럼 실제로 클릭하니 로그인 100% 성공!"

 

이거 하나는 진짜 꿀이었습니다. 나머지는... 흠...


2. 현실은 해상도 지옥

PyAutoGUI는 좌표 기반입니다. 그래서:

  • 노트북과 데스크탑에서 좌표 다름
  • 브라우저 크기만 달라도 위치 달라짐
  • 크롬 툴바 하나만 있어도 오차 발생
pyautogui.click(324, 712)  # ← 이 좌표, 다른 PC에선 허공임

 

심지어 창 최대화 안 해놓으면 자동화 틀어짐 😵

내가 만든 프로그램인데, 내가 쓴 해상도 아니면 못 씀


3. 이미지 찾기는 더한 지옥

“그럼 이미지로 UI 요소 찾으면 되지 않을까?” → OpenCV 사용

 

하지만 현실은?

  • 글자 하나만 달라도 못 찾음
  • 브라우저 스케일 조금만 바뀌어도 못 찾음
  • 반응형 UI라 DOM 구조가 달라지면 무용지물

이미지로 찾으려다 정신 줄 놓을 뻔 🤯


4. 게시물 구조는 왜 자꾸 바뀌는 거야

가장 핵심은 게시글 클릭 & 페이지 넘김이었는데...

  • 게시글 제목 길어서 두 줄이면 → 아래 네비게이션 위치가 바뀜
  • 평소에 글이 15개가 있는데 마지막 페이지 도착하면 글이 1~15개 사이에 있음.
  • 평소에 10개인데 마지막 페이지 도착하면 페이징 번호가 1~9개 사이로 뭐가 있을지 모름

좌표 기반 자동화는 이런 변화에 너무 약했습니다.

 

그래서 OCR 도입 시도!


5. Tesseract OCR: 나를 속인 너의 음성...

"동적으로 변화하는 숫자 인식해서 페이지 넘버 추적하자!"

 

결과는? 두구 두구 두구

 

  • '3' → '8'
  • '5' → 'S'
  • '10' → '1O' 또는 'lO'

이건 무슨 캡챠보다 어려운 숫자 인식… 😵‍💫


6. 결국 DOM 접근을 시도하다

크롬 개발자 도구 열고, 자바스크립트로 DOM 추출.

document.querySelectorAll('.board-box a')

 

→ 게시물 수 확인 후 PyAutoGUI로 클릭 위치 계산

 

근데 절차는....(조상님이 대신해 줄려나?)

  1. 개발자 도구 열고
  2. 콘솔에 JS 입력하고
  3. 결과 복사하고
  4. PyAutoGUI로 넘기고

자동화가 아니라 수동화된 자동화가 되어버림 😮‍💨


7. 최악은 타이밍 지옥

렌더링 완료 전에 PyAutoGUI가 클릭하면?

  • 요소가 안 떠서 클릭이 안 됨
  • 커서는 이동했는데 클릭은 씹힘
time.sleep(5)
pyautogui.moveTo(..., duration=1.5)
pyautogui.press('tab')

 

갖은 꼼수를 써봤지만...

속도는 느려지고, 디버깅은 지옥이고, 안정성은 로또 🎲


8. 최종 결론

💣 해상도 바뀌면 이 프로그램은 못 씁니다. 끝.

 

이건 내 컴퓨터에서만 돌아가는 맞춤형 자동화.

내가 만든 건 크로스 플랫폼 자동화가 아니라, 단일 기기 매크로.


🎯 배운 점 요약

시도 결과

PyAutoGUI 로그인 OK → 나머지는 불안정
OCR 인식률 구림
OpenCV 환경 따라 결과 달라짐
JS + PyAutoGUI 혼합 정신 혼미, 유지보수 불가

✍️ 마무리하며

이 여정을 통해 진짜 배운 건...

UI 흉내내기보다, 진짜 데이터 구조를 이해하는 게 진짜 자동화다.

 

하지만 현실에선 결국 PyAutoGUI 같은 꼼수도 필요하다는 것.

어쩌면 자동화는 늘 창과 방패의 싸움이 아닐까요?

 

전쟁의 후 전리품

 

읽어주셔서 감사합니다!!

지난 1편에서는 네이버 카페에서 이미지를 손으로 저장하다가 🤦‍♂️
Octoparse → Selenium까지 시도하게 된 여정을 소개했는데요.

 

이번엔 그 이후,
"도대체 어떤 도구를 써야 잘 되는 걸까?" 를 파헤쳐보며 자동화 삽질 일지를 공유합니다.


👀 자동화는 선택이 아닌 필수

네이버 같은 대형 플랫폼은 보안도 단단하고, UI도 자주 바뀝니다.

처음에는 “날먹각이다!” 싶었던 툴들이 시간이 지나면 무용지물이 되곤 했죠. 그래서 저는 다양한 자동화 도구를 하나하나 간을 보았습니다.

 

제가 누구입네꽈아아아아~


🧰 현지에서 접할 수 있는 크롤링 도구 비교

도구 장점 단점
크롬 확장 프로그램
(예: Image Downloader)
- 진입장벽 없음
- 단순 이미지 추출 용이
- 게시글 이동 시 무력화
- 반복 작업엔 부적합
Octoparse - 코딩 몰라도 사용 가능
- 빠른 프로토타이핑
- 로그인 유지 힘듦
- UI 변경에 취약
Selenium - 동적 페이지 완벽 대응
- 실제 브라우저처럼 작동
- 느림
- 감지 위험 높음
- 메모리 사용 큼
Requests + BS4 - 빠르고 가벼움
- 서버 부하 적음
- JS 렌더링 불가
- 로그인/세션 처리 어려움
Playwright - 최신 웹 기술 대응
- 인증 처리 강력
- 초반 설정 복잡
- 러닝커브 있음
PyAutoGUI - 모든 UI 제어 가능
- 네이버도 못 막음
- 해상도 민감
- 디버깅 지옥
- 불안정

💣 어떤 삽을 고를 까나?

1️⃣ Octoparse – “잘 되던 게 왜 갑자기…?”

  • 예전엔 OK → 몇 달 뒤 돌리니 로그인 풀림
  • 네이버 로그인 구조 변경이 문제
  • 쿠키도 소용 없음. 결국 다시 플로우 짜야 함

🧨 교훈: GUI 툴은 구조가 바뀌면 바로 무너짐


2️⃣ Selenium – “감지된다고요?”

  • 로그인까진 성공했지만…
    게시글 접근 시 감지 → 리다이렉트
options.add_argument('--disable-blink-features=AutomationControlled')
  • 네이버가 webdriver 속성으로 탐지하는 듯

🧨 교훈: 감지를 우회하려면 지속적인 트윅이 필요


3️⃣ Requests + BeautifulSoup – “빠르긴 한데…”

  • JS 렌더링 전 데이터는 전혀 안 옴
  • API 추적해도 세션/캡챠 우회는 거의 불가능

🧨 교훈: 깔끔하지만, 네이버에겐 역부족


4️⃣ PyAutoGUI – “사람처럼 한다고 다가 아님”

✅ 장점

  • 실제 브라우저에 로그인 가능 → 이것만은 개꿀

❌ 단점

  1. 좌표 기반 → 해상도 바뀌면 끝
  2. OpenCV로 이미지 찾기 → 오인식 심각
  3. 게시글 구조 변경 → 두 줄 제목으로 위치 바뀜
  4. OCR(Tesseract) 숫자 인식 → 처참한 정확도
  5. 개발자 도구 활용 → 돔 파싱 후 PyAutoGUI에 전달
    → 프로세스 복잡해지고 속도 느려짐
  6. 렌더링 딜레이로 클릭 씹힘 발생
  7. 디버깅: 마우스는 움직이는데 클릭 안 됨 🤯

🧨 교훈:
PyAutoGUI는 최후의 수단.
눈물 없이 못 씀.
그리고 해상도 다르면 그냥 못 씀. ← 핵심


💡 자동화 도구별 실전 팁

✅ Selenium 쓰는 법

  • headless 감지되니 옵션 조정 필수
  • 쿠키 저장 → 다음 세션 재활용
# 로그인 후 쿠키 저장
cookies = driver.get_cookies()
# 로그인 없이 쿠키만으로 로그인 유지
for cookie in cookies:
    driver.add_cookie(cookie)

✅ API 추적해서 Requests로 우회

  • 개발자 도구 → Network → XHR 추적
  • 실제 사용하는 URL을 복사해서 직접 요청
  • 단, 로그인 인증 우회는 별도 처리가 필요

✅ PyAutoGUI 생존 팁

  • 화면 해상도 고정 필요
  • 이미지 매칭 & OCR 혼용 시에도 완벽하지 않음
  • sleep()duration으로 타이밍 조절 필수
pyautogui.moveTo(300, 500, duration=1.5)
pyautogui.click()

🧭 마무리: 자동화는 창과 방패의 싸움

네이버 크롤링은 단순히 “이미지 저장”의 문제가 아니었습니다.
사이트 보안 회피 + 구조 이해 + 다양한 도구의 유기적 사용이 필요했죠.

결국 저는 PyAutoGUI까지 내려가서 “사람처럼 하는 자동화”를 구현했지만,
이건 어디까지나 임시방편이었습니다.

✅ 진짜 해결책은?

 

이번 글에서는 제가 겪은 네이버 카페 이미지 자동 다운로드 여정을 공유해보려고 합니다.

처음에는 확장 프로그램과 GUI 툴로 날로 먹으려고 했지만,

언제나 인생은 계획대로 되지 않음을 다시 한번 느끼게 되었네요.

 


1. 확장 프로그램으로 이미지 다운로드는 완벽했다

처음엔 그냥 게시판 글 하나 하나 들어가면서 우측 버튼 누르고 "다른 이름으로 저장" 누르고

그렇게 묵묵히 하고 있었습니다. 그러면서 남은 방대한 페이지들을 보면서 "아. 이게 뭐하는 짓이지?" 하며 현타가 왔지요. 좋은 것이 없나 하고 보니 크롬 이미지 다운로드 확장 프로그램을 써봤습니다.

  • 이미지 추출은 정말 빠르고 더 이상 이미지 찾아가면서 저장 안해도 될 만큼 정말 빨랐어요

“이야~ 이거면 게임 끝이다!”

 

라고 생각했죠.


2. 사람이 앉게 되면 눕고 싶다.

문제는 그 다음이었습니다.
다운로드는 쉬운데, 들어가야 할 게시글이 50개 넘는 상황.

  • 하나하나 클릭하고
  • 탭 열고
  • 확장프로그램 눌러서 이미지 받고
  • 다시 닫고…

이걸 반복하다가 정신이 멍해졌습니다.

“이걸 진짜 다 수작업으로 하라고?”

그 순간, 자동화를 고민하게 됐습니다. 음 자동화 툴이 뭐가 있어더라.. 코딩을 해야하나 어디 스크래핑 서비스를 써야 하나.


3. 그래서 선택한 Octoparse! 

**예전에는 스크래핑도 기술이라 몇천 만원짜리 솔루션 구매해서 실무에서 사용한 경험이 있었는데요. 이제는 접근성도 좋아지고 코딩을 몰라도 배우기도 쉬워 스크래핑을 정말 빠르게 작업이 가능했어요.**

  • GUI 방식으로 클릭만 하면 플로우가 만들어지고
  • 게시글 순회 → 이미지 추출 → 저장까지
  • 거의 1시간 안에 로그인부터 실행까지 완성!

그 당시엔 정말 만족했죠. 아 날먹~ 호호호호~ 개~~꿀~~!


4. 그런데 3개월 뒤… 다시 돌려보니 엥 로그인 실패?

시간이 지나고,
다시 같은 작업을 하려고 Octoparse 프로젝트를 실행해봤습니다.
그런데 로그인은 성공했는데, 이후 페이지에서 로그인이 유지되지 않더라고요.

  • 게시글에 접근하려 하면 다시 로그인 페이지로 리다이렉트
  • 세션 쿠키를 수동으로 유지시켜도 동일
  • 로그인 플로우를 새로 짜봐도 마찬가지

“어라? 이거 예전엔 잘 됐는데?”

분명 예전에 만들 플로어 돌렸는데 왜 지금은 안돌지 뭘 잘 못 건들였나?

하고 한시간만 돌려서 결과막 쏙 뽑아 먹을려고 했더만.

네이버 로그인 방법이 문제인지** Octoparse 설정을 잘 못건들였는지 무용지물이 되어버렸습니다.**


5. 그래서 결국… “내가 직접 만들자”

시간은 시간대로 쓰고 결과는 결과대로 못 얻얻게 되자

“이럴 바에야 차라리 내가 직접 만들자.”

 

개발자인 내가 이렇게 단순한 크롤링도 못 만드는 건가 싶어서 자존심이 살짝 상했죠. 지금 생각해 보면 객기 아닌 객기를 부렸....

 

정말 프로세스는 정말 단순하다.

**1.로그인 → 2.세션 유지 → 3.까페 접속 → 4.게시물 검색 → 5.페이지에 나온 게시물 클릭 → 6. 모든 이미지 다운로드 **

 

직접 구현해보기로 했습니다.

엣헴!

 

🧩 마무리하며

크롤링이라는 건 생각보다 복잡하고, 사이트마다 구조도 다르고 보안도 점점 강해지고 있더군요.
법적으로도 저작권 문제나 서버 부하 문제로 금지되는 경우가 많기 때문에, 관련 법령이나 가이드라인(예: 정보통신망법 제48조, robots.txt 등)을 확인하는 것이 중요합니다.
제 경우에는 개인적인 학습 목적으로 접근했고, 창과 방패의 싸움처럼 느껴졌던 과정이 참 기억에 남네요.

 

 

이번 글에서는 MCP 툴 없이 직접 모델과 통신한 구현MCP 툴 기반 구조를 나란히 비교해보려 합니다.

두 방식 모두 질문 → SQL 생성 → 실행 → 요약이라는 흐름은 같지만, 구현 철학과 역할 분담 구조가 완전히 다릅니다.


1. 직접 모델 호출 방식 (비-MCP 방식)

먼저, 모델을 직접 호출해 SQL을 만들고, 그 결과를 요약하는 전통적인 접근입니다.

const result = await model.generateContent(prompt);
const text = (await result.response.text()).trim();

그리고 결과 rows를 직접 넣어서 요약 프롬프트를 또 호출하죠.

const prompt = `
  질문: ${question}
  결과: ${JSON.stringify(rows)}
  → 요약해줘
`;
const summary = (await model.generateContent(prompt)).response.text();

이 구조의 특징은:

  • 모델에게 질문과 함께 컨텍스트를 주입
  • 직접적인 요청 흐름
  • 개발자가 모든 흐름을 설계함

2. MCP 툴 기반 구조

이번엔 MCP 툴 기반 구조입니다. 모델에게 툴 목록을 미리 등록해두고, 질문이 오면 모델이 스스로 "어떤 툴을 어떤 순서로 호출할지" 결정하도록 합니다.

MCP 방식에서는 이런 3개의 툴이 등록되어 있습니다:

🔧 1) generate-sql

server.tool(
  'generate-sql',
  { properties: { question, tables } },
  async (args) => {
    const prompt = `질문: ${args.question}
테이블: ${args.tables.join(', ')}`;
    const result = await model.generateContent(prompt);
    return result.response.text();
  }
);

🔧 2) execute-sql

server.tool(
  'execute-sql',
  { properties: { sql: { type: 'string' } } },
  async (args) => {
    const result = await pool.query(args.sql);
    return JSON.stringify(result.rows);
  }
);

🔧 3) summarize-sql-result

server.tool(
  'summarize-sql-result',
  { properties: { sql, rows } },
  async (args) => {
    const prompt = `질문 결과 요약:
SQL: ${args.sql}
데이터: ${JSON.stringify(args.rows)}`;
    const result = await model.generateContent(prompt);
    return result.response.text();
  }
);

각 툴은 독립적으로 실행되며, 모델이 "연결 순서"를 판단하여 generate → execute → summarize 흐름을 조립합니다.


💡 동일한 기능을 직접 호출 방식으로 구현하면?

MCP 없이 구현한 코드에서는 아래처럼 3개의 함수를 직접 호출합니다:

📦 1) SQL 생성

async function generateSQL(question, tables) {
  const prompt = `질문: ${question}
테이블: ${tables.join(', ')}`;
  const result = await model.generateContent(prompt);
  return result.response.text();
}

📦 2) SQL 실행

const rows = await pool.query(sql);

📦 3) 결과 요약

async function generateSummary(question, rows) {
  const prompt = `질문: ${question}
데이터: ${JSON.stringify(rows)}`;
  const result = await model.generateContent(prompt);
  return result.response.text();
}

결론적으로 MCP 방식은 동일한 함수 단위를 툴로 감싸서 등록해두고, 모델이 필요한 순서로 호출하게 만든 구조입니다.

예: 등록된 MCP 툴 목록

  • generate-sql: 자연어 → SQL
  • execute-sql: SQL 실행
  • summarize-sql-result: 결과 요약
const parsedSteps = JSON.parse(await model.generateContent(prompt));
for (const step of parsedSteps) {
  const toolResult = await server.callTool(step.tool, step.args);
}

 

이 구조는:

  • 툴을 기반으로 설계됨
  • 모델이 실행 순서를 조립함
  • 서버는 단순히 툴 실행자 역할

구조적 비교

항목 직접 모델 호출 방식 MCP 툴 기반 구조
흐름 설계 개발자가 모두 설계 모델이 조립
확장성 프롬프트마다 추가 구현 필요 툴만 추가하면 됨
복잡도 처음은 단순하나 점점 커짐 초반 설계가 필요하지만 구조는 깔끔
모델 의존도 단발성 호출 중심 연속된 흐름도 모델이 주도

실제로 구현해보니

기능적인 결과는 거의 같습니다. 질문하면 SQL 생성되고, 실행되고, 요약도 됩니다. 사용자 입장에서는 두 방식 차이를 못 느낄 정도죠.

하지만 서버 입장에서 느껴지는 구조적 차이는 꽤 큽니다.

  • 비-MCP는 일회성 처리가 편하고 빠름
  • MCP는 구조를 한번 만들면 계속 재사용, 확장이 쉬움

또한 툴 단위로 분리해두면, 각 도구의 로깅, 테스트, 교체가 훨씬 유리해집니다.


마치며

두 가지 구조를 비교해보며 느낀 점은:

“작고 단순한 프로젝트는 직접 호출이 빠르지만, 복잡해질수록 MCP 기반이 구조적으로 더 낫다.”

 

 

긴 글 읽어 주셔서 감사합니다.

 

그리고 지금까지 소스 공개합니다.


REST API 기준

https://github.com/pm2makeq/chatbot_restapi

 

GitHub - pm2makeq/chatbot_restapi: restapi chatbot

restapi chatbot. Contribute to pm2makeq/chatbot_restapi development by creating an account on GitHub.

github.com

 

MCP 기준

https://github.com/pm2makeq/mcp-project

 

GitHub - pm2makeq/mcp-project

Contribute to pm2makeq/mcp-project development by creating an account on GitHub.

github.com

 

전체글

 

지난 글에서는 MCP 기반 챗봇의 클라이언트 구현을 중심으로 다뤘습니다. 이번에는 진짜 핵심인 서버 편입니다. 챗봇이 작동하려면, 결국 서버가 질문을 받아서 SQL을 생성하고 실행하고 요약까지 해줘야 하니까요.

이번 글에서는 동일한 기능을 REST API 방식과 MCP 방식 두 가지로 각각 구현해본 뒤, 어떤 차이가 있었는지 정리해보았습니다.


🧩 REST API 방식부터 살펴봅니다

app.post("/ask", async (req, res) => {
  const { question, tables } = req.body;
  const updatedTables = [...(tables || []), "students"];

  const sql = await generateSQL(question, updatedTables);
  const result = await client.query(sql);
  const summary = await generateSummary(question, result.rows);

  res.json({ sql, rows: result.rows, summary });
});

REST 방식은 익숙합니다. 구조도 간단하죠:

  1. 질문 받기
  2. SQL 생성
  3. SQL 실행
  4. 결과 요약

이걸 하나의 API에서 처리하면 됩니다. 명확하고 직선적인 구조, 딱 필요한 것만 구현하면 됩니다.


🔁 MCP 방식은 이렇게 다릅니다

이번엔 같은 기능을 MCP 방식으로 구현한 서버입니다. 구조는 좀 더 복잡해 보일 수 있지만, 핵심은 모델이 도구들을 직접 호출하게 하는 것입니다.

const prompt = `
  - 질문을 받으면 generate-sql → execute-sql → summarize-sql-result 순으로
  - 각 도구에 필요한 args를 구성해서 JSON 배열로 응답
`;

const result = await model.generateContent(prompt);
const parsedSteps = JSON.parse(result);

for (const step of parsedSteps) {
  const toolResult = await server.callTool(step.tool, step.args);
  // 결과 누적 및 가공
}

MCP 방식의 차이점은 다음과 같습니다:

  • 질문을 받고 → 어떤 도구를 쓸지 모델이 판단함
  • 각 도구에 적절한 인자를 주입함
  • 실행 결과를 다시 다음 도구로 넘김

즉, 흐름을 사람이 짜는 게 아니라 모델이 조립합니다. 이것이 MCP의 핵심이죠.


⚙️ 구조 비교 정리

항목 REST 방식 MCP 방식
흐름 제어 서버 코드에 명시적으로 작성 모델이 도구 흐름을 결정
유연성 새로운 기능 추가 시 코드 수정 필요 도구만 추가하면 됨
모델 참여도 모델은 단순 질문 응답 모델이 도구 조합까지 주도

🧪 돌려보고 느낀 점

둘 다 돌아가는 기능은 거의 똑같습니다. 결과도 유사하고, 처리 시간도 큰 차이는 없었습니다.

특히 클라이언트 입장에서는 둘이 어떻게 구현되었는지 알 수 없을 정도로 결과만 놓고 보면 똑같습니다.

하지만 차이점은 "설계자 관점"에서 확연했습니다:

  • REST는 내가 다 설계해야 함
  • MCP는 모델이 설계의 일부를 맡음

따라서 도구가 많아지고 복잡해질수록 MCP가 더 깔끔하게 느껴졌습니다.


✍️ 마치며

서버 편을 정리하며 느낀 건 하나입니다:

“처음엔 REST가 편하지만, 확장성과 유지보수를 생각하면 MCP 구조가 훨씬 유연하다.”


 

다음 편에서는 이 MCP 서버와 API 서버를 연결하는 구조를 정리해보겠습니다.

 

다음 글에서 뵐게요 👋

 

다음글

 

학생 관리 RAG 챗봇, MCP 툴로 붙여보니 이런 느낌이었습니다

 

저는 요즘 챗봇을 만들면서 개발자로서의 사고방식이 꽤 많이 바뀌고 있습니다. 특히 MCP를 도입하고 나서는, 단순히 API를 설계하던 습관에서 벗어나 모델이 중심이 되는 구조를 점점 익혀가고 있는데요.

이번에는 실제로 학생 정보를 관리하는 간단한 챗봇을 MCP 기반으로 구현해본 경험을 이야기해보려 합니다.

개발은 결국 시행착오죠. 잘된 점도 있었고, "어 이건 아닌데..." 싶은 부분도 있었거든요.

 

 

개발은 결국 시행착오죠. 엣헴!

 

🧩 목표는 단순했습니다: "질문하면 요약해줘"

기획 자체는 별거 없었습니다. 그냥 이거였어요:

"학생 테이블을 대상으로, 사용자가 자연어로 질문하면 → SQL을 생성하고 → 실행한 뒤 → 결과를 요약해 보여주는 챗봇을 만들자"

기존이라면 이걸 어떻게 구현했을까요? 아마도:

  • 질문을 백엔드에서 해석하고
  • SQL을 직접 작성해서 날리고
  • 결과를 프론트에 출력하는 방식이었겠죠.

이번에는 다릅니다. MCP 툴을 이용해 이 흐름을 모델이 직접 주도하도록 구성했습니다.

  • /proxy/8080/ask 라우터 하나에 모든 로직을 몰아넣고
  • 질문만 던지면 → SQL 생성 → 실행 → 요약까지 한번에
  • 결과는 sql, rows, summary 3개 필드로 정리

💻 핵심 함수 하나로 설명됩니다

async function callAsk(question, tables = []) {
  tables.push("students");
  const resp = await fetch('/proxy/8080/ask', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question, tables }),
  });

  let data;
  try {
    data = await resp.json();
  } catch (e) {
    return { summary: '서버 응답을 해석할 수 없습니다.' };
  }

  return {
    sql: data.sql ?? 'SQL 없음',
    rows: data.rows ?? '데이터 없음',
    summary: data.summary ?? '요약 없음'
  };
}

딱 하나의 질문으로 3가지 정보를 받아옵니다. 이게 가능했던 건, MCP 툴 내부에 질문 → SQL 생성 → 실행 → 요약까지 연결되어 있었기 때문이죠.

🎨 UI는 이렇게 그렸습니다

  • 🤖/👤 아이콘으로 역할 구분
  • 🕑 처리 중… 문구로 피드백 주고
  • 응답이 오면 SQL, 데이터, 요약 순서로 정리해서 보여줍니다

기능적으로는 단순하지만, 사용성은 굉장히 직관적입니다.

 


🧪 돌려보고 느낀 점

사실, REST API 방식으로 구현했을 때와 이번 MCP 방식은 기능적으로 큰 차이는 없었습니다. 질문을 받고 → SQL을 만들고 → 실행하고 → 요약을 보여준다는 흐름은 거의 같았고요.

특히 클라이언트 측에서는 거의 차이를 느끼기 어려웠습니다. fetch로 요청을 보내고, 응답을 받아 출력하는 구조는 동일했기 때문입니다. 즉, 사용자 인터페이스나 처리 흐름 자체는 바뀌지 않았습니다.

그렇다면 굳이 MCP를 왜 썼냐고요? 핵심은 "모델 중심의 설계 경험"이었습니다.


🚀 다음은 API 서버 연동입니다

  • MCP 툴 연동
  • 자연어 질문을 도구로 연결해 답변 생성하기
  • 예외처리

다음에 또 뵐게요! 👋

 

다음글

+ Recent posts