<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>System Consultant Group(SCG) 개발 블로그</title>
    <link>https://scg-skku.tistory.com/</link>
    <description>성균관대학교 최고의 소프트웨어 개발 단체</description>
    <language>ko</language>
    <pubDate>Tue, 7 Apr 2026 10:34:05 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>scg-skku</managingEditor>
    <image>
      <title>System Consultant Group(SCG) 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8561459/attach/a8fc524c8be54b7baf96228269507788</url>
      <link>https://scg-skku.tistory.com</link>
    </image>
    <item>
      <title>웍실 네트워크 개편</title>
      <link>https://scg-skku.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 시스템컨설턴트그룹 30기 인프라 담당 신동현입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마 전 양현준님과 함께 미루고 미뤄왔던 웍실 네트워크 개편을 드디어 진행했습니다. 기존 웍실 네트워크는 사실 언제 터질지 모르는 시한폭탄이었습니다. 가정용 공유기나 스위치들이 직렬로 주렁주렁 물려있어서, 중간에 하나만 죽어도 하위 네트워크 전체가 끊기는 구조였거든요. 게다가 웍실의 업스트림 포트는 두 개나 되는데, 정작 시스코 스위치에는 하나만 연결해두고 아까운 자원을 낭비하고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없음(2).png&quot; data-origin-width=&quot;4104&quot; data-origin-height=&quot;3080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FKUZM/dJMcaaSmAvJ/KEzC2Jw6nXE3HQJRkkAzeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FKUZM/dJMcaaSmAvJ/KEzC2Jw6nXE3HQJRkkAzeK/img.png&quot; data-alt=&quot;네트워크 구조도 (초록색: 추가, 빨간색: 제거)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FKUZM/dJMcaaSmAvJ/KEzC2Jw6nXE3HQJRkkAzeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFKUZM%2FdJMcaaSmAvJ%2FKEzC2Jw6nXE3HQJRkkAzeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4104&quot; height=&quot;3080&quot; data-filename=&quot;제목 없음(2).png&quot; data-origin-width=&quot;4104&quot; data-origin-height=&quot;3080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;네트워크 구조도 (초록색: 추가, 빨간색: 제거)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size23&quot;&gt;'고성능'&amp;nbsp; 라우터 투입&lt;/h3&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;이번 개편의 핵심 중 하나는 라우터 분리였습니다. 기존 무선 AP가 기기 50여 대를 감당하다 보니 자꾸 뻗어버리더라고요. 그래서 웍실 구석에 굴러다니던 데스크톱을 라우터로 개조해 투입했습니다. 무려 '브로드웰' 기반의 PC인데, 지금이 2026년이니 무려 10년도 더 된 할아버지급 CPU죠. 그래도 단순 가정용 AP 보다는 훨씬 듬직하게 트래픽을 처리해 줍니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&quot;랜선에 불은 들어오는데 왜 안 되지?&quot;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;구조를 바꾸면서 골칫덩어리였던 빨간색 공유기(의문의 스위치)를 떼어내고, 책상 밑 라인들을 정보검색실 스위치로 직결하는 작업을 했습니다. 실제 서버를 건드리기 전에 혹시 몰라서 남는 PC에 새 케이블을 꽂고 테스트를 해봤는데... 분명 랜 포트에 불은 잘 들어오는데 서버와 통신이 아예 안 되는 겁니다. 처음엔 케이블 단선을 의심했는데, 가만히 생각해보니 작년 '네트워크시스템개론' 수업에서 들었던 내용이 머리를 스쳐지나갔습니다. 결론부터 말하자면 이건 &lt;b data-index-in-node=&quot;119&quot; data-path-to-node=&quot;7&quot;&gt;MAC 주소 테이블과 ARP 캐시&lt;/b&gt;가 만들어낸 환장의 콜라보였습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;조금 깊게 파보는 MAC과 ARP의 세계&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 장비들은 멍청하게 모든 데이터를 사방으로 뿌리지 않습니다. 나름의 '기억력'을 가지고 있죠.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;스위치의 MAC 테이블:&lt;/b&gt; 스위치는 자신의 몇 번 포트에 어떤 기기(MAC 주소)가 연결되어 있는지 기억해 둡니다. 패킷이 들어오면 이 테이블을 보고 목적지가 있는 포트로만 쏙 보내줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;라우터의 ARP 캐시:&lt;/b&gt; 라우터는 특정 IP 주소가 어떤 MAC 주소를 가지고 있는지 매칭해 둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;자, 여기서 서버에 꽂혀있던 랜선을 뽑아서 다른 경로(다른 스위치나 포트)로 옮겨 꽂으면 어떻게 될까요? 물리적인 선은 옮겨졌지만, 상위 스위치와 라우터의 '기억'은 아직 과거에 머물러 있습니다. 상위 장비들은 여전히 &lt;i data-index-in-node=&quot;122&quot; data-path-to-node=&quot;11&quot;&gt;&quot;아, 그 서버는 예전 길로 가면 있지?&quot;&lt;/i&gt; 하고 엉뚱한 옛날 포트로 데이터를 던져버립니다. 이 상황에서 서버가 가만히 숨만 쉬고 있으면(외부로 데이터를 먼저 보내지 않으면), 스위치 입장에서는 이 녀석이 다른 포트로 이사 갔다는 사실을 알아챌 방법이 없습니다. 기존 장비들의 캐시가 만료될 때까지 통신이 완전히 먹통이 되는 겁니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;Ping 한 줄로 해결한 무중단 핫스왑&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;이 가설이 맞는지 확인하려고 먹통이 된 서버에 키보드랑 모니터를 직접 연결해서 터미널을 열었습니다. 그리고 딱 한 줄을 쳤죠. &quot;ping 1.1.1.1&quot;. 엔터를 치는 순간 핑이 나가면서 즉시 통신이 복구됐습니다! 서버가 외부로 핑을 쏘면서 자신의 MAC 주소를 담은 패킷을 '새로운 경로'로 내보냈고, 그 패킷을 받은 스위치들이 &lt;i data-index-in-node=&quot;98&quot; data-path-to-node=&quot;16&quot;&gt;&quot;어? 이 MAC 주소가 이제 이쪽 포트에서 들어오네?&quot;&lt;/i&gt; 하고 즉각 자신의 테이블을 갱신해 버린 겁니다. 이 원리를 바탕으로, 실제 서비스 중인 서버들의 랜선을 끊김 없이 교체할 꼼수가 생겼습니다. 서버들의 케이블을 뽑기 전에, 미리 각 서버에 SSH로 접속해서 &quot;ping 1.1.1.1 &amp;amp; disown&quot;을 실행했습니다. 서버가 쉴 새 없이 자기 위치를 동네방네 떠들게 만든 상태에서 랜선을 새 경로로 확 바꿔치기했죠. 선이 연결되는 그 0.1초의 순간에 핑 패킷이 새 경로를 타고 올라가서 스위치들의 경로를 강제로 업데이트시켜 버렸습니다. 결과적으로 단 몇 초의 끊김도 없는 완벽한 핫스왑에 성공했습니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size23&quot;&gt;앞으로의 계획&lt;/h3&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;이제 당면한 급한 불은 껐고, 다음 목표는 라우터를 중앙 시스코 스위치와 메인 업스트림 포트 사이의 최상단으로 옮기는 겁니다. 현재 웍실은 수동으로 적어둔 IP 관리 대장이 업데이트가 안 돼서, 무슨 IP가 빈자리인지도 모르는 막장 상태거든요. 라우터를 최상단으로 올리면 전체 IP 사용 현황을 한눈에 모니터링할 수 있습니다. 겸사겸사 좌/우측 PC들도 전부 DHCP와 NAT 기반으로 돌릴 예정입니다. 예전엔 학교에 포트 개방을 요청할 수 있어서 PC마다 공인 IP를 직접 먹여놨었는데, 이제 정책상 더 이상 불가능해졌습니다. PC 초기화할 때마다 IP, 서브넷 마스크, 게이트웨이, DNS를 수동으로 잡아주는 노가다도 이젠 끝내보려고 합니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>ARP</category>
      <category>MAC</category>
      <category>ping</category>
      <category>SCG</category>
      <category>시스템컨설턴트그룹</category>
      <author>aperso</author>
      <guid isPermaLink="true">https://scg-skku.tistory.com/5</guid>
      <comments>https://scg-skku.tistory.com/5#entry5comment</comments>
      <pubDate>Fri, 3 Apr 2026 15:16:48 +0900</pubDate>
    </item>
    <item>
      <title>8년 된 레거시 시스템의 에러를 추적한 과정 - 로깅 한 줄의 힘</title>
      <link>https://scg-skku.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 :) 시스템컨설턴트그룹 27기 백엔드 개발을 하고 있는 노주희입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중인 서비스에서 에러가 발생했다는 연락을 받았을 때, 가장 먼저 확인하는 것은 로그입니다. 그런데 로그에 아무런 단서가 없다면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 성균관대학교 &lt;a href=&quot;https://cssys.cs.skku.ac.kr/cssys/login&quot;&gt;소프트웨어융합대학의 졸업평가 시스템&lt;/a&gt;(연구논문작품시스템, 이하 cssys)에서 발생한 파일 업로드 500 에러를 추적하고 해결한 과정을 정리한 것입니다. 로깅이 부재한 레거시 시스템에서 어떻게 원인을 좁혀나갔는지, 그리고 그 과정에서 느낀 점을 공유합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배경&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cssys는 소프트웨어학과 졸업평가 연구논문작품의 서류 제출 및 심사 과정이 이루어지는 시스템입니다. 학생들은 서약서, 제안서, 중간보고서, 최종보고서 등을 이 시스템을 통해 제출하고, 행정실에서는 관리자 페이지를 통해 서류를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스는 SCG에서 운영하고 있으며, 첫 커밋이 2018년 1월로 기록되어 있는, 8년 이상의 역사를 가진 레거시 시스템입니다. 정보통신대학의 졸업평가 시스템(iccsys)이 2016년에 만들어졌고 해당 프로젝트를 fork 해서 만든 서비스로 파악됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RCQt9/dJMcahcGIoh/xWs9QC803wdusHqrdJkRaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RCQt9/dJMcahcGIoh/xWs9QC803wdusHqrdJkRaK/img.png&quot; data-alt=&quot;GitLab에 보관된 원본 소스 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RCQt9/dJMcahcGIoh/xWs9QC803wdusHqrdJkRaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRCQt9%2FdJMcahcGIoh%2FxWs9QC803wdusHqrdJkRaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;645&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;807&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GitLab에 보관된 원본 소스 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 기술 스택은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;런타임/프레임워크&lt;/b&gt;: Node.js + Express 4&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ORM&lt;/b&gt;: Sequelize&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB&lt;/b&gt;: MySQL&lt;/li&gt;
&lt;li&gt;&lt;b&gt;템플릿 엔진&lt;/b&gt;: Swig&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포&lt;/b&gt;: 물리 서버의 VM 위에서 pm2로 수동 배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 저장&lt;/b&gt;: 서버 로컬 파일시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마이그레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 물리 서버의 디스크 용량 부족 문제로 인해, 2026년 3월 초에 28기 양현준 님과 27기 송영욱 님이 함께 다음과 같은 마이그레이션을 진행했습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 83.9535%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;width: 27.3256%;&quot;&gt;Before&lt;/td&gt;
&lt;td style=&quot;width: 41.6279%;&quot;&gt;After&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;배포 환경&lt;/td&gt;
&lt;td style=&quot;width: 27.3256%;&quot;&gt;VM 위 수동 실행&lt;/td&gt;
&lt;td style=&quot;width: 41.6279%;&quot;&gt;Kubernetes + ArgoCD + GitHub Actions CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;파일 저장&lt;/td&gt;
&lt;td style=&quot;width: 27.3256%;&quot;&gt;서버 로컬 폴더&lt;/td&gt;
&lt;td style=&quot;width: 41.6279%;&quot;&gt;MinIO Object Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;라이브러리&lt;/td&gt;
&lt;td style=&quot;width: 27.3256%;&quot;&gt;mysql / Sequelize v2&lt;/td&gt;
&lt;td style=&quot;width: 41.6279%;&quot;&gt;mysql2 / Sequelize v6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;템플릿 엔진&lt;/td&gt;
&lt;td style=&quot;width: 27.3256%;&quot;&gt;Swig&lt;/td&gt;
&lt;td style=&quot;width: 41.6279%;&quot;&gt;EJS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 후 행정실에 테스트를 요청했고, 특별한 이상이 없다는 회신을 받았습니다. 3월 6일~7일에 서버 이전 작업이 완료되었고, 서비스가 정상적으로 운영되는 것으로 판단했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 발생&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 보고 (3월 11일)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 이전이 완료된 지 며칠 후, 행정실로부터 메일이 도착했습니다. 관리자 계정으로 학생의 신청서를 업로드하려 했으나, &quot;에러가 발생하였습니다. 시스템 관리자에게 문의해주세요.&quot;라는 팝업이 표시된다는 내용이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdfmYM/dJMcagkxt0M/VevNvBguKi46AikDHqXyT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdfmYM/dJMcagkxt0M/VevNvBguKi46AikDHqXyT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdfmYM/dJMcagkxt0M/VevNvBguKi46AikDHqXyT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdfmYM%2FdJMcagkxt0M%2FVevNvBguKi46AikDHqXyT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;455&quot; height=&quot;154&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기 상황 파악의 어려움&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 에러를 받았을 때, 몇 가지 난관이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;의미 있는 로그가 없었습니다.&lt;/b&gt; ArgoCD를 통해 로그를 확인할 수는 있었지만, 에러 핸들러가 URL만 기록하고 에러 내용 자체는 출력하지 않는 구조였습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 기존 에러 핸들러 (app.js)
console.error(`[${now}] [ERROR] ${req.method} ${req.originalUrl}`);
// err 객체는 출력하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;관리자 페이지에 직접 접근할 수 없었습니다.&lt;/b&gt; 관리자 계정으로 접속하면 IP가 로그에 남기 때문에, 보안상의 이유로 개발자의 직접 접근을 지양하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째, &lt;b&gt;개발 서버가 존재하지 않았습니다.&lt;/b&gt; 운영 환경만 존재하는 상황에서, 변경사항을 테스트하려면 매번 행정실에 재시도를 요청해야 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1: 초기 가설 수립&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에서 확인할 수 있었던 것은 다음 정보뿐이었습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;발생 시간: 2026-03-11 13:06~13:09
클라이언트 IP: 115.145.180.***
HTTP 메서드: POST
대상 엔드포인트: /cssys/work/admin/student/8805
상태 코드: 500
에러 메시지: Response timeout&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST 요청에서 500 에러가 발생했고, 행정실에서 전달받은 파일은 139kB에 불과했으므로 파일 크기 문제는 아니었습니다. 마이그레이션 과정에서 MinIO 연결 설정에 문제가 있을 수 있다는 가설을 세웠지만, 로그 정보만으로는 원인을 특정할 수 없었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 2: 로깅 추가 및 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인 추적을 위해 에러 핸들러에 스택 트레이스 출력을 추가하는 것을 최우선 조치로 결정했습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 변경 후
console.error(`[${now}] [ERROR] ${req.method} ${req.originalUrl}\\n${err.stack}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단 한 줄의 수정&lt;/b&gt;이었지만, 이것이 이후 문제 해결의 결정적인 전환점이 되었습니다. &lt;a href=&quot;https://github.com/SystemConsultantGroup/CSE-undergrad-gp-manage-v2/pull/3&quot;&gt;PR&lt;/a&gt;을 생성하고 머지한 뒤, CI/CD 파이프라인을 통해 자동 배포되었습니다. 이후 행정실에 동일한 동작의 재시도를 요청하는 메일을 발송했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 3: 1차 원인 특정 &amp;mdash; multer 필드명 불일치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행정실에서 재시도한 후 로그를 확인하니, 드디어 스택 트레이스가 남아 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;MulterError: Unexpected field
    at wrappedFileFilter (multer/index.js:40:19)
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MulterError: Unexpected field. multer가 예상하지 못한 필드명으로 파일이 전송되었다는 의미입니다. 코드를 확인한 결과, 원인은 &lt;b&gt;어드민 뷰의 폼 필드명과 multer 설정의 불일치&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 라우터 설정 (admin.js)
router.post('/student/:id', upload.single('upload'), ...)
// multer는 'upload'라는 이름의 파일 필드만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;!-- 어드민 뷰 (student_view.ejs) --&amp;gt;
&amp;lt;input type=&quot;file&quot; name=&quot;oath&quot;&amp;gt;           &amp;lt;!-- 실제 파일 필드: 'oath' --&amp;gt;
&amp;lt;input type=&quot;hidden&quot; name=&quot;upload&quot; value=&quot;oath&quot;&amp;gt;  &amp;lt;!-- 파일 타입 정보: 텍스트 --&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multer는 upload라는 이름의 파일 필드를 기다리고 있었지만, 실제 폼에서는 oath라는 이름으로 파일이 전송되고 있었습니다. name=&quot;upload&quot;인 필드는 파일이 아닌 hidden 텍스트 필드로, 어떤 종류의 파일인지를 서버에 알려주는 역할이었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 4: 수정 및 2차 에러 발견&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 방향은 어드민 뷰의 7개 파일 input 필드명을 upload으로 통일하는 것으로 결정했습니다. &lt;a href=&quot;https://github.com/SystemConsultantGroup/CSE-undergrad-gp-manage-v2/pull/4&quot;&gt;(수정 PR)&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 수정 전 --&amp;gt;
&amp;lt;input type=&quot;file&quot; name=&quot;oath&quot;&amp;gt;

&amp;lt;!-- 수정 후 --&amp;gt;
&amp;lt;input type=&quot;file&quot; name=&quot;upload&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 배포하고 행정실에 다시 테스트를 요청했지만, 여전히 에러가 발생한다는 회신이 돌아왔습니다. 로그를 확인하니 이번에는 다른 에러가 기록되어 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;SequelizeValidationError: notNull Violation: StudentFile.time cannot be null,
notNull Violation: StudentFile.ip cannot be null
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 업로드 기록을 DB에 저장할 때 time과 ip 값이 null로 들어가 유효성 검사에 실패한 것이었습니다. 마이그레이션 과정에서 기존 time/ip 파서의 호환성이 깨진 문제로, 마이그레이션 담당자인 현준 님이 이미 인지하고 있던 이슈였기에 곧바로 수정해 주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스택 트레이스 로깅이 남아 있었기 때문에, 2차 에러의 원인 파악에는 1분도 걸리지 않았습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 5: 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 에러까지 수정하고 배포를 완료했습니다. 행정실 담당자분의 퇴근 시간 이후였기 때문에 다음 날 최종 확인을 받을 수 있었습니다. 에러 보고를 받은 오후 1시부터 수정 배포 완료까지 약 5시간이 소요되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Claude Code 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 트러블슈팅 과정에서 Claude Code(Anthropic의 CLI 기반 AI 코딩 도구)를 적극적으로 활용했습니다. 다만, AI에 완전히 의존하지 않고 &lt;b&gt;판단의 주체는 사람이 가져가는 방식&lt;/b&gt;으로 사용했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;가설 수립 단계&lt;/b&gt;: 에러 현상, 환경 변경사항, 제약조건 등의 컨텍스트를 제공하고 원인 가설을 요청했습니다. AI는 MinIO SDK의 HTTPS 설정 불일치, K8s 네트워크 문제, 에러 핸들링 누락 등 여러 가설을 제시했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가설 검증 단계&lt;/b&gt;: AI의 가설에 대해 &quot;다른 서비스에서는 MinIO 연결에 문제가 없다&quot;는 사실을 제공하여 반박하고, AI가 재분석하도록 유도했습니다. AI는 응답 시간 4.514ms라는 단서에 주목하여 MinIO hang이 아닌 즉시 실패라는 결론을 도출했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정 방향 결정 단계&lt;/b&gt;: AI는 upload.any()로 서버를 변경하는 방안을 제시했지만, 폼 필드명을 일치시키는 방향이 더 적절하다고 판단하여 직접 방향을 수정했습니다. 학생/어드민 구조 차이를 AI에게 분석시킨 뒤, 최종 수정 범위를 확정했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 단계&lt;/b&gt;: PR 생성, 커밋 메시지 작성, 코드 수정 등의 실행은 AI에 위임했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;패턴 정리&lt;/h4&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;AI 가설 제시 &amp;rarr; 사람이 검증/반박 &amp;rarr; AI 재분석 &amp;rarr; 사람이 방향 결정 &amp;rarr; AI 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 &quot;만능 해결사&quot;가 아닌 &quot;빠른 분석 도구&quot;로 활용한 것이 효과적이었습니다. 특히 익숙하지 않은 코드베이스(Node.js/Express)를 빠르게 파악하는 데 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G8xc0/dJMcacWJ1BW/IerSNhEfXKE8KfGDLj8yDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G8xc0/dJMcacWJ1BW/IerSNhEfXKE8KfGDLj8yDk/img.png&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;753&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.3123%; margin-right: 10px;&quot; data-widthpercent=&quot;47.87&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G8xc0/dJMcacWJ1BW/IerSNhEfXKE8KfGDLj8yDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG8xc0%2FdJMcacWJ1BW%2FIerSNhEfXKE8KfGDLj8yDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;753&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bax3P5/dJMcaiQacir/0LdWiBZTx9cEicYMoP6ka0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bax3P5/dJMcaiQacir/0LdWiBZTx9cEicYMoP6ka0/img.png&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;678&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;52.13&quot; style=&quot;width: 51.5249%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bax3P5/dJMcaiQacir/0LdWiBZTx9cEicYMoP6ka0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbax3P5%2FdJMcaiQacir%2F0LdWiBZTx9cEicYMoP6ka0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;656&quot; height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;원인 파악, 수정 방향 결정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 도구를 운영 서비스에 적용하면서 느낀 점은 개인 블로그의&amp;nbsp;&lt;a href=&quot;https://yesjuhee.tistory.com/44&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;별도 글&lt;/a&gt;로 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 느낀 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로깅의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험에서 가장 크게 느낀 것은 로깅의 힘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 핸들러에 err.stack 한 줄을 추가한 것이 문제 해결의 전환점이었습니다. 실제 슬랙 타임스탬프를 기반으로 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;MTTR&lt;/b&gt;&lt;/span&gt;(Mean Time To Repair, 장애 복구 평균 시간)을 비교해 보면 그 차이가 극명합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1차 에러 (로깅 없는 상태) &amp;mdash; MTTR: 약 3시간 32분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 보고(13:11) &amp;rarr; 로깅 추가 배포(14:48) &amp;rarr; 행정실 재시도(15:54) &amp;rarr; 원인 특정(16:03).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에 URL만 남아 있었기 때문에 원인을 추정할 수 없었고, 로깅 코드를 추가하고 배포한 뒤 행정실에 재시도를 요청하는 과정을 거쳐야 했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2차 에러 (로깅 있는 상태) &amp;mdash; MTTR: 약 0분&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 로그 확인(17:22) &amp;rarr; 원인 특정(17:22).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 트레이스에 &lt;i&gt;&lt;b&gt;SequelizeValidationError: notNull Violation: StudentFile.time cannot be null&lt;/b&gt;&lt;/i&gt;이 그대로 찍혀 있었기 때문에, 로그를 본 즉시 원인이 파악되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnQEhg/dJMcadaeGBo/MH0vKClw2NLbGeVAp1NpX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnQEhg/dJMcadaeGBo/MH0vKClw2NLbGeVAp1NpX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnQEhg/dJMcadaeGBo/MH0vKClw2NLbGeVAp1NpX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnQEhg%2FdJMcadaeGBo%2FMH0vKClw2NLbGeVAp1NpX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;667&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 슬랙 대화를 보면 이 속도감이 잘 드러납니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;행정실&lt;/b&gt; [오후 5:21] 에러 메일 발신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노주희&lt;/b&gt; [오후 5:22] 에러 로그 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;양현준&lt;/b&gt; [오후 5:22] 원인 파악 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 시스템, 같은 팀, 같은 날에 발생한 에러인데, 로그 한 줄의 유무가 원인 파악 시간을 &lt;b&gt;3시간에서 0분으로&lt;/b&gt; 바꿔놓았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 로깅이 없었다면 어떻게 했을까요? 로컬 환경을 구성해서 디버거를 돌리거나, 추정되는 지점마다 &lt;b&gt;console.log&lt;/b&gt;를 찍어가며 하나씩 배포했어야 했을 것입니다. 로그가 얼마나 많은 시간과 노력을 절약해 주는지 체감한 경험이었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 환경의 부재가 만드는 비효율&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cssys는 운영 환경만 존재합니다. 개발 서버가 따로 없고, 어드민 페이지는 보안상 개발자가 직접 접근하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 변경사항을 테스트하려면 매번 행정실 담당자에게 메일로 재시도를 요청해야 했습니다. 에러 보고부터 해결까지 주고받은 메일이 상당했는데, 개발 서버가 있었다면 그중 대부분은 필요하지 않았을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 서버가 있으면 단순히 테스트뿐 아니라, 대량 데이터를 넣고 부하 테스트를 수행하는 등 학습 목적으로도 활용할 수 있습니다. 장기적으로 보면 개발 서버 구축은 조직 전체의 효율에 기여하는 투자입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SCG가 나아갔으면 하는 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 에러 대응을 거치면서, 올해 맡게 된 31기 신입 백엔드 스터디의 방향에 대한 확신이 생겼습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCG는 기술적으로 계속 성장하고 있고, 훌륭한 회원들이 많은 단체입니다. 다만 학생 단체의 특성상, 개발 당시에는 기능을 빠르게 완성하는 데 집중하게 되고, 그것을 만든 사람들은 얼마 지나지 않아 졸업합니다. 과거의 코드를 보면, 미래에 유지보수를 담당할 사람에 대한 고려가 부족한 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말을 하고 있는 저 역시 다르지 않았습니다. 아는 것이 없었으니 아는 만큼 구현하고 끝냈습니다. 그런데 지난 1년간 우아한테크코스를 거치면서 좋은 코드에 대한 이야기를 반복적으로 나누었고, 그 경험이 시야를 넓혀주었습니다. 이렇게 얻은 것을 다시 SCG에 전파하고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 모든 것을 바꾸기는 어렵습니다. 그래서 31기 신입 스터디부터 시작해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 거치고 나서, 다음과 같은 문화가 SCG에 정착되었으면 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드를 짜는 것을 당연하게 생각한다.&lt;/li&gt;
&lt;li&gt;로깅을 어떻게 남기는 것이 좋을지 고민한다.&lt;/li&gt;
&lt;li&gt;유지보수하기 좋은 코드에 대해 고민한다.&lt;/li&gt;
&lt;li&gt;개발 컨벤션에 대한 논의를 거쳐 팀 전체가 같은 컨벤션을 지킨다.&lt;/li&gt;
&lt;li&gt;내일의 나와 미래의 후배를 위해 문서로 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8년 된 코드 위에 새로운 인프라를 올리고, 로그 한 줄 없는 상태에서 에러를 추적하고, 행정실과 메일을 주고받으며 문제를 좁혀나간 하루였습니다. 돌이켜보면 기술적으로 어려운 문제는 아니었습니다. multer 필드명 불일치라는, 코드를 보면 바로 알 수 있는 버그였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &quot;코드를 볼 수 있는 상태&quot;에 도달하기까지의 과정이었습니다. 로그가 없었고, 테스트 환경이 없었고, 관리자 페이지에 접근할 수 없었습니다. 결국 err.stack 한 줄을 추가하는 것이 이 모든 장벽을 뚫는 열쇠가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 작성하는 시간보다 유지보수하는 시간이 훨씬 깁니다. 오늘 한 줄의 로그가, 내일 누군가의 5시간을 30분으로 줄여줄 수 있습니다.&lt;/p&gt;</description>
      <category>Troubleshooting</category>
      <category>logging</category>
      <category>MTTR</category>
      <category>SCG</category>
      <category>시스템컨설턴트그룹</category>
      <author>yesjuhee</author>
      <guid isPermaLink="true">https://scg-skku.tistory.com/4</guid>
      <comments>https://scg-skku.tistory.com/4#entry4comment</comments>
      <pubDate>Sun, 22 Mar 2026 21:49:25 +0900</pubDate>
    </item>
    <item>
      <title>MinIO에 다량의 파일을 빠르게 업로드하는 방법</title>
      <link>https://scg-skku.tistory.com/3</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8h1NB/dJMcacChKP7/V9f3Un7eWxW1tmYBFYDD9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8h1NB/dJMcacChKP7/V9f3Un7eWxW1tmYBFYDD9K/img.png&quot; data-alt=&quot;MinIO 로고&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8h1NB/dJMcacChKP7/V9f3Un7eWxW1tmYBFYDD9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8h1NB%2FdJMcacChKP7%2FV9f3Un7eWxW1tmYBFYDD9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;300&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MinIO 로고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 시스템컨설턴트그룹 28기 인프라 담당 양현준입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 MinIO를 다루면서 겪은 일과 다량의 파일을 효율적으로 MinIO에 업로드하는 방법에 대해 정리했습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MinIO란?&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MinIO는 고성능 오픈소스 오브젝트 스토리지로 Amazon S3와 호환되는 API를 제공하는 오브젝트 스토리지입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소스코드가 AGPLv3 라이선스 하에 공개되어 있는 오픈소스 소프트웨어입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;MinIO Object Browser&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MinIO는 관리자 홈페이지(Object Browser)를 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 손쉽게 MinIO 클러스터를 관리하고, 오브젝트 브라우저 기능을 통해 클러스터에 파일을 업로드하고 다운로드 받을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;파일 마이그레이션을 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 단체에서 기존에 운영하던 애플리케이션이 있습니다. 이는 로컬 파일시스템을 이용해서 사용자로부터 받은 파일을 로컬에 저장하고 이를 서빙하도록 코드가 작성되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 점차 컨테이너 기반으로 인프라 및 모든 애플리케이션을 운영하기로 결정함에 따라서 해당 애플리케이션도 MinIO 오브젝트 스토리지를 적극적으로 이용할 수 있게끔 마이그레이션하고, 파일들을 MinIO에 업로드해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일들은 모두 합해 약 35GiB 정도 되었고, 파일의 수는 약 17,000개 정도 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 마이그레이션과 관련한 내용은 &lt;a href=&quot;https://stringju.tistory.com/41&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Web UI를 통한 첫번째 시도&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cygNip/dJMcaa5w2g8/aZKKGEMEckkHRs2G4NvhZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cygNip/dJMcaa5w2g8/aZKKGEMEckkHRs2G4NvhZ1/img.png&quot; data-alt=&quot;MinIO Object browser. minio 제공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cygNip/dJMcaa5w2g8/aZKKGEMEckkHRs2G4NvhZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcygNip%2FdJMcaa5w2g8%2FaZKKGEMEckkHRs2G4NvhZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;791&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MinIO Object browser. minio 제공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 마이그레이션한 애플리케이션의 QA 진행을 위해 1차로 파일들을 업로드해야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 MinIO에서 제공되는 Object Browser를 이용해 파일을 업로드 하려고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 전부 선택해서 업로드하는데, 업로드가 하나하나 되면서 매우 느리게 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 파일을 업로드하니 약 2시간이 지나있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 파악하기 위해 Claude에게 물어봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lMz1D/dJMcaakaala/MBuKy7kKeypMgefgxeI7hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMz1D/dJMcaakaala/MBuKy7kKeypMgefgxeI7hk/img.png&quot; data-alt=&quot;minio 관리자 페이지를 통해 파일을 업로드하면 받는 response header&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMz1D/dJMcaakaala/MBuKy7kKeypMgefgxeI7hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMz1D%2FdJMcaakaala%2FMBuKy7kKeypMgefgxeI7hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;379&quot; height=&quot;282&quot; data-origin-width=&quot;379&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;minio 관리자 페이지를 통해 파일을 업로드하면 받는 response header&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 파일 업로드에 사용한크롬 브라우저는 HTTP/1.1 프로토콜 기준으로 도메인당 최대 6개의 HTTP 요청을 처리한다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약 17,000개의 파일을 업로드하니 그만큼 각 파일별로 HTTP POST(PUT) 요청이 발생하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MinIO 관리자 페이지는 각 파일을 PutObject API로 파일을 전송합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;17,000번의 독립적인 요청으로 인해 그만큼의 TCP 핸드셰이크와 HTTP 오버헤드가 발생하게 되는 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파일 중 약 15,000개가 개별 용량이 100MB가 안되는 작은 파일이기에 오버헤드 비율이 더 높아질 수 밖에 없었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 파일을 로컬 컴퓨터에서 브라우저로 메모리에 올리기에 메모리 사용량도 크게 늘었을 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것은 명백한 제 실수이긴 합니다만, 작업할 때 유선랜을 사용하지 않고 무선랜을 사용해 네트워크 레이턴시가 더 심하게 발생했을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;파일 업로드의 대안, Rclone&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYcZgl/dJMcagYXTDZ/ekmGfM5KMirdsYPfbktQVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYcZgl/dJMcagYXTDZ/ekmGfM5KMirdsYPfbktQVK/img.png&quot; data-alt=&quot;Rclone 로고.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYcZgl/dJMcagYXTDZ/ekmGfM5KMirdsYPfbktQVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYcZgl%2FdJMcagYXTDZ%2FekmGfM5KMirdsYPfbktQVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;216&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Rclone 로고.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Rclone은 Go 언어로 만든 클라우드 스토리지 특화 업로드 및 다운로드 프로그램입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Rclone의 성능을 볼 수 있는 극단적인 사례가 하나 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 대학교에서 제공하는 구글 드라이브는 Google Workspace for Education 서비스로부터 제공이 되는데, 2018년에는 해당 약관에 대역폭에 대한 제한이 없었다고 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 파악한 캘리포니아 대학교의 한 학생이 rclone을 이용해 무려 총 128TB의 파일들을 구글 드라이브에 400MB/s (MB/s입니다. Mbps가 아니에요.) 가 넘어가는 속도로 업로드를 시도한 사례가 있었습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;아무튼, MinIO는 Amazon S3와 호환되는 API를 제공하기 때문에 rclone에서 S3를 Storage로 지정하여 바로 연결할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Rclone 설치 및 설정&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Rclone을 설치하는 방법은 간단합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772905985806&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl https://rclone.org/install.sh | sudo bash&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;위 명령어를 통해 간단하게 rclone을 설치할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772905998356&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rclone config

n) New remote
name&amp;gt; minio
Storage&amp;gt; s3
provider&amp;gt; Minio
env_auth&amp;gt; false
access_key_id&amp;gt; YOUR_ACCESS_KEY
secret_access_key&amp;gt; YOUR_SECRET_KEY
region&amp;gt; us-east-1
endpoint&amp;gt; http://MINIO_HOST:9000
location_constraint&amp;gt;
acl&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설정 명령어를 통해 S3 스토리지, minio provider를 지정하여 세팅할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 아래 명령어를 통해 버킷 목록이 나오면 정상적으로 연결된 것이라고 할 수 있겠습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772906009224&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rclone lsd minio:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rclone 파일 업로드 및 검증&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파일 업로드는 rclone copy 명령어를 이용해 진행할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저의 경우에는 아래처럼 사용했습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1772906033915&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rclone copy ./webdata minio:mybucket/webdata \
  --progress \
  --fast-list \
  --transfers 8 \
  --checkers 16 \
  --retries 10 \
  --low-level-retries 20 \
  --s3-upload-concurrency 4 \
  --check-first \&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;progress 옵션은 실시간 전송 현황을 콘솔에 표시합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;fast-list 옵션은 메모리를 더 사용하는 대신 목록 조회시 API 호출을 줄여 속도를 향상시킵니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;transfer 옵션은 동시에 전송할 파일의 개수입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;checker 옵션은 전송 전 파일 비교를 수행하는 병렬 스레드의 수입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;retries 옵션은 전체 작업 실패시 재시도 횟수입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;low-level-retries 옵션은 개별 HTTP 요청 실패시 저수준 재시도 횟수입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;s3-upload-concurrency 옵션은 단일 파일의 멀티파트 업로드 내 병렬 청크 수입니다. 대용량 파일에 효과적이라고 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;check-first는 전송 시작 전 모든 파일 체크를 먼저 완료한 뒤에 업로드를 시작하는 옵션입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;업로드가 완료되면 아래 명령어를 통해 누락된 파일 또는 손상된 파일이 있는지 추가로 검증할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1772906050004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rclone check ./webdata minio:mybucket/webdata --one-way --fast-list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Rclone을 이용해 병렬처리를 잘 활용하여 파일을 업로드했더니, 35GiB 파일을 모두 업로드하는데 약 10분 정도가 소요되었습니다. 웹 콘솔에 모두 반영되는데까지는 약 5분정도 추가로 더 걸렸습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Rclone을 이용한 덕분에 매우 큰 시간단축을 이뤘습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 파일을 이전하는 문제 때문에 서비스를 이전하는 총 다운타임을 4-5시간 정도로 잡았는데 실제 작업시간은 1시간 밖에 걸리지 않았습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 SCG에서 팀블로그를 개설하면서 저는 처음 여기에 글을 쓰게 되었는데요. 잘 읽으셨을지 모르겠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모쪼록 도움이 되었길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라나 백엔드, 기타 잡다한 이야기에 관심있으시다면 &lt;a href=&quot;https://stringju.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;제 개인 블로그&lt;/a&gt;에도 한번 놀러와주세요. 감사합니다!&lt;/p&gt;</description>
      <category>Infra</category>
      <category>minio</category>
      <category>시스템컨설턴트그룹</category>
      <category>인프라</category>
      <category>파일 업로드</category>
      <author>현주씌</author>
      <guid isPermaLink="true">https://scg-skku.tistory.com/3</guid>
      <comments>https://scg-skku.tistory.com/3#entry3comment</comments>
      <pubDate>Sun, 8 Mar 2026 02:56:10 +0900</pubDate>
    </item>
    <item>
      <title>[개발로그] 웹 개발 단체가 네이티브 앱을 만들어야 할 때 벌어지는 일  -React + Tauri</title>
      <link>https://scg-skku.tistory.com/2</link>
      <description>&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;손전등 켜는 거&amp;hellip; 웹에서는 안 되잖아요?&quot;&lt;/p&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;이 한마디에서 시작된, SCG의 모바일 앱 개발 대장정을 기록합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;프롤로그: 새로운 의뢰, 그리고 낯선 영역&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어느 날, SCG에 새로운 프로젝트 의뢰가 들어왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요구사항을 정리해보니, 이건 단순한 웹사이트가 아니었어요. 푸시 알림, 생체 인증, 네이티브 UI 위젯, 앱스토어 배포까지. 지금까지 SCG가 해왔던 웹 개발의 영역을 한참 벗어나는 스펙이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 25년 동안 웹을 해온 단체입니다. React, Next.js, TypeScript &amp;mdash; 이런 건 눈 감고도 합니다. 그런데 모바일 네이티브? iOS 빌드? APK 사이닝? 이건 처음이었어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 이런 상황에서 선택지는 두 가지입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;외주 맡기자&quot; 아니면, &quot;우리가 직접 부딪혀보자.&quot;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SCG는 당연히 후자를 택했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 1. 스택 전쟁 &amp;mdash; &quot;Flutter냐, WebView냐, 그것이 문제로다&quot;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트의 첫 번째 관문은 기술 스택 선정이었습니다. 이 단계에서 구성원들 사이에 상당히 치열한 논의가 오갔는데요, 크게 세 가지 선택지가 테이블 위에 올라왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Flutter.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모든 걸 네이티브 수준으로 만들 수 있다는 점에서 매력적이었습니다. 실제로 팀 내에서도 개발이 용이하다는 평가가 나왔어요. 툴링이 압도적이고, VS Code 안에서 DevTools까지 쓸 수 있고, Hot Reload도 훌륭하다고요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 저희의 고민은 다른 곳에 있었습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;Flutter에 익숙한 개발자를 지속적으로 공급받을 수 있을지가 가장 큰 문제입니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학생 개발 단체의 숙명이죠. 매 학기 구성원이 바뀝니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;누군가가 졸업하면 그 자리를 새로운 신입 멤버가 채워야 하는데, Flutter 숙련자를 매번 구할 수 있을까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기능 구현 수준의 학습은 가능하겠지만, 코드 리뷰를 해주고 좋은 아키텍처로 설계할 수 있는 수준의 개발자는 웹에 비해 희소합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;거기에 한 가지 더. Flutter는 OTA(Over-The-Air) 업데이트가 공식적으로 지원되지 않습니다. Shorebird라는 서드파티 솔루션이 있긴 한데, iOS에서는 약관 위반의 회색지대에 놓여 있었어요. 전문 QA 인력이 없는 학생 단체에서 매번 앱 심사를 거쳐 릴리즈하기엔 에너지 소모가 너무 컸습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Capacitor + React.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;오래된 만큼 플랫폼 연동이 풍부하다는 장점이 있었지만, 팀원이 직접 PoC를 돌려본 결과 &quot;툴링이 별로&quot;라는 냉정한 평가가 돌아왔습니다. DevTools도 수동으로 Chrome Inspect를 써야 했고, 개발 서버 연동도 ionic 프레임워크 없이는 수동이었어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tauri + React.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여기서 반전이 일어났습니다. Tauri는 상대적으로 신생이지만, HMR 지원도 잘 되고, Android Studio 설치 없이도 빌드가 가능했어요. 무엇보다 웹 기술 그대로 &amp;mdash; React + Vite &amp;mdash; 를 쓸 수 있다는 점이 결정적이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 결정적인 한 마디가 나왔습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;nav bar는 네이티브 위젯으로 관리하고, 주 화면은 웹뷰로 관리하는 등 섞었을 때 장점이 많은 것 같습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄이 방향을 바꿨어요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 2. &quot;코드 섞어 쓰는 건 항상 지옥입니다&quot;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Flutter + WebView 하이브리드라는 매력적인 아이디어가 나왔지만, 곧바로 반론이 터졌습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;코드 섞어 쓰는 건 항상 지옥입니다&amp;hellip;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;핵심 쟁점은 이거였어요. 웹뷰 안에서 Flutter가 제공하는 네이티브 UI 엘리먼트를 positioning하는 것이 기술적으로 가능한지, 그리고 가능하더라도 유지보수가 지속 가능한지.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;토스급의 sleek한 UX를 구현할 수 있을지에 대한 질문도 나왔습니다. 한 팀원이 실제로 React에서 iOS 스타일의 bottom drawer를 구현해본 경험을 공유했는데 &amp;mdash; &quot;리액트로 거의 흑마법을 써야 합니다&quot;라는 코멘트와 함께 말이죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JavaScript와 네이티브 영역을 이어주는 브릿지 함수, IPC(Inter-Process Communication) 같은 솔루션도 논의됐지만 &quot;trivial하지 않을 것 같다&quot;는 의견이 지배적이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, 논의는 이쪽으로 수렴하기 시작했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;대부분의 기능은 웹으로 충분하다. 네이티브가 필요한 영역은 극히 제한적이다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;푸시 알림, 외부 링크 열기, 결제 연동 정도. 나머지는 전부 웹으로 커버할 수 있었습니다. 그리고 웹뷰 방식은 모바일 웹과 PC를 최소 공수로 동시에 커버할 수 있다는 강력한 장점이 있었어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 최종 방향이 정해졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tauri + React (WebView 기반) + 필요한 곳만 네이티브 플러그인.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 3. 프로토타입의 탄생 &amp;mdash; &quot;의외로 큰 난관은 없었습니다&quot;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;방향이 정해지자 속도가 붙었습니다. 우리는 PoC(Proof of Concept) 프로토타입 스펙을 정의했어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로토타입에서 검증할 것들:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹의 버튼으로 디바이스 손전등 켜기/끄기&lt;/li&gt;
&lt;li&gt;손전등 상태를 웹뷰 내에 표시&lt;/li&gt;
&lt;li&gt;외부 링크를 디바이스 기본 브라우저로 이동&lt;/li&gt;
&lt;li&gt;커스텀 스킴 처리 (카카오톡, 페이북 등)&lt;/li&gt;
&lt;li&gt;내부 링크는 SPA처럼 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Tauri로 작업을 시작했는데, 결과는 예상 밖이었습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import { toggle } from &quot;@sosweetham/tauri-plugin-torch-api&quot;;

function App() {
  const [flashOn, setFlashOn] = useState(false);
  useEffect(() =&amp;gt; { toggle(flashOn) }, [flashOn]);

  return (
    &amp;lt;main className=&quot;container&quot;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setFlashOn((f) =&amp;gt; !f)}&amp;gt;
        Toggle Flash ({flashOn ? &quot;Off&quot; : &quot;On&quot;})
      &amp;lt;/button&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;손전등? 플러그인 하나로 해결&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생체인증?&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import { authenticate } from &quot;@tauri-apps/plugin-biometric&quot;;
// ...
&amp;lt;button onClick={() =&amp;gt; authenticate(&quot;몰?루&quot;)}&amp;gt;생체 인증&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것도 해결&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;알림 전송까지.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import { requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
// ...
&amp;lt;button onClick={() =&amp;gt; sendNotification(&quot;몰?루&quot;)}&amp;gt;알림 전송&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전부 해결.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드 APK를 빌드해서 self-sign하고, Cloudflare 터널로 웹 서버를 올리고, 실제 디바이스에서 테스트까지. 이 모든 과정이 한 시간도 채 걸리지 않았습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;역시 플러그인이 사기네요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;맞는 말이었어요. Tauri 플러그인 생태계가 생각보다 훨씬 풍부했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 4. iOS, 그 험난한 길&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안드로이드에서의 성공에 고무되어, 다음 타겟은 iOS였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서부터 진짜 전쟁이 시작됐어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음부터 순탄치 않았습니다. npm run tauri ios dev를 실행했더니 Xcode가 열리고, 시뮬레이터가 뜨고, 55701 포트로 뭔가 통신하기 시작하는데 &amp;mdash; 빌드가 실패합니다. 에러 로그도 제대로 안 보여요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 Xcode에 익숙하지 않아 로그를 찾는 것부터 난관이었습니다. 결국 Slack 허들(음성 통화)을 켜고 화면공유를 하면서 실시간 디버깅에 돌입했죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 뒤에 이어진 1시간 29분의 기록은, 그야말로 처절했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 1: Rust 에디션 호환성.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Cargo가 2024 edition을 요구했는데 맞지 않았어요. rustup update로 해결.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 2: 네트워크.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;iOS 디바이스는 개발 컴퓨터와 같은 와이파이 네트워크에 있어야 했습니다. 근데 학교 와이파이가 기기 간 통신을 막고 있었어요. 안드로이드는 USB로 그냥 되는데, iOS는 와이파이가 필수.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 3: 디바이스 페어링 무한루프.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;Device is busy&quot;라는 메시지가 반복적으로 나타나면서 앱 설치가 안 됐습니다. Xcode에서 언페어링(unpair)을 시도하는데, 우클릭이 안 먹히고, 설정이 꼬이고&amp;hellip;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 4: 1Password(Warp) 간섭.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;알고 보니 개발자의 맥북에 켜져 있던 VPN 앱이 로컬 네트워크 통신을 방해하고 있었습니다. 이걸 끄자 페어링이 됐어요.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 5: 로컬 네트워크 퍼미션.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;iOS는 앱이 로컬 네트워크에 접근하려면 Info.plist에 권한을 명시해야 합니다. 이걸 설정하지 않으면 웹뷰가 로컬 개발 서버에 접근할 수 없었어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 문제를 하나씩 격파하고, 마침내 &amp;mdash;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;오, 됐다, 됐다. 완벽해요. 네이티브 완전 떠요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;손전등 토글, 생체인증(Face ID), 네이티브 다이얼로그, 알림 전송. 전부 iOS 실기기에서 완벽하게 동작했습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;와 미쳤는데. 이거 DX 괜찮은데요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹 코드를 수정하면 iOS 기기에서 즉시 반영되는 HMR(Hot Module Replacement)도 확인. 이 순간, Tauri + WebView 조합의 가능성이 완전히 검증됐습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 5. 런타임에서 갈라지는 세계&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로토타입을 통해 흥미로운 발견도 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 같은 URL을 열어보니, PC 크롬에서는 알림 권한 요청 팝업이 뜹니다. 네이티브 앱에서만 되는 줄 알았던 기능이 웹에서도 작동한 거죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 발견이 아키텍처 결정에 중요한 영향을 미쳤습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// 앱 환경인지 판별하는 한 줄
const isApp = '__TAURI__' in window;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄이면 충분했어요. 기본적으로는 웹사이트를 개방하고, 네이티브에서만 가능한 기능(생체인증, 푸시 알림 등)은 런타임 분기로 처리하면 됩니다. 개발할 때도 그냥 브라우저에서 모바일 viewport로 세팅해두고 작업하면 돼요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일 웹 + 데스크톱 웹 + 네이티브 앱을 하나의 코드베이스로 커버&lt;/li&gt;
&lt;li&gt;기존 웹 개발 노하우를 그대로 활용&lt;/li&gt;
&lt;li&gt;신입 멤버 온보딩 비용 최소화 (React만 알면 됨)&lt;/li&gt;
&lt;li&gt;대부분의 업데이트는 서버사이드(웹)에서만 하면 끝&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Chapter 6. 아직 남은 전투들&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로토타입은 성공적이었지만, 프로덕션까지는 아직 갈 길이 멀어요. 우리가 앞으로 풀어야 할 과제들을 정리해봤습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 앱스토어 배포와 심사&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애플 개발자 프로그램 연회비 ₩129,000. 금액 자체보다, 앱 심사를 통과해야 한다는 게 핵심입니다. 권한 요청에 대한 명확한 사유, 사용하지 않는 권한의 제거, 심사 가이드라인 준수 등을 꼼꼼히 챙겨야 합니다. 생각보다 까다로운 영역이에요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 푸시 알림 &amp;mdash; 로컬이 아닌 리모트&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로토타입에서 구현한 건 로컬 알림이었습니다. 실제 서비스에서는 FCM(Firebase Cloud Messaging)을 통한 리모트 푸시가 필요해요. Tauri에 FCM 플러그인이 있긴 하지만, 서버 인프라 구축과 토큰 관리까지 고려해야 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 오프라인 대응과 PWA&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Tauri 앱 안에 PWA(Progressive Web App)를 래핑하면 오프라인에서도 동작하는 앱을 만들 수 있습니다. Vite PWA 플러그인을 사용하면 캐싱과 서비스 워커 설정을 간단하게 할 수 있어요. 한 팀원의 표현을 빌리자면 &quot;마트로슈카 인형급 래핑&quot;이지만, 이게 OTA 업데이트 문제에 대한 가장 현실적인 해답이기도 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. SPA 라우팅과 외부 접근&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;React Router로 SPA 라우팅을 구현하되, 외부에서 특정 경로로 직접 접근할 수 있어야 합니다. 해시(#) 라우팅을 쓸지, History API를 쓸지. 웹과 앱 양쪽에서 모두 자연스럽게 동작하도록 설계해야 해요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. 네이티브 Alert의 부재&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의외의 발견이었는데, iOS 웹뷰에서는 JavaScript의 window.alert()가 먹히지 않았습니다. 대신 Tauri의 다이얼로그 플러그인을 사용해야 했어요. 이런 식으로 웹에서 당연히 되는 것들이 네이티브 래퍼 안에서는 안 되는 케이스가 곳곳에 숨어 있을 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6. 지속 가능한 인력 구조&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이건 기술적 과제가 아니라 조직적 과제입니다. 1학기 스터디에서 React를 배우는 신입 멤버들이, 2학기에 이 프로젝트에 자연스럽게 합류할 수 있어야 합니다. Tauri + React 조합을 선택한 가장 큰 이유가 바로 이 지점이에요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;에필로그: &quot;그만큼 안전하다는 거죠&quot;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;iOS 디버깅 중, 허들에서 나온 대화가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;디바이스 페어링을 할 때마다 매번 &quot;Trust this computer?&quot;를 물어보는 iOS에 지쳐가던 그때, 반 농담으로 던진 재원님의 한 마디...&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;그만큼 안전하다는 거죠.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웃긴 말이었지만, 어떻게 보면 이번 프로토타이핑의 교훈을 관통하는 말이기도 합니다. iOS 개발이 까다로운 데에는 이유가 있고, 네이티브 앱 개발이 복잡한 데에도 이유가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중요한 건, 그 복잡성 앞에서 멈추지 않고 하나씩 격파해 나가는 것.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SCG는 웹 개발 단체입니다. 하지만 이번 프로젝트를 통해 우리의 웹 기술이 네이티브 영역까지 확장될 수 있다는 걸 직접 증명했어요. 하루 저녁, Slack 허들 하나, 디바이스 두 대. 그리고 끈질긴 삽질.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;iOS 유기가 마려워지는 하루였습니다 ㅋㅋㅋㅋ&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;유기하진 않았습니다. 대신 정복했어요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;TL;DR&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결정 사항 선택 이유&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;프레임워크&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Tauri + React&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;웹 기술 재사용, 신입 멤버 온보딩 용이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;UI 구현&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;95% WebView + 5% 네이티브 플러그인&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;유지보수 단순화, 모바일 웹 동시 대응&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;업데이트 전략&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;PWA 래핑으로 캐싱/OTA 해결&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;앱 심사 없이 빠른 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;환경 분기&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;'__TAURI__' in window&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;하나의 코드베이스로 웹/앱 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;라우팅&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;React Router (해시 라우팅)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;정적 배포 + 외부 링크 대응&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 SCG 내부 실제 기술 논의를 바탕으로 작성되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실시간으로 의견을 나누고, 즉석에서 프로토타입을 만들고, 함께 삽질하는 &amp;mdash; 이게 SCG가 문제를 푸는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;함께 삽질하고 싶은 분, 언제든 환영합니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>hybridapp</category>
      <category>SCG</category>
      <category>Tauri</category>
      <category>webview</category>
      <category>시스템컨설턴트그룹</category>
      <author>장재원</author>
      <guid isPermaLink="true">https://scg-skku.tistory.com/2</guid>
      <comments>https://scg-skku.tistory.com/2#entry2comment</comments>
      <pubDate>Tue, 3 Mar 2026 16:44:42 +0900</pubDate>
    </item>
    <item>
      <title>SCG 블로그를 개설합니다!</title>
      <link>https://scg-skku.tistory.com/1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SCG 개발 블로그를 시작합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요, 성균관대학교 소프트웨어융합대학 학생 개발 단체 SCG(System Consultant Group)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCG는 올해로 25년째 운영되고 있는 단체로, 단과대학 내 서비스들을 직접 개발하고 유지보수하는 일을 하고 있습니다. 학교 시스템을 실제로 운영하면서 겪는 장애 대응, 인프라 관리, 코드 리뷰, 문서화 같은 경험들이 매 학기 쌓이는데요&amp;mdash;사실 이런 내용들이 밖으로 잘 나가지 않는 게 늘 아쉬웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에 개발 블로그를 열게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 블로그를 시작하나요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면, 예전부터 &quot;블로그 해야지&quot;라는 이야기는 계속 나왔습니다. 근데 매번 흐지부지됐어요. 바쁘기도 하고, 글 쓰는 게 개발하는 것보다 훨씬 귀찮기도 하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 매년 활동이 끝나고 나면 항상 같은 후회를 하게 됩니다. &quot;아 그때 정리해둘 걸.&quot; 스터디에서 공부한 내용, 프로젝트 하면서 삽질한 기록, 서버 터졌을 때 어떻게 대응했는지&amp;mdash;이런 것들이 슬랙 스레드 어딘가에 묻혀 있거나, 노션 깊숙한 곳에 잠들어 있거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 진짜로 해보려 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 글을 올릴 건가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거창한 건 아닙니다. 저희가 평소에 하는 활동들을 기록하는 게 목표입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학기별 스터디에서 다룬 기술 주제들&lt;/li&gt;
&lt;li&gt;신입 프로젝트 진행 과정과 회고&lt;/li&gt;
&lt;li&gt;내부 서비스를 운영하면서 만난 장애와 해결 과정&lt;/li&gt;
&lt;li&gt;인프라 개선기, 코드 리팩토링 경험 같은 실무 이야기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽한 글보다는, 나중에 돌아봤을 때 &quot;아 우리 이런 거 했었지&quot; 하고 떠올릴 수 있는 기록을 남기는 게 먼저라고 생각합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앞으로의 계획&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단은 티스토리에서 팀 블로그 형태로 운영을 시작합니다. 회원들이 부담 없이 글을 쓸 수 있는 환경을 먼저 만드는 게 중요하다고 판단했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장기적으로는 SCG 자체 블로그를 개발하는 것도 논의 중입니다. 작성 프로세스를 더 매끄럽게 만들거나, 회원들의 활동 이력과 연결되는 구조를 만들어보고 싶은 욕심도 있고요. 하지만 그건 차근차근, 하나씩 해보려 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;25년간 이어져 온 단체인 만큼, 그동안 쌓인 경험과 노하우가 정말 많습니다. 다만 그걸 밖으로 꺼내는 데에는 좀 게을렀던 것 같습니다. 이 블로그가 그 시작점이 되었으면 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 종종 들러주세요. 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SCG 운영진 일동&lt;/b&gt;&lt;/p&gt;</description>
      <category>Culture</category>
      <category>SCG</category>
      <category>성균관대</category>
      <category>시스템컨설턴트그룹</category>
      <author>장재원</author>
      <guid isPermaLink="true">https://scg-skku.tistory.com/1</guid>
      <comments>https://scg-skku.tistory.com/1#entry1comment</comments>
      <pubDate>Wed, 25 Feb 2026 14:15:25 +0900</pubDate>
    </item>
  </channel>
</rss>