<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyoin_0219.log</title>
        <link>https://velog.io/</link>
        <description>배워야 할 게 많은 개발자... 하지만 공부를 포기하지 않지!!</description>
        <lastBuildDate>Sun, 22 Mar 2026 13:12:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyoin_0219.log</title>
            <url>https://velog.velcdn.com/images/hyoin_0219/profile/68f9a567-d068-427e-9d22-b0dc37f3ec52/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyoin_0219.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyoin_0219" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[ROS2] bridge node로 단일로봇 코드를 멀티로봇으로 확장]]></title>
            <link>https://velog.io/@hyoin_0219/ROS2-bridge-node%EB%A1%9C-%EB%8B%A8%EC%9D%BC%EB%A1%9C%EB%B4%87-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%A9%80%ED%8B%B0%EB%A1%9C%EB%B4%87%EC%9C%BC%EB%A1%9C-%ED%99%95%EC%9E%A5</link>
            <guid>https://velog.io/@hyoin_0219/ROS2-bridge-node%EB%A1%9C-%EB%8B%A8%EC%9D%BC%EB%A1%9C%EB%B4%87-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%A9%80%ED%8B%B0%EB%A1%9C%EB%B4%87%EC%9C%BC%EB%A1%9C-%ED%99%95%EC%9E%A5</guid>
            <pubDate>Sun, 22 Mar 2026 13:12:24 GMT</pubDate>
            <description><![CDATA[<h2 id="1-프로젝트-소개">1. 프로젝트 소개</h2>
<p>졸업 프로젝트로 쇼핑몰 환경에서 여러 대의 로봇이 자율주행으로 물건을 배송하는 시스템을 만들었다. ROS2 기반 네비게이션 스택 위에 FastAPI 백엔드, React 프론트엔드까지 붙인 꽤 규모 있는 프로젝트였다.</p>
<hr>
<h2 id="2-통합-작업이-나한테-몰린-이유">2. 통합 작업이 나한테 몰린 이유</h2>
<p>나는 FMS를 구현하는 역할을 맡았다. FMS 특성상 여러 대의 로봇을 중앙에서 관리해야 했고, 자연스럽게 멀티로봇 환경 구축이 내 작업의 전제 조건이 됐다.</p>
<p>팀원들은 각자 맡은 기능을 ROS2 노드로 구현하는 데 집중했다. 네비게이션, 장애물 감지, 서버 통신 등 기능 단위로 나눠서 개발했고, 각자의 환경에서는 잘 돌아갔다.</p>
<p>문제는 그게 전부 <strong>단일 로봇 환경</strong> 기준이었다는 것. 멀티로봇으로 확장하려면 누군가 전체를 묶는 작업을 해야 했는데, FMS를 맡은 나로서는 선택의 여지가 없었다.</p>
<hr>
<h2 id="3-내가-설계한-것">3. 내가 설계한 것</h2>
<h3 id="3-1-state-기반-노드토픽-제어">3-1. state 기반 노드/토픽 제어</h3>
<p>로봇의 동작을 state 단위로 나눴다. 각 state에 따라 어떤 노드를 실행하고, 어떤 토픽을 구독할지를 중앙에서 제어하는 구조를 만들었다:
<img src="https://velog.velcdn.com/images/hyoin_0219/post/131f638d-0ded-41f9-90d2-ceb94921b771/image.png" alt=""></p>
<p>일부 state는 시간 관계상 launch 파일을 실행해 노드가 생성되고 토픽을 계속 발행하도록 구현했다. 단순히 코드를 합치는 게 아니라, <strong>언제 무엇을 켜고 끌지</strong>를 설계하는 작업이었다.</p>
<h3 id="3-2-bridge-node로-내외부-인터페이스-분리">3-2. bridge node로 내외부 인터페이스 분리</h3>
<p>팀원들의 코드를 전부 멀티로봇 환경에 맞게 수정하는 건 현실적으로 불가능했다. 코드 양도 많았고, 각자 개발한 로직을 건드리다가 오히려 더 많은 문제가 생길 수 있었다.</p>
<p>그래서 선택한 방식이 <strong>bridge node 삽입</strong>이었다.
<img src="https://velog.velcdn.com/images/hyoin_0219/post/fb0f42b8-394d-4431-bb4f-ef38cac6562a/image.png" alt=""></p>
<ul>
<li><strong>외부 (관제서버 ↔ 로봇)</strong>: namespace로 로봇을 구분</li>
<li><strong>내부 (로봇 내부 노드들)</strong>: 단일 로봇 환경 그대로 동작</li>
</ul>
<p>팀원 코드는 손대지 않으면서, 외부에서 봤을 때는 멀티로봇 시스템처럼 동작하도록 인터페이스를 분리한 것이다.</p>
<hr>
<h2 id="4-그-과정에서-만난-문제">4. 그 과정에서 만난 문제</h2>
<p>구조 자체는 깔끔하게 될 거라 생각했다. 근데 막상 실행하니 충돌이 났다.</p>
<p>원인은 팀원들의 코드 곳곳에 <strong>namespace나 포트 번호가 디폴트값 또는 ~/.bashrc의 환경변수로 하드코딩</strong>되어 있었던 것. 로봇이 한 대일 때는 문제가 없었지만, 여러 대를 동시에 띄우니 namespace가 겹치고 포트가 충돌했다.</p>
<p>일단 전체 코드를 뒤지며 어디서 어떤 값이 고정되어 있는지 파악하는 작업부터 다시 시작했다. 특히 디폴트값 디버깅이 오래 걸렸다. 에러가 안 뜨고 바로 실행되다 보니 파악 자체가 느렸다.</p>
<hr>
<h2 id="5-해결-방식">5. 해결 방식</h2>
<p>bridge node에서 namespace와 포트를 런타임 파라미터로 주입받도록 구조를 바꿨다. 로봇마다 고유한 값을 launch 시점에 넘겨주는 방식으로, 코드 자체를 수정하지 않고도 충돌 없이 여러 대를 동시에 띄울 수 있게 됐다.</p>
<p>팀원 코드를 최대한 건드리지 않는다는 원칙을 지키면서 문제를 해결했다.</p>
<hr>
<h2 id="6-이-경험에서-얻은-것">6. 이 경험에서 얻은 것</h2>
<p>기능 개발과 통합은 완전히 다른 종류의 일이라는 걸 느꼈다. 기능 개발은 내가 정한 환경 안에서 동작하면 되지만, 통합은 <strong>내가 통제하지 못한 것들</strong> 사이에서 동작해야 한다.</p>
<p>팀원 코드를 건드리지 않으면서 시스템 전체가 돌아가도록 만드는 과정에서, 아이러니하게도 전체 시스템을 가장 깊이 이해하는 사람이 됐다. 토픽 흐름, 노드 간 인터페이스, 환경 의존성까지 전부 머릿속에 그려지는 상태가 됐고, 그게 이후 디버깅에서도 큰 무기가 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AMCL 삽질: 좀비 프로세스였던 sllidar_node]]></title>
            <link>https://velog.io/@hyoin_0219/AMCL-%EC%82%BD%EC%A7%88-%EC%A2%80%EB%B9%84-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%98%80%EB%8D%98-sllidarnode</link>
            <guid>https://velog.io/@hyoin_0219/AMCL-%EC%82%BD%EC%A7%88-%EC%A2%80%EB%B9%84-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%98%80%EB%8D%98-sllidarnode</guid>
            <pubDate>Sun, 15 Mar 2026 16:19:49 GMT</pubDate>
            <description><![CDATA[<h2 id="증상">증상</h2>
<p>오늘은 삽질 기록이다.</p>
<p><code>ros2 launch</code>를 실행했는데 AMCL이 <code>map → odom</code> TF를 퍼블리시하지 않는 상황이 발생했다.</p>
<p>에러 로그도 없었다. AMCL 노드는 멀쩡히 떠 있었다. 근데 TF는 없었다.</p>
<p>rviz를 열어보면, /map topic에 아무것도 안 뜬다.</p>
<p><code>map → odom</code>이 끊겨 있으니 당연히 로봇 위치도 안 잡히고, Nav2도 뻗는다. 아무 에러도 없는데 아무것도 안 된다. 제일 무서운 패턴이다...</p>
<p>비슷한 증상이 하나 더 있었다. <code>Ctrl+C</code>를 연타해서 bringup을 끄고 바로 다시 켜면, 이번엔 LiDAR 자체를 인식하지 못하는 에러가 뜨는 것이었다. 처음엔 별개의 문제라고 생각했는데, 알고 보니 같은 원인이었다.</p>
<hr>
<h2 id="의심한-것들">의심한 것들</h2>
<p>처음엔 당연히 AMCL 설정을 의심했다.</p>
<ul>
<li>노드 실행 순서 문제인가?</li>
<li>QoS 미스매치로 토픽을 못 받고 있나?</li>
</ul>
<p>다 확인해봤다. 다 정상이었다.</p>
<p>그다음엔 scan 토픽을 의심했다. AMCL은 <code>/scan</code> 토픽을 받아야 동작하는데, 혹시 LiDAR 데이터가 제대로 안 들어오는 건 아닐까?</p>
<pre><code class="language-bash">ros2 topic echo /scan</code></pre>
<p>데이터는 잘 들어오고 있었다. 의심할 곳이 없어지기 시작했다.</p>
<hr>
<h2 id="범인-특정">범인 특정</h2>
<p>막혀서 기본으로 돌아갔다. 현재 떠 있는 노드 목록부터 다시 확인해보자.</p>
<pre><code class="language-bash">ros2 node list</code></pre>
<p>그런데 출력이 이상했다.</p>
<pre><code>/sllidar_node
/sllidar_node
/amcl
/map_server
...</code></pre><p><code>/sllidar_node</code>가 두 개였다.</p>
<p><code>sllidar_node</code>는 Pinky Pro 로봇에 붙어 있는 LiDAR 패키지의 노드다. bringup을 실행하면 이 노드가 함께 올라오는데, 이전 세션에서 <code>Ctrl+C</code>로 종료했을 때 프로세스가 완전히 죽지 않고 살아남은 것이었다. 겉으로 보기엔 <code>ros2 node list</code>에서 중복으로 뜨는 것 말고는 아무 이상이 없었다.</p>
<p>문제는 이 좀비 노드가 <code>/scan</code> 토픽을 물고 있었다는 것이다. AMCL은 <code>/scan</code>을 정상적으로 구독하고 있었지만, 좀비 <code>sllidar_node</code>와 충돌하면서 데이터를 제대로 처리하지 못했고, 결과적으로 <code>map → odom</code> TF를 퍼블리시하지 못한 것이었다.</p>
<p>구조로 정리하면 이렇다.
<img src="https://velog.velcdn.com/images/hyoin_0219/post/cbc7e08b-6440-468f-8b7a-a9448a7be831/image.png" alt=""></p>
<p>두 노드가 같은 토픽에 퍼블리시하면서 AMCL이 혼선을 일으킨 것이다.</p>
<hr>
<h2 id="해결">해결</h2>
<h3 id="1-좀비-프로세스-죽이기">1. 좀비 프로세스 죽이기</h3>
<p>일단 좀비 프로세스를 죽이는 게 먼저였다.</p>
<pre><code class="language-bash"># 프로세스 PID 확인
ps aux | grep sllidar

# 킬
kill -9 &lt;PID&gt;</code></pre>
<p>이후 다시 launch를 실행하니 <code>map → odom</code> TF가 정상적으로 퍼블리시됐다.</p>
<h3 id="2-재발-방지-systemd-파일">2. 재발 방지: systemd 파일</h3>
<p>재발 방지가 문제였다. bringup 파일은 팀 전체가 공유하는 파일이라 직접 수정하기가 어려운 상황이었다. launch 파일 내부에 종료 핸들러를 달거나 노드 구성을 바꾸는 건 협업 흐름상 맞지 않았다.</p>
<p>그래서 Raspberry Pi에 별도의 systemd 서비스 파일을 만들어서, bringup을 서비스 단에서 관리하도록 했다. <code>systemctl stop</code>으로 끄면 관련 프로세스가 확실히 정리되고, <code>systemctl start</code>로 다시 켜는 방식이다.</p>
<p><code>ExecStopPost</code>에 <code>pkill -f sllidar_node</code>를 넣어뒀기 때문에, 서비스가 종료될 때 좀비 프로세스가 남지 않는다. <code>Ctrl+C</code> 연타로 인한 라이다 인식 실패 문제도 이걸로 같이 잡혔다.</p>
<hr>
<h2 id="회고">회고</h2>
<p>이번 삽질에서 배운 것 하나만 꼽으라면, <strong>에러 로그 없는 버그가 제일 무섭다</strong>는 것이다.</p>
<p>에러가 있으면 거기서부터 따라가면 된다. 근데 노드도 살아있고, 토픽도 들어오고, 로그도 없는데 TF만 없는 상황은 어디서부터 파야 할지 감이 안 잡힌다.</p>
<p>앞으로 ROS2 디버깅 순서를 이렇게 잡기로 했다.</p>
<ol>
<li><code>ros2 node list</code> — 노드 중복 여부 먼저 확인</li>
<li><code>ros2 topic list</code> / <code>ros2 topic echo</code> — 토픽 정상 여부 확인</li>
<li><code>ros2 topic info &lt;topic&gt;</code> — 퍼블리셔/서브스크라이버 수 이상 여부 확인</li>
<li>그다음 설정 파일 확인</li>
</ol>
<p>설정보다 환경 먼저 의심하는 습관을 들이는 게 맞는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nav2의 정밀 도달 한계 극복하기]]></title>
            <link>https://velog.io/@hyoin_0219/Nav2%EC%9D%98-%EC%A0%95%EB%B0%80-%EB%8F%84%EB%8B%AC-%ED%95%9C%EA%B3%84-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyoin_0219/Nav2%EC%9D%98-%EC%A0%95%EB%B0%80-%EB%8F%84%EB%8B%AC-%ED%95%9C%EA%B3%84-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 01 Mar 2026 14:03:02 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-상황">1. 문제 상황</h2>
<p>쇼핑몰 자동화 시스템 프로젝트에서 Pinky 로봇 3대를 Nav2로 자율주행 제어하던 중, 특정 구간에서 반복적으로 로봇이 목표 지점에 도달하지 못하는 문제를 만났다.</p>
<p>증상은 크게 세 가지였다.</p>
<ul>
<li>목표 지점 근처에서 멈추거나 제자리 회전을 반복</li>
<li>좁은 통로 진입 자체를 포기하고 Navigation Failed 반환</li>
<li>goal_tolerance 범위 안에 들어왔다 해도 실제 위치가 부정확한 경우</li>
</ul>
<p>실제로 문제가 발생한 구간은 p3 → p4, p4 → p6, p6 → p8 로, 쇼핑몰 좁은 통로 구간이다. 로봇이 목표 지점 1m 전에서 멈추거나, 진입조차 시도하지 않았다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p><strong>Costmap inflation 문제</strong></p>
<p>Nav2는 장애물 주변에 inflation layer를 적용해 로봇이 안전하게 우회할 수 있도록 한다. 문제는 좁은 통로에서 양쪽 벽의 inflation 영역이 겹쳐버리면, 해당 공간 자체가 &#39;진입 불가&#39;로 판단된다는 것이다. <code>inflation_radius</code>를 낮추면 통로를 지나갈 수 있지만, 장애물에 너무 가까이 붙어 충돌 위험이 높아진다.</p>
<p><strong>AMCL 위치 추정 오차 누적</strong></p>
<p>긴 경로를 이동하는 동안 AMCL 파티클 필터의 오차가 누적되면, 로봇이 자신의 위치를 잘못 인식해 goal_tolerance 기준 자체가 흔들린다.</p>
<hr>
<h2 id="2-해결-전략-nav2--pid-하이브리드">2. 해결 전략: Nav2 + PID 하이브리드</h2>
<p>여러 방법을 검토했다.</p>
<ul>
<li>Nav2 파라미터 튜닝만으로 해결: inflation 낮추면 충돌 위험, goal_tolerance 높이면 정밀도 하락</li>
<li>전체 경로를 PID로 제어: 글로벌 장애물 회피 불가, 실용적이지 않음</li>
<li><strong>Nav2 + PID 하이브리드: 각각의 장점만 취함</strong></li>
</ul>
<p>핵심 아이디어는 단순하다. 원거리는 Nav2가 잘하는 일(경로 계획, 장애물 회피)을 맡기고, 목표 지점 근처의 짧은 구간에서만 Nav2를 취소하고 PID 제어로 전환해 정밀하게 도달하는 것이다.</p>
<hr>
<h2 id="3-구현-상세">3. 구현 상세</h2>
<h3 id="3-1-구간별-pid-zone-설정">3-1. 구간별 PID Zone 설정</h3>
<p>모든 구간에 동일한 PID Zone 거리를 적용하지 않았다. 구간마다 통로 너비, 장애물 분포, 필요 정밀도가 다르기 때문이다.</p>
<table>
<thead>
<tr>
<th align="center">출발 POI</th>
<th align="center">도착 POI</th>
<th align="center">PID 전환 거리</th>
</tr>
</thead>
<tbody><tr>
<td align="center">p3</td>
<td align="center">p4</td>
<td align="center">1.03m</td>
</tr>
<tr>
<td align="center">p4</td>
<td align="center">p6</td>
<td align="center">0.52m</td>
</tr>
<tr>
<td align="center">p6</td>
<td align="center">p8</td>
<td align="center">0.70m</td>
</tr>
</tbody></table>
<pre><code class="language-python"># 구간별 PID 전환 거리 설정
# (출발 POI, 도착 POI) → PID 전환 거리(m)
PID_EDGES = {
    (&#39;p3&#39;, &#39;p4&#39;): 1.03,
    (&#39;p4&#39;, &#39;p6&#39;): 0.52,
    (&#39;p6&#39;, &#39;p8&#39;): 0.70,
}</code></pre>
<p>넓은 구간은 <code>radius=0</code>으로 설정해 Nav2만 사용한다.</p>
<h3 id="3-2-모드-전환-로직">3-2. 모드 전환 로직</h3>
<p>100ms 주기로 현재 위치와 목표 지점 사이의 거리를 체크한다. 거리가 <code>pid_zone_radius</code> 이하로 진입하는 순간 Nav2를 취소하고 PID 모드로 전환한다.
<img src="https://velog.velcdn.com/images/hyoin_0219/post/d2c798a1-4e8f-4ba3-a09c-050049c36481/image.png" alt=""></p>
<h3 id="3-3-pid-제어기">3-3. PID 제어기</h3>
<p>선속도(전진)와 각속도(회전)를 별도의 PID로 제어한다.</p>
<pre><code class="language-python"># Linear PID (전진 제어)
Kp=0.5, Ki=0.0, Kd=0.1
max_vel = 0.15 m/s

# Angular PID (회전 제어)
Kp=1.5, Ki=0.0, Kd=0.2
max_vel = 1.0 rad/s</code></pre>
<p>PID 게인은 처음에 잘 설계된 초기값에서 시작해, 실제 주행 테스트에서 오버슈트가 발생하지 않도록 <code>max_vel</code>만 낮추는 방향으로 조정했다.</p>
<p>한 가지 포인트: <strong>각도 오차가 30° 이상이면 전진 속도를 0으로 설정하고 제자리 회전을 먼저 수행한다.</strong> 방향이 크게 틀어진 상태에서 전진하면 목표에서 더 멀어지는 경우가 생기기 때문이다. (요건 April tag로 yaw 재정렬하는 방식으로 각도오차를 줄이도록 개선할 예정이다)</p>
<h3 id="3-4-핵심-코드">3-4. 핵심 코드</h3>
<p><strong><code>_zone_check()</code>: Nav2 → PID 전환 판단</strong></p>
<pre><code class="language-python">def _zone_check(self):
    dx = self.goal_x - self.current_x
    dy = self.goal_y - self.current_y
    dist = math.sqrt(dx**2 + dy**2)

    key = (self.start_poi, self.goal_poi)
    radius = PID_EDGES.get(key, 0.0)

    if radius &gt; 0 and dist &lt; radius:
        self._switch_to_pid()</code></pre>
<p><strong><code>_pid_loop()</code>: PID 제어 루프 (50Hz)</strong></p>
<pre><code class="language-python">def _pid_loop(self):
    dx = self.goal_x - self.current_x
    dy = self.goal_y - self.current_y
    dist = math.sqrt(dx**2 + dy**2)

    if dist &lt; 0.05:
        self._complete()
        return

    angle_to_goal = math.atan2(dy, dx)
    angle_err = angle_to_goal - self.current_yaw

    # 각도 정규화 (-pi ~ pi)
    angle_err = math.atan2(math.sin(angle_err), math.cos(angle_err))

    angular_vel = self.angular_pid.compute(angle_err)

    # 각도 오차 30도 이상이면 전진 정지
    if abs(angle_err) &gt; math.radians(30):
        linear_vel = 0.0
    else:
        linear_vel = self.linear_pid.compute(dist)

    self._publish_cmd(linear_vel, angular_vel)</code></pre>
<h3 id="3-5-예외-처리">3-5. 예외 처리</h3>
<ul>
<li>Nav2 Navigation Failed 시 1회 자동 재시도</li>
<li><code>goal_generation</code> 번호로 이전 콜백 무시 (race condition 방지)</li>
<li>TF(<code>map → base_footprint</code>) 조회 실패 시 해당 주기 스킵</li>
</ul>
<hr>
<h2 id="4-결과">4. 결과</h2>
<h3 id="before-vs-after">Before vs After</h3>
<table>
<thead>
<tr>
<th align="center">구간</th>
<th align="center">Nav2만 사용</th>
<th align="center">Nav2 + PID</th>
</tr>
</thead>
<tbody><tr>
<td align="center">p4 → p6</td>
<td align="center">목표 0.8m 전에서 정지</td>
<td align="center">목표 도달 (오차 &lt; 5cm)</td>
</tr>
<tr>
<td align="center">p6 → p8</td>
<td align="center">좁은 구간 진입 포기</td>
<td align="center">정상 도달</td>
</tr>
</tbody></table>
<p>p4 → p6 구간은 Nav2만 사용했을 때 항상 0.8m 전에서 멈췄는데, PID 전환 후에는 오차 5cm 이내로 도달하게 됐다. p6 → p8은 아예 진입을 포기하던 구간이었는데 정상 도달이 가능해졌다.</p>
<h3 id="한계점-및-추후-개선-방향">한계점 및 추후 개선 방향</h3>
<ul>
<li><strong>PID Zone에서 동적 장애물 회피 불가</strong> → 해당 구간은 충분히 짧게 설정해 리스크를 최소화</li>
<li><strong>PID 게인 수동 튜닝 필요</strong> → 로봇/환경이 바뀌면 재조정 필요</li>
</ul>
<hr>
<h2 id="5-마무리">5. 마무리</h2>
<p>Nav2는 강력한 프레임워크지만, 좁은 공간에서의 정밀 도달에는 구조적 한계가 있다. inflation을 낮추는 것만으로는 충돌 안전성을 희생해야 하고, goal_tolerance를 높이면 정밀도가 떨어진다.</p>
<p>이번에 구현한 Nav2 + PID 하이브리드 방식은 두 가지를 모두 포기하지 않는 실용적인 해법이었다. &#39;원거리는 Nav2, 근거리는 PID&#39;라는 단순한 원칙이 생각보다 잘 동작했다.</p>
<p>ROS2로 실제 로봇을 제어하면서 교과서적인 파라미터 튜닝만으로 해결되지 않는 문제들이 있다는 걸 느꼈다. 그럴 때는 시스템의 한계를 인정하고, 다른 제어 방식을 조합하는 접근이 유효한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AprilTag로 AMCL yaw 재정렬하기]]></title>
            <link>https://velog.io/@hyoin_0219/AprilTag%EB%A1%9C-AMCL-yaw-%EC%9E%AC%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyoin_0219/AprilTag%EB%A1%9C-AMCL-yaw-%EC%9E%AC%EC%A0%95%EB%A0%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 22 Feb 2026 14:53:10 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가며">1. 들어가며</h2>
<p>쇼핑몰 환경에서 3대의 Pinky 로봇을 운용하는 프로젝트를 진행 중이다. 로봇이 넓은 공간을 돌아다니다 보면 odometry 누적 오차(drift)가 생기고, 이걸 주기적으로 교정해줘야 AMCL 기반 위치 추정이 안정적으로 유지된다.</p>
<p>탑뷰 카메라가 있었다. 천장에서 로봇 위치를 내려다보며 교정하는 방식인데, 구현 자체는 어렵지 않다. 카메라로 로봇을 인식하고 좌표 뽑아서 넣어주면 끝이다. 너무 단순하게 느껴졌다.</p>
<p>그래서 <strong>AprilTag를 이용한 셀프 교정</strong>을 시도해봤다.</p>
<blockquote>
<p>&quot;태그가 맵 어디에 어떤 방향으로 붙어있는지 우리가 이미 안다. 로봇이 그 태그를 카메라로 인식하는 순간, 태그의 기울기를 역산하면 로봇의 절대 yaw를 구할 수 있다.&quot;</p>
</blockquote>
<p>탑뷰 카메라 없이, 로봇 자신이 벽에 붙은 태그를 보는 것만으로 자기 방향을 교정하는 방식이다. 이론은 깔끔한데... 현실은 삽질의 연속이었다.</p>
<h2 id="2-핵심-아이디어">2. 핵심 아이디어</h2>
<p>전체 흐름은 이렇다.</p>
<ol>
<li>맵에 AprilTag를 붙이고, 각 태그의 절대 위치와 방향(<code>TAG_INFO</code>)을 사전에 정의해둔다.</li>
<li>로봇이 이동하다가 카메라로 태그를 인식하면, <code>pupil_apriltags</code>로 태그의 R 행렬(회전 행렬)을 뽑는다.</li>
<li>R 행렬에서 태그 기준 yaw를 계산하고, <code>TAG_INFO</code>의 절대 방향과 합산해서 로봇의 절대 yaw를 역산한다.</li>
<li>역산한 yaw를 <code>/initialpose</code> 토픽으로 AMCL에 주입해서 위치를 교정한다.</li>
</ol>
<p>깔끔해 보이지만, 각 단계마다 함정이 있었다.</p>
<h2 id="3-삽질-1--tag_info-yaw-방향을-반대로-정의했다">3. 삽질 #1 — TAG_INFO yaw 방향을 반대로 정의했다</h2>
<h3 id="문제">문제</h3>
<p>로봇이 태그 정면에 서있을 때 <code>/initialpose</code>의 yaw 값이 약 -2.94였다. 이걸 그대로 TAG_INFO에 넣었다.</p>
<p><strong>시도:</strong> 로봇이 태그 정면에 섰을 때의 yaw(-2.94)를 TAG_INFO에 사용<br><strong>결과:</strong> yaw 교정값이 계속 180도 가까이 틀어져 나옴</p>
<h3 id="원인">원인</h3>
<p>이 값은 <strong>로봇이 바라보는 방향</strong>이다. 카메라에 찍힌 태그 이미지로 로봇 yaw를 역산하려면, TAG_INFO에는 <strong>태그가 map에서 바라보는 방향</strong>이 들어가야 한다. 로봇이 태그를 정면으로 바라보고 있을 때, 태그는 로봇의 반대 방향을 향하고 있다. 즉 두 값은 약 180도 차이가 난다.</p>
<h3 id="해결">해결</h3>
<p>수식으로 고치려고 <code>+π</code>를 더하거나 빼거나 부호를 바꾸는 등 여러 번 시도했는데, 다른 오차들이 섞여서 정확히 안 잡혔다. 결국 가장 확실한 방법으로 바꿨다.</p>
<pre><code>TAG_YAW = 0으로 놓고 계산 yaw 출력
→ 실제 AMCL yaw와 비교
→ 오프셋 = 실제 yaw - 계산 yaw
→ TAG_YAW = 오프셋</code></pre><p>좌표계나 π 계산을 직접 따질 필요 없이, 실측으로 오차를 한 번에 흡수하는 방식이다. 태그를 새로 추가할 때도 동일하게 적용할 수 있어서 재현성도 있다.</p>
<h2 id="4-삽질-2--yaw만-고치면-됐는데-x-y까지-덮어썼다">4. 삽질 #2 — yaw만 고치면 됐는데 x, y까지 덮어썼다</h2>
<h3 id="문제-1">문제</h3>
<p>태그 위치를 알고 있으니, 태그까지의 거리와 방향을 이용해서 로봇의 x, y 위치까지 계산하고 <code>/initialpose</code>로 x, y, yaw를 전부 덮어쓰는 방식으로 설계했다.</p>
<p><strong>시도:</strong> <code>/initialpose</code>로 x, y, yaw 전부 덮어씀<br><strong>결과:</strong> 로봇이 태그 위치로 텔레포트, AMCL 파티클 붕괴</p>
<h3 id="원인-1">원인</h3>
<p>AMCL은 파티클 필터 기반이라, <code>/initialpose</code>로 위치를 덮어쓰면 파티클이 해당 위치 주변으로 재분포된다. x, y까지 같이 바꾸면 파티클이 현재 위치에서 완전히 다른 위치로 순간이동하는 셈이다. LiDAR 기반으로 x, y는 이미 어느 정도 맞게 잡혀있는 상황에서 굳이 건드릴 필요가 없었다.</p>
<h3 id="해결-1">해결</h3>
<p>x, y는 <code>/amcl_pose</code> 토픽에서 현재값을 그대로 가져오고, <strong>yaw만 교정값으로 교체</strong>하는 방식으로 변경했다.</p>
<pre><code class="language-python"># 수정 전: x, y, yaw 전부 덮어쓰기
initial_pose.pose.pose.position.x = tag_x
initial_pose.pose.pose.position.y = tag_y
initial_pose.pose.pose.orientation = corrected_yaw_quat

# 수정 후: x, y는 현재 AMCL값 유지, yaw만 교정
current_pose = # /amcl_pose 토픽에서 가져옴
initial_pose.pose.pose.position.x = current_pose.pose.pose.position.x
initial_pose.pose.pose.position.y = current_pose.pose.pose.position.y
initial_pose.pose.pose.orientation = corrected_yaw_quat</code></pre>
<h2 id="5-삽질-3--검증-기준-자체가-amcl을-흔들었다">5. 삽질 #3 — 검증 기준 자체가 AMCL을 흔들었다</h2>
<h3 id="문제-2">문제</h3>
<p>교정이 제대로 되는지 확인하려고 RViz의 2D Pose Estimate 버튼으로 로봇의 실제 위치를 찍어서 교정 전후 yaw를 비교했다.</p>
<p><strong>시도:</strong> 2D Pose Estimate로 실제 위치 찍어서 비교<br><strong>결과:</strong> 비교할 때마다 결과가 달라짐. 교정이 잘 된 건지 아닌 건지 판단 불가</p>
<h3 id="원인-2">원인</h3>
<p><strong>2D Pose Estimate 자체가 <code>/initialpose</code>를 발행한다.</strong> 검증을 위해 위치를 찍는 행위가 AMCL의 파티클을 흩트려서, 비교 기준이 되는 AMCL pose 자체를 바꿔버리고 있었다. 측정 도구가 피측정 대상에 영향을 주는 상황이었다.</p>
<h3 id="해결-2">해결</h3>
<p>두 가지로 바꿨다.</p>
<p>첫째, 비교 기준을 <code>/amcl_pose</code> 토픽 직접 구독으로 변경했다. 토픽을 읽는 건 AMCL에 아무 영향을 주지 않는다.</p>
<p>둘째, ROS2 없이 단순 파이썬 스크립트로 yaw 값만 빠르게 검증하는 방식을 추가했다. 시스템 전체를 돌리지 않아도 계산 로직이 맞는지 빠르게 확인할 수 있어서 디버깅 속도가 훨씬 빨라졌다.</p>
<h2 id="6-트러블슈팅-정리">6. 트러블슈팅 정리</h2>
<h3 id="문제-1-yaw-교정값이-계속-180도-가까이-틀어져-나온다">문제 1: yaw 교정값이 계속 180도 가까이 틀어져 나온다</h3>
<p><strong>원인:</strong> TAG_INFO yaw를 로봇 시점으로 정의함<br><strong>해결:</strong> TAG_YAW=0 기준으로 실측값과 비교해 오프셋 역산</p>
<h3 id="문제-2-교정-후-로봇이-순간이동한다">문제 2: 교정 후 로봇이 순간이동한다</h3>
<p><strong>원인:</strong> <code>/initialpose</code>에 x, y, yaw를 전부 덮어씀<br><strong>해결:</strong> x, y는 <code>/amcl_pose</code>에서 현재값 그대로 사용, yaw만 교정값으로 교체</p>
<h3 id="문제-3-검증할-때마다-결과가-달라진다">문제 3: 검증할 때마다 결과가 달라진다</h3>
<p><strong>원인:</strong> 2D Pose Estimate가 내부적으로 <code>/initialpose</code>를 발행해 AMCL을 흔듦<br><strong>해결:</strong> <code>/amcl_pose</code> 토픽 직접 구독으로 비교, ROS2 없는 독립 파이썬 스크립트로 계산 로직 단위 검증</p>
<h2 id="7-배운-점">7. 배운 점</h2>
<h3 id="1-방향-정의는-누구의-시점인가를-먼저-확인하자">1. 방향 정의는 &quot;누구의 시점인가&quot;를 먼저 확인하자</h3>
<p>태그 yaw처럼 방향이 개입되는 값은 항상 기준 시점을 명확히 해야 한다. 머릿속에서 맞춰보려 하지 말고, 실측값 기준으로 역산하는 게 훨씬 안전하다.</p>
<h3 id="2-교정-범위는-필요한-것만">2. 교정 범위는 필요한 것만</h3>
<p>AMCL처럼 파티클 필터 기반 시스템은 값을 크게 바꿀수록 불안정해진다. x, y가 이미 맞다면 yaw만 건드리는 게 맞다. 욕심부리다 오히려 더 망가진다.</p>
<h3 id="3-측정-도구가-피측정-대상에-영향을-주는지-확인하자">3. 측정 도구가 피측정 대상에 영향을 주는지 확인하자</h3>
<p>2D Pose Estimate로 검증한다는 게 사실 AMCL을 계속 흔들고 있었다... 디버깅할 때 내가 쓰는 도구 자체가 시스템에 영향을 주는지 항상 의심해봐야 한다.</p>
<h3 id="4-단위-검증을-먼저">4. 단위 검증을 먼저</h3>
<p>ROS2 전체 시스템을 띄우지 않아도 계산 로직만 따로 파이썬 스크립트로 검증할 수 있었다. 처음부터 이렇게 했으면 훨씬 빨리 끝났을 것이다.</p>
<h2 id="8-마무리">8. 마무리</h2>
<p>탑뷰 카메라가 있음에도 AprilTag 셀프 교정을 시도한 건 &quot;더 어렵고 재미있어 보여서&quot;였다. 결과적으로는 세 가지 삽질을 거쳐 yaw만 교정하는 깔끔한 구조가 완성됐다.</p>
<p>돌아보면 삽질의 공통점이 있었다. <strong>&quot;이렇겠지&quot;라고 가정하고 전체를 다 구현한 뒤 테스트했다</strong>는 점이다. 가장 단순한 케이스부터 하나씩 확인했으면 훨씬 빨리 끝났을 것이다. 로봇 소프트웨어는 가정이 틀렸을 때 증상이 엉뚱한 곳에서 나타나는 경우가 많다. 작은 단위로 쪼개서 검증하는 습관이 특히 중요하다는 걸 다시 한번 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ROS2 Python 패키지에서 Custom Message 빌드 관련 트러블 슈팅]]></title>
            <link>https://velog.io/@hyoin_0219/ROS2-Python-%ED%8C%A8%ED%82%A4%EC%A7%80%EC%97%90%EC%84%9C-Custom-Message-%EB%B9%8C%EB%93%9C-%EA%B4%80%EB%A0%A8-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@hyoin_0219/ROS2-Python-%ED%8C%A8%ED%82%A4%EC%A7%80%EC%97%90%EC%84%9C-Custom-Message-%EB%B9%8C%EB%93%9C-%EA%B4%80%EB%A0%A8-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Sun, 15 Feb 2026 18:11:32 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가며">1. 들어가며</h2>
<p>쇼핑몰 로봇 프로젝트를 진행하면서 로봇과 서버 간 통신을 위한 custom message가 필요했습니다. &quot;Python 패키지니까 msg 파일만 만들면 되겠지?&quot;라고 생각했지만... 현실은 빌드 에러의 연속이었습니다.</p>
<h2 id="2-문제-상황-빌드-에러의-연속">2. 문제 상황: 빌드 에러의 연속</h2>
<h3 id="첫-번째-에러-could-not-find-package-rosidl_default_generators">첫 번째 에러: Could not find package &quot;rosidl_default_generators&quot;</h3>
<pre><code class="language-bash">CMake Error: Could not find package configuration file provided by &quot;rosidl_default_generators&quot;</code></pre>
<p><strong>시도:</strong> msg 파일만 만들고 빌드<br><strong>결과:</strong> CMake가 메시지 생성 도구를 찾지 못함</p>
<h3 id="두-번째-에러-rosidl_generate_interfaces-정의되지-않음">두 번째 에러: rosidl_generate_interfaces 정의되지 않음</h3>
<pre><code class="language-bash"># package.xml에 의존성 추가 후 재빌드
CMakeLists.txt doesn&#39;t define rosidl_generate_interfaces</code></pre>
<p><strong>시도:</strong> package.xml만 수정<br><strong>결과:</strong> CMakeLists.txt 설정 부족</p>
<h3 id="세-번째-에러-no-module-named-mall_e_controllermsg">세 번째 에러: No module named &#39;mall_e_controller.msg&#39;</h3>
<pre><code class="language-bash">source install/setup.bash
python3 test_publisher.py

# 에러 메시지
ModuleNotFoundError: No module named &#39;mall_e_controller.msg&#39;</code></pre>
<p><strong>시도:</strong> 빌드는 성공했지만 Python에서 import 실패<br><strong>원인:</strong> <code>&lt;member_of_group&gt;</code> 태그 누락</p>
<h2 id="3-원인-분석-python-패키지-≠-cmakeliststxt-불필요">3. 원인 분석: Python 패키지 ≠ CMakeLists.txt 불필요</h2>
<h3 id="핵심-깨달음">핵심 깨달음</h3>
<ul>
<li><strong>ROS2에서 msg/srv/action은 언어 중립적</strong></li>
<li>Python 패키지여도 <strong>메시지 타입 생성은 빌드 시스템 필요</strong></li>
<li>rclpy는 생성된 메시지를 사용하는 것일 뿐, 메시지를 만들어 주진 않음</li>
</ul>
<h3 id="구조-이해">구조 이해</h3>
<pre><code>.msg 파일 (정의)
    ↓
rosidl (빌드 도구) ← CMake 기반
    ↓
C++/Python 코드 자동 생성
    ↓
패키지에서 import해서 사용</code></pre><h3 id="python-vs-c-패키지-차이">Python vs C++ 패키지 차이</h3>
<ul>
<li>Python 코드: setup.py로 관리</li>
<li>메시지 타입: CMakeLists.txt로 관리</li>
</ul>
<h2 id="4-해결-과정-세-가지-필수-설정--올바른-빌드">4. 해결 과정: 세 가지 필수 설정 + 올바른 빌드</h2>
<h3 id="4-1-msg-파일-작성">4-1. msg 파일 작성</h3>
<pre><code class="language-msg"># mall_e_controller/msg/RobotMessage.msg

# Header
string message_id
int32 timestamp_sec
int32 timestamp_nsec
string robot_id
string message_type
int32 priority

# Body
string robot_status
float32 battery_pct</code></pre>
<h3 id="4-2-cmakeliststxt-설정">4-2. CMakeLists.txt 설정</h3>
<pre><code class="language-cmake">cmake_minimum_required(VERSION 3.8)
project(mall_e_controller)

# 컴파일러 설정
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES &quot;Clang&quot;)
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 필수 패키지 찾기
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED) # Python 지원을 위해
find_package(rclpy REQUIRED)

# 메시지 생성 도구
find_package(rosidl_default_generators REQUIRED)

# 메시지 파일 등록 - 이게 빠지면 메시지 생성 안 됨!
rosidl_generate_interfaces(${PROJECT_NAME}
  &quot;msg/RobotMessage.msg&quot; # 패키지를 루트로 삼아야 함.
)

# Python 패키지 설정
ament_python_install_package(${PROJECT_NAME})

ament_package()</code></pre>
<p><strong>내가 했던 실수:</strong></p>
<ul>
<li><code>rosidl_generate_interfaces()</code> 빠뜨림: msg 파일을 C++/Python 코드로 변환하는 작업이 불가능</li>
<li>메시지 경로 실수: 패키지를 루트로 삼아야 한다.</li>
<li><code>find_package(ament_cmake_python REQUIRED)</code> 누락: python 지원이 안 된다.</li>
</ul>
<h3 id="4-3-packagexml-설정">4-3. package.xml 설정</h3>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;package format=&quot;3&quot;&gt;
  &lt;name&gt;mall_e_controller&lt;/name&gt;
  &lt;version&gt;0.0.1&lt;/version&gt;
  &lt;description&gt;Mall robot controller&lt;/description&gt;
  &lt;maintainer email=&quot;you@example.com&quot;&gt;Your Name&lt;/maintainer&gt;
  &lt;license&gt;Apache License 2.0&lt;/license&gt;

  &lt;!-- 빌드 도구 --&gt;
  &lt;buildtool_depend&gt;ament_cmake&lt;/buildtool_depend&gt;
  &lt;buildtool_depend&gt;ament_cmake_python&lt;/buildtool_depend&gt;

  &lt;!-- 메시지 생성 의존성 - 이거 없으면 빌드 실패! --&gt;
  &lt;build_depend&gt;rosidl_default_generators&lt;/build_depend&gt;
  &lt;exec_depend&gt;rosidl_default_runtime&lt;/exec_depend&gt;

  &lt;!-- 이게 없으면 다른 패키지에서 메시지 못 찾음! --&gt;
  &lt;member_of_group&gt;rosidl_interface_packages&lt;/member_of_group&gt;

  &lt;!-- Python 의존성 --&gt;
  &lt;depend&gt;rclpy&lt;/depend&gt;

  &lt;export&gt;
    &lt;build_type&gt;ament_cmake&lt;/build_type&gt;
  &lt;/export&gt;
&lt;/package&gt;</code></pre>
<p><strong>내가 했던 실수:</strong></p>
<ul>
<li><code>&lt;member_of_group&gt;rosidl_interface_packages&lt;/member_of_group&gt;</code> 빠뜨림: 다른 패키지에서 msg를 못 찾음.</li>
<li>build_depend (컴파일할 때만 필요) 와 exec_depend(실행할 때만 필요) 구분 안 함</li>
<li>ament_cmake_python 누락: python 패키지인데 커스텀 메시지를 만들 때 필요</li>
</ul>
<h2 id="5-트러블슈팅-정리">5. 트러블슈팅 정리</h2>
<h3 id="문제-1-no-module-named-package_namemsg">문제 1: &quot;No module named &#39;{package_name}.msg&#39;&quot;</h3>
<p><strong>원인:</strong> 환경 변수 미로드 또는 빌드 실패</p>
<p><strong>해결:</strong>
환경변수를 다시 빌드해 봅시다.</p>
<pre><code class="language-bash"># 환경 변수 다시 로드
source install/setup.bash

# 전체 재빌드
rm -rf build/ install/ log/
colcon build --packages-select mall_e_controller
source install/setup.bash</code></pre>
<h3 id="문제-2-package-package_name-not-found">문제 2: &quot;package &#39;{package_name}&#39; not found&quot;</h3>
<p><strong>원인:</strong> package.xml에 <code>&lt;member_of_group&gt;</code> 누락</p>
<p><strong>해결:</strong></p>
<pre><code class="language-xml">&lt;!-- package.xml에 추가 --&gt;
&lt;member_of_group&gt;rosidl_interface_packages&lt;/member_of_group&gt;</code></pre>
<h3 id="문제-3-cmake-error-could-not-find-rosidl_default_generators">문제 3: &quot;CMake Error: Could not find rosidl_default_generators&quot;</h3>
<p><strong>원인:</strong> package.xml 의존성 누락</p>
<p><strong>해결:</strong></p>
<pre><code class="language-xml">&lt;!-- package.xml에 추가 --&gt;
&lt;build_depend&gt;rosidl_default_generators&lt;/build_depend&gt;
&lt;exec_depend&gt;rosidl_default_runtime&lt;/exec_depend&gt;</code></pre>
<h3 id="문제-4-cmakeliststxt-doesnt-define-rosidl_generate_interfaces">문제 4: &quot;CMakeLists.txt doesn&#39;t define rosidl_generate_interfaces&quot;</h3>
<p><strong>원인:</strong> CMakeLists.txt에 메시지 생성 명령 누락</p>
<p><strong>해결:</strong></p>
<pre><code class="language-cmake"># CMakeLists.txt에 추가
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  &quot;msg/RobotMessage.msg&quot;
)</code></pre>
<h3 id="문제-5-설정-고쳤는데도-계속-같은-에러">문제 5: 설정 고쳤는데도 계속 같은 에러</h3>
<p><strong>원인:</strong> 빌드 캐시 문제</p>
<p><strong>해결:</strong></p>
<pre><code class="language-bash">cd ~/robotics_ws
rm -rf build/ install/ log/
colcon build --packages-select mall_e_controller
source install/setup.bash</code></pre>
<p>사실 이 빌드 캐시 문제가 많이 발생했습니다 하하... 프로젝트 기본설정할 때 클린빌드는 꼭 해야 하는 것 같습니다.</p>
<h2 id="6-배운-점">6. 배운 점</h2>
<h3 id="1-python-패키지-≠-cmakeliststxt-불필요">1. Python 패키지 ≠ CMakeLists.txt 불필요</h3>
<p>메시지 타입은 언어 중립적이라 빌드 시스템 필수. Python 코드만 있어도 메시지 생성엔 CMake 필요합니다.</p>
<h3 id="2-세-가지는-세트로-관리">2. 세 가지는 세트로 관리</h3>
<p>msg 파일 + CMakeLists.txt + package.xml 모두 제대로 설정해야 합니다. 하나라도 빠지면 빌드 실패 또는 import 실패합니다.</p>
<h3 id="3-빌드-위치와-순서-중요">3. 빌드 위치와 순서 중요</h3>
<p>워크스페이스 루트에서 빌드해야 합니다. src 내부에서 빌드하면 충돌이 발생합니다. 설정 변경 후엔 반드시 재빌드 + source 해야 합니다.</p>
<h3 id="4-빌드-캐시-주의">4. 빌드 캐시 주의</h3>
<p>설정 변경 후 안 되면 캐시 삭제가 해결사입니다. <code>rm -rf build/ install/ log/</code></p>
<h3 id="5-packagexml의-member_of_group이-핵심">5. package.xml의 member_of_group이 핵심</h3>
<p>이게 없으면 다른 패키지에서 메시지를 못 찾습니다. 빌드는 성공해도 import 실패하는 주범입니다.</p>
<h2 id="10-마무리">10. 마무리</h2>
<p>ROS2에서 Python으로 개발한다고 해서 빌드 시스템을 무시할 수는 없습니다. 특히 custom message를 만들 때는:</p>
<ul>
<li><strong>msg 파일 작성</strong> (인터페이스 정의)</li>
<li><strong>CMakeLists.txt 설정</strong> (메시지 생성)</li>
<li><strong>package.xml 설정</strong> (의존성 + 그룹 등록)</li>
<li><strong>올바른 위치에서 빌드</strong> (워크스페이스 루트)</li>
<li><strong>환경 변수 로드</strong> (source install/setup.bash)</li>
</ul>
<p>이 5가지를 모두 챙겨야 성공합니다.</p>
<p>처음엔 &quot;Python인데 왜 CMake가 필요해?&quot;라고 생각했지만, ROS2의 메시지 시스템이 언어 중립적으로 설계되어 있다는 걸 이해하고 나니 구조가 명확해졌습니다.</p>
<p>같은 문제로 고생하고 계신 분들께 도움이 되길 바랍니다!</p>
<hr>
<p><strong>참고 자료:</strong></p>
<ul>
<li><a href="https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Custom-ROS2-Interfaces.html">ROS2 공식 문서 - Creating custom msg and srv files</a></li>
<li><a href="https://docs.ros.org/en/jazzy/How-To-Guides/Ament-CMake-Python-Documentation.html">ament_cmake_python 사용법</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ROS2 기반 멀티 컴포넌트 시스템 성능 측정]]></title>
            <link>https://velog.io/@hyoin_0219/ROS2-%EA%B8%B0%EB%B0%98-%EB%A9%80%ED%8B%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95</link>
            <guid>https://velog.io/@hyoin_0219/ROS2-%EA%B8%B0%EB%B0%98-%EB%A9%80%ED%8B%B0-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95</guid>
            <pubDate>Sun, 08 Feb 2026 13:45:38 GMT</pubDate>
            <description><![CDATA[<h2 id="1-성능-측정-배경">1. 성능 측정 배경</h2>
<h3 id="11-성능-측정의-이유">1.1 성능 측정의 이유</h3>
<p><strong>초기 우려사항:</strong></p>
<p>이 시스템은 여러 컴포넌트가 HTTP로 연결된 <strong>분산 아키텍처</strong>입니다:</p>
<pre><code>Robot → (ROS2) → malle_service → (HTTP) → AI Service → (HTTP) → Web Service</code></pre><p><strong>HTTP 통신의 잠재적 문제:</strong></p>
<ul>
<li>TCP 기반: 3-way handshake, 연결 설정 오버헤드</li>
<li>요청-응답 모델: 동기적 대기 시간</li>
<li>다중 홉: 여러 서비스를 거치며 지연 누적</li>
</ul>
<p><strong>대안으로 고려했던 것:</strong></p>
<ul>
<li>UDP 통신: 연결 설정 없이 빠른 전송</li>
<li>gRPC: HTTP/2 기반 고성능 RPC</li>
</ul>
<h3 id="12-핵심-질문">1.2 핵심 질문</h3>
<blockquote>
<p><strong>&quot;HTTP 통신이 병목이 될까? UDP를 필수적으로 적용해야 할까?&quot;</strong></p>
</blockquote>
<p>이를 확인하기 위해 성능 측정을 진행했습니다.</p>
<h3 id="13-측정-목표">1.3 측정 목표</h3>
<ol>
<li><p><strong>전체 파이프라인 지연 시간</strong> 측정</p>
<ul>
<li>로봇 메시지 발행 → 웹 UI 표시까지 총 시간</li>
</ul>
</li>
<li><p><strong>구간별 병목 지점</strong> 파악</p>
<ul>
<li>AI 처리 vs HTTP 통신 vs 오버헤드</li>
</ul>
</li>
<li><p>궁극적으론, <strong>UDP 적용 필요성</strong> 판단</p>
<ul>
<li>HTTP 통신 오버헤드가 허용 가능한 수준인가?</li>
<li>실시간성이 보장되는가?</li>
</ul>
</li>
</ol>
<h3 id="14-허용-가능한-지연-시간">1.4 허용 가능한 지연 시간</h3>
<p><strong>요구사항:</strong></p>
<ul>
<li>로봇 센서 데이터: 1초에 1회 발행</li>
<li>AI 처리 시간: ~1초 (모델 추론)</li>
<li><strong>목표 전체 지연</strong>: 2초 이내</li>
<li><strong>허용 통신 오버헤드</strong>: 100ms 이내</li>
</ul>
<p><strong>가설:</strong>
아래 2개 상황일 경우, UDP 전환 없이도 충분히 실시간성을 확보할 수 있음:</p>
<ul>
<li>AI 처리가 주요 병목일 것 (1초)</li>
<li>HTTP 통신 오버헤드는 수십 ms 수준일 것</li>
</ul>
<p><strong>검증 계획:</strong></p>
<p>1분간 메시지를 연속 전송하며 다음 요소를 측정:</p>
<ul>
<li>평균 지연 시간</li>
<li>최대 지연 시간 (P99)</li>
<li>안정성 (표준편차)</li>
<li>각 구간별 비율</li>
</ul>
<p>결과를 바탕으로 <strong>UDP 전환 필요 여부를 결정</strong>하고자 했습니다.</p>
<hr>
<h2 id="2-시스템-아키텍처">2. 시스템 아키텍처</h2>
<h3 id="21-개요">2.1 개요</h3>
<p>이번에 진행한 Malle 프로젝트는 <strong>다중 로봇 제어 시스템</strong>으로, 여러 대의 자율주행 로봇을 동시에 관리하고 모니터링합니다. 각 로봇의 센서 데이터를 수집하여 AI로 분석하고, 그 결과를 웹 대시보드에서 실시간으로 확인할 수 있습니다.</p>
<p><strong>프로젝트 배경:</strong></p>
<ul>
<li>복수의 로봇이 동일한 작업 공간에서 협업</li>
<li>각 로봇을 개별적으로 식별하고 제어해야 함</li>
<li>중앙 서버에서 모든 로봇의 상태를 통합 관리</li>
<li>관리자가 웹 UI에서 실시간 모니터링</li>
</ul>
<h3 id="22-통신-흐름">2.2 통신 흐름</h3>
<p>통신 흐름은 Sequence Diagram으로 미리 설계했습니다.
<img src="blob:https://velog.io/5c64ef24-679b-412d-a535-c44c43edbaae" alt="업로드중.."></p>
<h3 id="23-컴포넌트별-역할">2.3 컴포넌트별 역할</h3>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>기술 스택</th>
<th>포트</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Robot Publisher</strong> (다이어그램 내 bot)</td>
<td>ROS2 Humble</td>
<td>-</td>
<td>센서 데이터 발행 (다중 로봇)</td>
</tr>
<tr>
<td><strong>malle_service</strong> (다이어그램 내 malle server)</td>
<td>ROS2 + FastAPI</td>
<td>8000</td>
<td>ROS Subscriber + 중앙 조정</td>
</tr>
<tr>
<td><strong>malle_ai_service</strong> (다이어그램 내 malle AI server)</td>
<td>Flask</td>
<td>5000</td>
<td>AI 데이터 분석</td>
</tr>
<tr>
<td><strong>malle_web_service</strong> (다이어그램 내 malle web server)</td>
<td>FastAPI</td>
<td>8001</td>
<td>WebSocket 브로드캐스트</td>
</tr>
<tr>
<td><strong>Web UI</strong> (다이어그램 내 malle UI server)</td>
<td>HTML/JS</td>
<td>3000</td>
<td>실시간 모니터링</td>
</tr>
</tbody></table>
<h3 id="24-메시지-포맷">2.4 메시지 포맷</h3>
<p><strong>설계 배경:</strong></p>
<p>다중 로봇 시스템에서는 각 메시지의 <strong>출처를 명확히 식별</strong>하는 것이 필수입니다. 단순히 센서 데이터만 보내면:</p>
<ul>
<li>어떤 로봇에서 온 데이터인지 알 수 없음</li>
<li>메시지 순서 보장 불가 (네트워크 지연 시)</li>
<li>중복 메시지 감지 불가</li>
<li>디버깅 및 추적 곤란</li>
</ul>
<p>따라서 <strong>풍부한 메타데이터를 포함한 Header</strong> 구조를 설계했습니다:</p>
<pre><code class="language-python"># RobotMessage (ROS2 Custom Message)
Header header
  string message_id        # UUID - 메시지 고유 식별자 (중복 감지용)
  int64 timestamp_sec      # Unix timestamp (초) - 발행 시간
  int64 timestamp_nsec     # 나노초 - 고정밀 타임스탬프
  string robot_id          # 로봇 식별자 (예: &quot;robot_001&quot;, &quot;robot_002&quot;)
  string message_type      # 메시지 타입 (예: &quot;status&quot;, &quot;alert&quot;, &quot;command&quot;)
  int32 priority           # 우선순위 (긴급 메시지 처리용)
  int32 sequence           # 시퀀스 번호 (메시지 순서 보장)

# Body (실제 센서 데이터)
float64 battery            # 배터리 잔량 (%)
string robot_status        # 상태 (&quot;running&quot;, &quot;idle&quot;, &quot;error&quot;)
string command             # 제어 명령
string error_message       # 에러 메시지</code></pre>
<p><strong>각 필드의 역할:</strong></p>
<table>
<thead>
<tr>
<th>필드</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>message_id</code></td>
<td>메시지 중복 제거, 추적</td>
<td><code>&quot;7f939-3a0a-4742-91cc&quot;</code></td>
</tr>
<tr>
<td><code>robot_id</code></td>
<td>로봇 식별</td>
<td><code>&quot;robot_001&quot;</code>, <code>&quot;robot_A&quot;</code></td>
</tr>
<tr>
<td><code>timestamp_*</code></td>
<td>메시지 발행 시간 (성능 측정)</td>
<td><code>1770365590.123456789</code></td>
</tr>
<tr>
<td><code>message_type</code></td>
<td>메시지 분류</td>
<td><code>&quot;status&quot;</code>, <code>&quot;alert&quot;</code></td>
</tr>
<tr>
<td><code>priority</code></td>
<td>긴급도 (높을수록 우선 처리)</td>
<td><code>1</code> (일반), <code>5</code> (긴급)</td>
</tr>
<tr>
<td><code>sequence</code></td>
<td>순서 보장 (패킷 손실 감지)</td>
<td><code>0, 1, 2, 3...</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="3-성능-측정-관련-트러블-슈팅">3. 성능 측정 관련 트러블 슈팅</h2>
<h3 id="31-분산-시스템의-시간-동기화">3.1 분산 시스템의 시간 동기화</h3>
<p><strong>문제 상황:</strong></p>
<pre><code class="language-python"># 로봇에서 타임스탬프 설정
publish_time = time.time()
msg.header.timestamp_sec = int(publish_time)

# malle_service에서 수신
receive_time = datetime.now()
ros_latency = (receive_time - publish_time).total_seconds()
# 결과: 1.684초 (비정상적으로 큼)</code></pre>
<p><strong>원인:</strong></p>
<ul>
<li><code>time.time()</code>과 <code>datetime.now()</code>의 미묘한 차이가 있었음.</li>
<li>메시지 타임스탬프가 과거 시점으로 잘못 설정될 가능성 존재.</li>
<li>서로 다른 기준점(발행 시간 vs 수신 시간)으로 측정됨.</li>
</ul>
<p><strong>해결:</strong></p>
<pre><code class="language-python"># 수신 시간을 기준점으로 통일
def listener_callback(self, msg):
    start_time = datetime.now()
    asyncio.run(self.process_message(msg, start_time))</code></pre>
<hr>
<h2 id="4-qos-최적화">4. QoS 최적화</h2>
<h3 id="41-ros2-qos-개념">4.1 ROS2 QoS 개념</h3>
<p>ROS2는 DDS 기반으로, QoS 설정을 통해 통신 특성을 조정할 수 있습니다.</p>
<p><strong>주요 QoS 파라미터:</strong></p>
<table>
<thead>
<tr>
<th>파라미터</th>
<th>옵션</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Reliability</strong></td>
<td>RELIABLE</td>
<td>모든 메시지 보장 (느림)</td>
</tr>
<tr>
<td></td>
<td>BEST_EFFORT</td>
<td>최선 노력, 손실 가능 (빠름)</td>
</tr>
<tr>
<td><strong>History</strong></td>
<td>KEEP_ALL</td>
<td>모든 메시지 보관</td>
</tr>
<tr>
<td></td>
<td>KEEP_LAST</td>
<td>최신 N개만 보관</td>
</tr>
<tr>
<td><strong>Depth</strong></td>
<td>1~N</td>
<td>보관할 메시지 개수</td>
</tr>
<tr>
<td><strong>Durability</strong></td>
<td>TRANSIENT_LOCAL</td>
<td>구독자 연결 전 메시지 보관</td>
</tr>
<tr>
<td></td>
<td>VOLATILE</td>
<td>현재 구독자에게만 전송</td>
</tr>
</tbody></table>
<h3 id="42-reliable-vs-best_effort">4.2 RELIABLE vs BEST_EFFORT</h3>
<p><strong>RELIABLE (신뢰성 우선):</strong></p>
<pre><code class="language-python">qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.RELIABLE,
    history=HistoryPolicy.KEEP_LAST,
    depth=10
)</code></pre>
<p>특징:</p>
<ul>
<li>메시지 손실 없음</li>
<li>재전송 오버헤드로 느림</li>
<li>사용 사례: 중요한 제어 명령</li>
</ul>
<p><strong>BEST_EFFORT (속도 우선):</strong></p>
<pre><code class="language-python">qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=1
)</code></pre>
<p>특징:</p>
<ul>
<li>낮은 지연시간</li>
<li>네트워크 혼잡 시 메시지 손실 가능</li>
<li>사용 사례: 센서 데이터, 모니터링</li>
</ul>
<h3 id="43-적용-결과">4.3 적용 결과</h3>
<p><strong>Publisher (test_publisher.py):</strong></p>
<pre><code class="language-python">from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy

qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=1,
    durability=DurabilityPolicy.VOLATILE
)

self.publisher_ = self.create_publisher(
    RobotMessage,
    &#39;robot_test_topic&#39;,
    qos_profile
)</code></pre>
<p><strong>Subscriber (malle_service):</strong></p>
<pre><code class="language-python"># Publisher와 동일한 QoS 설정 필요
qos_profile = QoSProfile(
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST,
    depth=1,
    durability=DurabilityPolicy.VOLATILE
)

self.subscription = self.create_subscription(
    RobotMessage,
    &#39;robot_test_topic&#39;,
    self.listener_callback,
    qos_profile
)</code></pre>
<p><strong>효과:</strong></p>
<ul>
<li>안정적인 latency</li>
<li>표준편차 감소 (=변동성이 낮아짐)</li>
</ul>
<hr>
<h2 id="5-결과-및-분석">5. 결과 및 분석</h2>
<h3 id="51-최종-성능-지표">5.1 최종 성능 지표</h3>
<p><strong>1분간 측정 (58개 메시지):</strong></p>
<pre><code>============================================================
📊 통계 분석 결과 (수신 시간 기준)
============================================================
총 메시지 수: 58개
수집 기간: 60초
초당 평균 메시지: 0.97개/초

------------------------------------------------------------
단계                 평균        최소        최대      중앙값
------------------------------------------------------------
AI 처리             1.006초     1.003초     1.007초     1.006초
웹 전송             0.003초     0.003초     0.003초     0.003초
오버헤드            0.028초     0.022초     0.067초     0.025초
전체                1.037초     1.029초     1.077초     1.034초
------------------------------------------------------------

📈 백분위수 분석 (전체 처리 시간)
------------------------------------------------------------
P50: 1.034초 (50%의 요청이 이 시간 이내 처리)
P75: 1.037초 (75%의 요청이 이 시간 이내 처리)
P90: 1.051초 (90%의 요청이 이 시간 이내 처리)
P95: 1.057초 (95%의 요청이 이 시간 이내 처리)
P99: 1.077초 (99%의 요청이 이 시간 이내 처리)

📊 각 단계별 시간 비율
------------------------------------------------------------
AI 처리:       97.00% (1.000초)
웹 전송:        0.29% (0.003초)
오버헤드:       2.71% (0.028초)
총합:         100.00% (1.037초)

✓ 합계 검증 통과 (차이: 0.000000초)

📉 표준편차 (안정성 지표)
------------------------------------------------------------
AI 처리           0.001초
웹 전송            0.000초
오버헤드            0.008초
전체              0.009초
============================================================</code></pre><h3 id="52-병목-분석">5.2 병목 분석</h3>
<p><strong>병목 순위:</strong></p>
<ol>
<li><p><strong>AI 처리: 97.00%</strong> (1.000초)</p>
<ul>
<li>의도된 병목 (AI 모델 추론 시간)</li>
</ul>
</li>
<li><p><strong>오버헤드: 2.71%</strong> (0.028초)</p>
<ul>
<li>Python asyncio 오버헤드</li>
<li>HTTP 클라이언트 생성/소멸</li>
<li>개선 방법: 연결 풀링, 코드 최적화</li>
</ul>
</li>
<li><p><strong>웹 전송: 0.29%</strong> (0.003초)</p>
<ul>
<li>로컬 네트워크 (localhost) 사용 중</li>
<li>이미 최적화됨</li>
</ul>
</li>
</ol>
<p><strong>결론:</strong> AI 처리 외 통신 오버헤드는 <strong>3% 미만</strong>으로 매우 효율적입니다.</p>
<h3 id="53-http-vs-udp-판단">5.3 HTTP vs UDP 판단</h3>
<p><strong>측정 결과 기반 의사결정:</strong></p>
<pre><code>전체 통신 오버헤드 (웹 전송 + 오버헤드): 
  0.003초 + 0.028초 = 0.031초 (31ms)</code></pre><p><strong>판단:</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>측정값</th>
<th>목표값</th>
<th>평가</th>
</tr>
</thead>
<tbody><tr>
<td><strong>총 지연 시간</strong></td>
<td>1.037초</td>
<td>&lt; 2초</td>
<td>✅ 통과</td>
</tr>
<tr>
<td><strong>통신 오버헤드</strong></td>
<td>31ms</td>
<td>&lt; 100ms</td>
<td>✅ 통과</td>
</tr>
<tr>
<td><strong>안정성 (σ)</strong></td>
<td>0.009초</td>
<td>-</td>
<td>✅ 매우 안정적</td>
</tr>
<tr>
<td><strong>P99 지연</strong></td>
<td>1.077초</td>
<td>&lt; 2초</td>
<td>✅ 통과</td>
</tr>
</tbody></table>
<p><strong>결론:</strong></p>
<blockquote>
<p><strong>HTTP 통신으로 충분하다. UDP 전환은 불필요.</strong></p>
</blockquote>
<p>나중에 리팩토링해도 되겠지만, 현재의 스펙에선 굳이 UDP로 바꾸지 않아도 실시간성이 유지된다.</p>
<p><strong>의사결정:</strong></p>
<p>현재 시스템에선 <strong>HTTP로 충분히 실시간성을 만족</strong>하므로, 불필요한 복잡도를 추가하지 않기로 결정했습니다.</p>
<h2 id="📚-참고-자료">📚 참고 자료</h2>
<ul>
<li><a href="https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Quality-of-Service-Settings.html">ROS2 QoS 공식 문서</a></li>
<li><a href="https://www.omg.org/spec/DDS/">DDS 표준 스펙</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Micro-ROS] ESP32 펌웨어 빌드와 Agent 연결]]></title>
            <link>https://velog.io/@hyoin_0219/Micro-ROS-ESP32-%ED%8E%8C%EC%9B%A8%EC%96%B4-%EB%B9%8C%EB%93%9C%EC%99%80-Agent-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@hyoin_0219/Micro-ROS-ESP32-%ED%8E%8C%EC%9B%A8%EC%96%B4-%EB%B9%8C%EB%93%9C%EC%99%80-Agent-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Sun, 25 Jan 2026 12:08:53 GMT</pubDate>
            <description><![CDATA[<h1 id="micro-ros의-필요성">Micro ROS의 필요성</h1>
<p>micro ros는 ROS2의 개념을 MCU 버전에 맞게 줄여서 넣은 버전이라고 생각하면 됩니다.
<strong>ROS2 vs Micro-ROS:</strong></p>
<ul>
<li>ROS2: 컴퓨터 등 비교적 리소스 넉넉한 시스템용</li>
<li>micro-ros: 수십~수백 KB RAM 등 MCU같은 리소스 제한 환경용
센서보드/모터보드 같은 MCU를 ROS2에 편입시키고 싶을 때 주로 사용합니다.
저는 실습 때 ESP32 보드를 사용했습니다.</li>
</ul>
<h2 id="아키텍처">아키텍처</h2>
<h3 id="큰-흐름">큰 흐름</h3>
<p>ROS2랑 비슷한 기능을 수행하면서 어떻게 리소스를 줄였을까요? 일단 micro ros가 들어가는 MCU는 ROS2가 돌아갈 만큼의 리소스를 가진 컴퓨터가 필요합니다.
<img src="https://velog.velcdn.com/images/hyoin_0219/post/59ccc445-c910-4c0d-bb2d-8dd7ad3e3c5e/image.png" alt=""></p>
<p>MCU 쪽엔 micro-ROS client, PC 쪽에 micro-ROS Agent가 있고, Agent가 ROS2 DDS로 브릿지 역할을 해 MCU 노드를 ROS2 시스템에 붙여줍니다.</p>
<ul>
<li>Agent의 역할: micro ROS client는 DDS-XRCE 프로토콜로 Agent와만 통신합니다. Agent가 DDS 네트워크로 변환해서 ROS2 전체 시스템과 연결해 줍니다.</li>
</ul>
<h3 id="os">OS</h3>
<p>FreeRTOS 등의 RTOS가 돌아갑니다. ROS2는 일반 리눅스에서도 돌고, 필요하면 RTOS + DDS 튜닝으로 하드 실시간도 노릴 수 있습니다.</p>
<h3 id="네트워크인터페이스">네트워크/인터페이스</h3>
<ul>
<li>ROS2: 주로 Ethernet/Wi-Fi 상의 DDS 통신으로 설계되었습니다.</li>
<li>micro-ROS: DDS-XRCE 기반으로 시리얼, CAN, Ethernet 등 다양한 링크 위에서 ROS2와 통신할 수 있게 설계되었습니다.</li>
</ul>
<h2 id="이미지-생성">이미지 생성</h2>
<p>일반적인 리눅스/라즈베리 파이랑은 다르게 조금 복잡했습니다. 펌웨어 이미지를 굽고, Agent까지 빌드 및 실행해야 했습니다.</p>
<h3 id="1-micro-ros-소스코드-다운로드">1. micro-ROS 소스코드 다운로드</h3>
<p>micro-ROS도 이미지를 구워야만 사용할 수 있습니다. 2026년 1월 기준으로, 충돌이 발생하기 쉽기 때문에, 도커 컨테이너를 만들어 해당 컨테이너 안에서 작업을 수행했습니다.
<a href="https://hub.docker.com/r/osrf/ros">Docker Hub-ROS</a></p>
<p>조금 신기했던 점은, 도커 허브의 ROS레포, ROS2레포가 제 생각과는 다른 네이밍이라는 점입니다.</p>
<ul>
<li>ROS 레포: ROS1과 ROS2 모두 포함. 일반적인 개발용도에 적합</li>
<li>ROS2 레포: ROS2 전용. 실험적 기능/ 불안정한 인터페이스
ROS 레포에서 ROS2용 배포버전(Jazzy 등)을 태그에서 선택하면 됩니다.</li>
</ul>
<h3 id="2-펌웨어-프로젝트-생성">2. 펌웨어 프로젝트 생성</h3>
<pre><code>ros2 run micro_ros_setup create_firmware_ws.sh freertos esp32</code></pre><p>이때 코드 편집기(vim/nano 등), 예제 코드를 다운해 둡니다.
예제 코드는 ping-pong을 사용했는데, 해당 예제코드만 그런 건지는 모르겠지만, app.c에서 ROS_DOMAIN_ID 초기화를 설정해야 했습니다. 이건 리팩토링하면 좋을 텐데... 조금 아쉬웠습니다.
게다가 초기화 문제를 잘 설정하지 않으면 USB를 인식하지 못했습니다.
추가로, USB 권한이 crw-rw-rw- 일 때 제대로 USB를 인식했습니다.</p>
<h3 id="3-펌웨어-빌드-및-플래시">3. 펌웨어 빌드 및 플래시</h3>
<p><strong>빌드</strong>:</p>
<pre><code>ros2 run micro_ros_setup build_firmware.sh</code></pre><p>평범하게 run으로 build shell 파일을 실행하면 됩니다.</p>
<p><strong>플래시</strong>:</p>
<pre><code>ros2 run micro_ros_setup flash_firmware.sh</code></pre><p>이것 또한 run으로 flash shell 파일을 실행합니다.</p>
<h3 id="5-agent-빌드-및-실행">5. Agent 빌드 및 실행</h3>
<p>MCU에 펌웨어를 올렸다고 끝이 아닙니다. 아키텍처에 있는 Agent를 실행해야만 MCU와 ROS2 네트워크가 연결됩니다.</p>
<p>Agent 빌드:</p>
<pre><code class="language-bash">ros2 run micro_ros_setup create_agent_ws.sh
ros2 run micro_ros_setup build_agent.sh</code></pre>
<p>Agent 실행:</p>
<pre><code class="language-bash">ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200</code></pre>
<p>Agent까지 실행되면 MCU의 노드가 ROS2 네트워크에 나타납니다. 또, node/topic list가 예제 프로그램에 대해 나타나는 걸 알 수 있습니다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://github.com/osrf/docker_images">Docker Hub 내 ROS, ROS2 Repository 매뉴얼</a></li>
<li><a href="https://micro.ros.org/">Micro-ROS 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 빵 자동 스캐너와 매장 모니터링 프로젝트]]></title>
            <link>https://velog.io/@hyoin_0219/ROS2-%EB%B9%B5-%EC%9E%90%EB%8F%99-%EC%8A%A4%EC%BA%90%EB%84%88-%EB%94%A5%EB%9F%AC%EB%8B%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hyoin_0219/ROS2-%EB%B9%B5-%EC%9E%90%EB%8F%99-%EC%8A%A4%EC%BA%90%EB%84%88-%EB%94%A5%EB%9F%AC%EB%8B%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 18 Jan 2026 09:13:02 GMT</pubDate>
            <description><![CDATA[<p>이 글은 딥러닝 기반 무인 키오스크 및 매장 모니터링 시스템을 구현한 프로젝트에 대한 회고입니다. 기술적인 결과만 나열하기보다는, 프로젝트를 진행하며 어떤 고민을 했고 어떤 선택을 했는지를 중심으로 적어보았습니다. 이후 포트폴리오로 활용하기 위해, 미래의 저에게 설명하듯 정리해 두려는 목적도 있습니다.</p>
<hr>
<h2 id="1-프로젝트-개요와-문제-정의">1. 프로젝트 개요와 문제 정의</h2>
<p>본 프로젝트는 두 가지 문제 해결에서 시작했습니다.</p>
<p>첫째, 베이커리 매장에서 <strong>외형이 유사한 빵 메뉴를 자동으로 인식·분류하는 무인 키오스크</strong>입니다. 빵은 외형에 따라 메뉴명이 달라지는 경우가 많아 키오스크 도입이 어렵다는 한계가 있었고, 이를 이미지 기반 분류로 해결하고자 했습니다.</p>
<p>둘째, <strong>매장 내 CCTV 영상을 활용해 상황을 모니터링하고 위험 상황을 감지하는 시스템</strong>입니다. 폭행, 낙상, 이동약자 감지를 통해 관리자에게 알림과 통계를 제공하는 것을 목표로 했습니다.</p>
<p>저는 이 프로젝트에서 다음 역할을 담당했습니다.</p>
<ul>
<li>무인 키오스크 기능 구현</li>
<li>관리자용 대시보드 UI 구성 및 API 연동</li>
<li>폭행 감지 모델 설계 및 구현</li>
<li>CCTV 모니터링용 모델 3종 통합</li>
</ul>
<p>특히 폭행 모델을 설계하고 모델들을 통합할 때, <strong>한정된 GPU 환경에서 실시간으로 여러 AI 모델을 동시에 운용해야 함</strong>에 집중했습니다. </p>
<hr>
<h2 id="2-전체-시스템-및-모델-구성">2. 전체 시스템 및 모델 구성</h2>
<h3 id="2-1-빵-스캔-기능">2-1. 빵 스캔 기능</h3>
<ul>
<li>빵 탐지: YOLO Segmentation (겹쳐진 빵을 인식할 목적 포함)</li>
<li>빵 분류: ResNet50 기반 feature extractor + kNN</li>
</ul>
<h3 id="2-2-cctv-모니터링-기능">2-2. CCTV 모니터링 기능</h3>
<ul>
<li>이동약자 감지: YOLO Detection</li>
<li>낙상 감지: YOLO Pose + LSTM</li>
<li>폭행 감지: XGBoost + Random Forest soft voting</li>
</ul>
<p>폭행 감지 모델 선정 과정과 리소스 제약에 대한 고민은 별도 글로 정리했습니다: <a href="https://velog.io/@hyoin_0219/%EB%AA%A8%EB%8D%B8-%EC%84%A4%EA%B3%84-6GB-GPU%EC%97%90%EC%84%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-AI-3%EC%A2%85-%EB%8F%8C%EB%A6%AC%EA%B8%B0">모델-설계-6GB-GPU에서-실시간-AI-3종-돌리기</a></p>
<hr>
<h2 id="3-주요-시행착오와-의사결정">3. 주요 시행착오와 의사결정</h2>
<h3 id="3-1-폭행-감지-모델-전처리-방식-변경">3-1. 폭행 감지 모델 전처리 방식 변경</h3>
<p>초기에는 YOLO Pose 기반 전처리를 사용해 폭행 여부를 분류하려 했습니다. 그러나 실제 구현 과정에서 다음과 같은 문제가 드러났습니다.</p>
<ul>
<li>매 프레임마다 YOLO Pose를 실행해야 해 연산 비용이 과도함</li>
<li>다중 인물 상황에서 인식 안정성이 낮음</li>
<li>신체가 크게 뒤틀리는 동작(엎어치기 등)에서는 인식 실패가 빈번함</li>
</ul>
<p>오프라인 성능 지표(f1 score, precision, recall)는 모니터링용으로 무난한 수준이었지만, <strong>실제 테스트에서 실시간 처리가 불가능하다는 점이 결정적인 한계</strong>였습니다. 이때 처음으로, 정량적 지표만 보고 모델을 선택하는 건 서비스에서는 위험할 수 있겠다는 생각이 들었습니다.</p>
<p>대안으로 선택한 것이 <strong>optical flow 기반 전처리</strong>였습니다. 매장은 소극적인 행동(걷기, 앉기 등)이 대부분이기 때문에, 폭행 상황에서만 픽셀 이동량이 급격히 변할 것이라는 가설을 세웠습니다. 이 가설을 바탕으로 optical flow를 특징으로 사용해 모델을 재학습했고, 실시간 환경에서도 안정적으로 동작하는 결과를 얻었습니다.</p>
<p>이 과정에서 얻은 가장 큰 교훈은 다음과 같습니다.</p>
<ul>
<li>실시간 서비스에서는 &quot;모델 성능&quot;보다 &quot;전체 파이프라인의 안정성&quot;이 더 중요하다</li>
<li>오프라인 지표와 실제 운영 환경 간에는 명확한 간극이 존재하며, 간극을 해소하기 위해선 실제 환경과 유사한 환경에서의 테스트가 필수적이다.</li>
</ul>
<hr>
<h3 id="3-2-발표-자료-구성에-대한-고민">3-2. 발표 자료 구성에 대한 고민</h3>
<p>지금까지 겪었던 프로젝트들은 시연 중심 발표였지만, 이번 프로젝트는 발표만으로도 청중이 시스템을 이해할 수 있어야 했습니다. 이에 따라 다음 원칙을 세우고 자료를 구성했습니다.</p>
<ul>
<li>설계는 구현의 지도이므로, 청중이 설계의 흐름을 따라갈 수 있어야 한다</li>
<li>기술 설명보다 시연을 먼저 배치해 맥락을 제공한다</li>
<li>시연은 기능 설명이 가능한 테스트케이스 중심으로 구성한다</li>
<li>기술 설명과 중복되는 트러블슈팅은 과감히 제거한다</li>
</ul>
<p>여러 차례 피드백과 자료 수정 끝에 발표를 마무리했고, 이후 받은 피드백을 바탕으로 파이널 프로젝트에서는 발표 흐름에 더 신경 쓸 계획입니다.</p>
<hr>
<h2 id="4-아쉬웠던-점과-한계">4. 아쉬웠던 점과 한계</h2>
<h3 id="4-1-통신-구조의-미완성">4-1. 통신 구조의 미완성</h3>
<p>전체 구조상 AI 서버와 GUI를 연결하는 central 서버를 두었지만, 실제 구현에서는 CCTV 스트리밍을 AI 서버에 직접 연결했습니다. 일정과 구현 난이도를 고려해 단기적으로 선택한 구조였지만, 결과적으로 UDP 기반 통신을 프로젝트 기간 내에 완성하지 못했습니다.</p>
<p>이 선택은 빠른 기능 구현에는 도움이 되었으나, 서비스 확장성과 역할 분리에 한계가 있다는 점이 분명합니다. 그래서 프로젝트가 끝난 지금, 별도로 UDP 통신을 구현하며 구조를 보완하고 있습니다.</p>
<h3 id="4-2-역할-외-기술에-대한-이해-부족">4-2. 역할 외 기술에 대한 이해 부족</h3>
<p>팀 내 소통은 원활했지만, 제가 직접 담당하지 않은 빵 탐지·분류 파트의 세부 기술에 대한 이해가 초반에는 부족했습니다. 매일 진행했던 스탠딩 회의에서 기능 설명을 들었기 때문에 기능 자체는 이해했지만, 사용된 모델과 기법의 정확한 기술적 맥락을 늦게 파악했다는 점이 아쉬움으로 남았습니다.</p>
<p>이 경험을 통해 이후 프로젝트에서는 다음을 원칙으로 삼으려 합니다.</p>
<ul>
<li>역할과 무관하게 전체 시스템의 핵심 기술 스택을 초기에 정리한다</li>
<li>발표 전에는 주요 모델과 기법의 기본 구조를 직접 확인한다</li>
</ul>
<hr>
<h2 id="5-정리하며">5. 정리하며</h2>
<p>이번 프로젝트는 단순히 여러 AI 모델을 구현하는 데서 끝나지 않고, <strong>제한된 리소스 환경에서 실시간 시스템을 설계하고 타협하는 경험</strong>이었습니다. 특히 폭행 감지 모델을 구현하며, 서비스 환경을 확인하기 위해 실제 환경과 유사한 테스트가 정말 중요하다는 걸 다시금 깨달았습니다.</p>
<p>이 경험을 바탕으로 이후 프로젝트에서는 모델 성능뿐 아니라, 시스템 전체 관점에서의 안정성과 확장성을 더 적극적으로 고려하고 싶습니다.</p>
<p><a href="https://github.com/addinedu-ros-11th/deeplearning-repo-3">프로젝트 소스코드</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[ROS2] ROS2의 원리와 통신 방법 정리]]></title>
            <link>https://velog.io/@hyoin_0219/ROS2-ROS2%EC%9D%98-%EC%9B%90%EB%A6%AC%EC%99%80-%ED%86%B5%EC%8B%A0-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@hyoin_0219/ROS2-ROS2%EC%9D%98-%EC%9B%90%EB%A6%AC%EC%99%80-%ED%86%B5%EC%8B%A0-%EB%B0%A9%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 11 Jan 2026 11:43:19 GMT</pubDate>
            <description><![CDATA[<p>학원에서 ROS2에 대해 배웠는데, ROS2의 깊은 원리에 대해 따로 공부해봤다.
ROS1과 ROS2의 차이점, ROS2의 통신 방식과 지원 인터페이스에 대해 정리해봤다.</p>
<h1 id="ros1-vs-ros2">ROS1 vs ROS2</h1>
<h2 id="ros1의-철학">ROS1의 철학</h2>
<p>중앙 집중식: master node를 중심으로 돌아간다.
빠른 개발과 실험 우선. 복잡한 설정 없이 연구자들이 빠르게 프로토타입을 만들 수 있다.
단순함이 중점이다. 실전 배포에선 한계에 노출된다.</p>
<h2 id="ros2의-철학">ROS2의 철학</h2>
<p>master node 없이 p2p 분산 시스템
보안 (DDS) 지원으로 실제 산업에서 사용할 수 있게 함.</p>
<p>ROS1의 master node가 없는 ROS2에선, 노드들이 서로 찾기 위해 Discovery 매커니즘을 사용한다:</p>
<ul>
<li>Simple Discovery(멀티캐스트): 각 노드가 네트워크에 &#39;나 이런 Topic을 가지고 있어&#39;라는 신호를 보내고, 다른 노드들이 이를 받아서 자동으로 연결한다. 노드가 많아지면 네트워크 트래픽이 n^2 로 증가한다.</li>
<li>Discovery Server: 중앙 서버를 통해 Discovery 정보를 관리한다. 대규모 시스템이나 멀티캐스트가 안 되는 상황에서 유용하다.</li>
</ul>
<h1 id="ros2의-통신-방식">ROS2의 통신 방식</h1>
<p>사용자가 node간 통신을 쉽게 구현할 수 있도록, ROS2에서 통신 인터페이스를 지원한다. 이 인터페이스는 파일 확장자로 구분한다.</p>
<ul>
<li>Topic <code>.msg</code>: 비동기 단방향 (Pub/Sub). 센서 스트림, 상태 공유 등에 사용</li>
<li>Service <code>.srv</code>: 동기 양방향 (Req/Resp). 설정 변경 등</li>
<li>Action <code>.action</code>: 비동기 양방향 (Goal/Feedback/Result). 취소가 가능한 긴 작업에 사용</li>
</ul>
<blockquote>
<p>Unlike ROS 1, which primarily only supported TCP, ROS 2 benefits from the flexibility of the underlying <strong>DDS transport in environments with lossy wireless networks</strong> where a “best effort” policy would be more suitable, or in real-time computing systems where the right Quality of Service profile is needed to meet deadlines.</p>
</blockquote>
<p>공식문서에도 언급하듯이 ROS2는 DDS를 이용하고 있다.</p>
<h2 id="dds와-qos">DDS와 QoS</h2>
<pre><code class="language-&quot;rclcpp/rclcpp.hpp&quot;">ROS2 QoS 설정
    ↓ (rclcpp::QoS로 변환)
DDS QoS 정책들 (Reliability, History, Durability 등 22개)
    ↓ (DDS 구현체 FastDDS/CycloneDDS로 전달)
실제 RTPS 프로토콜 통신 (UDP/TCP 위에서 동작)</code></pre>
<ol>
<li>publisher가 QoS를 설정하고 Topic에 publish하면(create_publisher() 호출) DDS DataWriter가 생성된다.</li>
<li>subscriber가 같은 Topic을 구독하면 DDS DataReader가 Publisher를 Discovery한다</li>
<li>DDS가 QoS 호환성을 검사한다.</li>
<li>호환되면 RTPS 통신을 시작한다. QoS 정책에 따라 재전송/버퍼링/타임아웃 등을 적용한다.</li>
</ol>
<h3 id="qos">QoS</h3>
<p>통신 품질을 정하는 설정 옵션이다. ROS2에서 publisher, subscriber가 메시지를 주고받을 때 &#39;얼마나 신뢰성 있게 보낼지, 얼마나 오래 보관할지, 유실을 허용할지&#39; 등을 QoS로 정한다.
UDP처럼 유실돼도 상관 없지만 지연이 적어야 하는 데이터, TCP처럼 신뢰성이 중요한 데이터로 나뉘는데, 이 두 데이터를 똑같이 다루면 안 되기  때문에 QoS로 통신 성격을 고른다.
Publisher 노드, Subscriber 노드에서 QoS를 설정하면 된다.
Publisher 노드:</p>
<pre><code class="language-&quot;rclcpp/rclcpp.hpp&quot;">#include &quot;rclcpp/rclcpp.hpp&quot;
#include &quot;std_msgs/msg/string.hpp&quot;

class MinimalPublisher : public rclcpp::Node
{
public:
  MinimalPublisher()
  : Node(&quot;minimal_publisher&quot;)
  {
    auto qos = rclcpp::QoS(rclcpp::KeepLast(10));  // History: KEEP_LAST, depth=10
    qos.reliable();                                // Reliability: RELIABLE
    qos.durability_volatile();                     // Durability: VOLATILE

    publisher_ = this-&gt;create_publisher&lt;std_msgs::msg::String&gt;(&quot;chatter&quot;, qos);
  }

private:
  rclcpp::Publisher&lt;std_msgs::msg::String&gt;::SharedPtr publisher_;
};
</code></pre>
<p>Subscriber 노드:</p>
<pre><code class="language-&quot;rclcpp/rclcpp.hpp&quot;">subscription_ = this-&gt;create_subscription&lt;std_msgs::msg::String&gt;(
  &quot;chatter&quot;,
  qos,
  std::bind(&amp;MinimalSubscriber::topic_callback, this, std::placeholders::_1)
);
</code></pre>
<p>QoS가 건드리는 주요 요소</p>
<ul>
<li>History/ Depth: 과거 메시지를 몇 개까지 보관할지 (최신 N개만 보관 vs 전부 보관)</li>
<li>Reliability: 유실을 감수하고 빠르게 보낼지(BEST_EFFORT), 재전송해서 확실히 보낼지(RELIABLE)</li>
<li>Durability: 늦게 붙은 subscriber에게 예전 메시지를 다시 줄지(TRANSIENT_LOCAL) 말지(VOLATILE)</li>
</ul>
<h3 id="dds">DDS</h3>
<p>OMG (Object Management Group)가 정의한 표준 미들웨어 스펙이며, ROS2는 이 DDS를 기반으로 통신을 구현한다. Fast-DDS, Cyclone DDS 등 다양한 구현체를 선택할 수 있다.</p>
<ul>
<li>데이터 중심: 메시저/노드가 아니라 topic을 중심으로 통신을 구성함</li>
<li>발행-구독: pub가 topic에 데이터를 올리고, subscriber가 관심 있는 topic을 구독해서 받는다. 중간 브로커 (ROS1에서의 master node)가 필요 없다.</li>
<li>분산형: 모든 노드가 직접 연결되어 중앙 장애점이 없고, 네트워크 변화에도 자동 복구된다.
노드들은 신경쓰지 않고, Topic에 어떤 데이터를 올릴지만 개발자가 결정하면 된다. 그 후엔 설정되어 있는 QoS에 맞춰서 전달된다.</li>
</ul>
<h3 id="rtps-real-time-publish-subscribe">RTPS (Real-Time Publish-Subscribe)</h3>
<p>DDS의 실제 네트워크 전송 프로토콜. DDS가 어떻게 통신할지의 설계도라면, RTPS는 그 설계도를 UDP/IP 레이어에서 실제 패킷으로 구현한다.
다른 DDS 벤더끼리도 RTPS 표준 때문에 서로 통신이 가능하다.
QoS는 RTPS 헤더에 넣어서 재전송/버퍼링/타임아웃 등을 네트워크 레벨에서 처리한다.</p>
<h2 id="출처">출처</h2>
<p><a href="https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Quality-of-Service-Settings.html#id1">ROS2 공식문서 - Quality of Service settings</a>
<a href="https://yhoons.tistory.com/117">DDS 이해하기</a>
<a href="https://luckydipper.tistory.com/17">ROS2, DDS와 QoS</a>
<a href="https://lab-notes.tistory.com/entry/DDS-DDS%EC%99%80-RTPS-%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC">DDS와 RTPS 이해하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 회고]]></title>
            <link>https://velog.io/@hyoin_0219/2025%EB%85%84-%ED%9A%8C%EA%B3%A0-%EB%B0%9C%EA%B2%AC%EC%9D%98-%EC%8B%9C%EA%B0%84</link>
            <guid>https://velog.io/@hyoin_0219/2025%EB%85%84-%ED%9A%8C%EA%B3%A0-%EB%B0%9C%EA%B2%AC%EC%9D%98-%EC%8B%9C%EA%B0%84</guid>
            <pubDate>Sun, 04 Jan 2026 10:43:28 GMT</pubDate>
            <description><![CDATA[<p>2025년은 4년간의 학부 생활이 마무리되던 해였다. 그리고 내가 어떤 개발자가 되고 싶은지, 무엇에 흥미를 느끼는지 확실히 알게 된 한 해이기도 했다.</p>
<h2 id="12월-기초-다지기-📚">1~2월: 기초 다지기 📚</h2>
<h3 id="졸업프로젝트-기획">졸업프로젝트 기획</h3>
<p>졸업 프로젝트로 금융 상품 추천 어플을 기획했다. 교수님과의 정기 미팅을 통해 방향을 잡아가며 파인튜닝과 랭체인 라이브러리를 공부했다. 팀원들과 스터디를 하면서 AI 개발의 기초를 쌓아갔다.</p>
<h2 id="35월-첫-발걸음-👣">3~5월: 첫 발걸음 👣</h2>
<h3 id="졸업프로젝트-진행">졸업프로젝트 진행</h3>
<p>3월부터 본격적으로 코드를 작성하기 시작했다. 파인튜닝에 집중하며 AI용 서버를 제작했고, 5월에는 AWS에 배포까지 완료했다. </p>
<h3 id="umc-활동">UMC 활동</h3>
<p>한편 서버 개발을 배우고 학교 친구들과 네트워킹하고 싶어서 UMC에 합류했다. 스터디와 미니프로젝트를 병행하며 바쁘지만 알찬 시간을 보냈다. </p>
<h2 id="6월-전환점-🔄">6월: 전환점 🔄</h2>
<h3 id="umc-미니-프로젝트-마무리">UMC 미니 프로젝트 마무리</h3>
<p>UMC 미니프로젝트를 마무리하면서 중요한 깨달음을 얻었다. 서버 개발이 재미있긴 했지만, 이게 내 길은 아니라는 확신이 들었다. <strong>나는 사람과 인터랙티브한 소프트웨어를 만들고 싶어서 컴퓨터공학으로 전과했는데, 웹/앱 개발은 스마트폰과 컴퓨터라는 제약이 크게 느껴졌다.</strong></p>
<h3 id="기숙사-퇴실">기숙사 퇴실</h3>
<p><img src="https://velog.velcdn.com/images/hyoin_0219/post/7b94cc7e-999b-4d0f-8682-8cf4da17864b/image.png" alt="">
대학시절 1년 반을 살았던 기숙사를 퇴실했다. 이제 더이상 서울에 내 장소가 없고, 친한 룸메이트들과 떨어져 산다는 게 살짝 섭섭했지만, 역시 집이 최고다! 기숙사비용이나 식비도 안 나가고, 조용한 내 방이 있다는 게 좋다.</p>
<h2 id="7월-새로운-방향-🧭">7월: 새로운 방향 🧭</h2>
<h3 id="umc-데모데이-시작">UMC 데모데이 시작</h3>
<p>서버가 내 길이 아니어도 맡은 역할은 해내야 했다. UMC 데모데이를 위해 Wayble 팀에서 &#39;장애인을 위한 대중교통 길찾기&#39;를 열심히 구현했고, 졸업프로젝트의 AI 서버도 견고하게 완성했다.</p>
<h3 id="토익스피킹-응시">토익스피킹 응시</h3>
<p>4학년인데 어학성적도 없다는 게 조금 부끄러워서 토익스피킹을 신청했는데, 막상 데모데이나 졸업프로젝트 때문에 준비할 시간이 없었다. 일주일정도 벼락치기해서 IM3를 받았다...</p>
<h3 id="인턴-지원">인턴 지원</h3>
<p>청년인턴은 로봇 관련 기업에 지원했다. 전혀 경험이 없던 분야였는데 면접까지 갔고, (당연히도) 최종 탈락했다. 면접관님도 그렇고, 지원자를 보면서 들었던 개인적인 생각도 그렇고, 나는 임베디드 분야 경험이 정말 없다시피 한다. 이때부터 임베디드 분야로 뭐든 프로젝트 경험을 쌓아야겠다고 생각했다.</p>
<h3 id="rtos-개인프로젝트-시도">RTOS 개인프로젝트 시도</h3>
<p>임베디드 분야하면 가장 먼저 생각나는 게 RTOS였기 때문에, RTOS를 활용한 개인 프로젝트를 하고자 했다. 그런데 하드웨어 제어의 어려움을 실감했다. 개념은 괜찮았지만, 보드에 모듈을 연결하는 실습이 쉽지 않았다. <strong>임베디드 분야로 프로젝트 경험을 쌓기 위해선, 부트캠프 등 멘토가 있는 환경에 들어가야겠다고 생각했다</strong></p>
<h2 id="8월-배움과-반성-💭">8월: 배움과 반성 💭</h2>
<h3 id="umc-데모데이-끝">UMC 데모데이 끝</h3>
<p><img src="https://velog.velcdn.com/images/hyoin_0219/post/38d028c7-24cc-4dc3-b5db-7d5fb9334d71/image.png" alt=""></p>
<p>UMC 데모데이가 끝났다. 프론트 연동이 늦어서 데모데이 날 처음 작동하는 걸 봤는데 기분이 좋았다. 하지만 데모 중에 Out of Memory 문제가 발생했다. 당시엔 앱을 재실행하는 방법으로 해결했는데, 집에 와서 확인해보니 대중교통 길찾기 기능에서 너무 많은 정보를 불러오다 메모리가 터진 것이었다. <strong>리소스를 펑펑 쓴 결과였다. 리팩토링은 해냈지만, 미리 테스트했다면 방지할 수 있었을 문제였다는 게 아쉬웠다.</strong></p>
<h3 id="졸업프로젝트-끝">졸업프로젝트 끝</h3>
<p>경험이 부족한 상태에서 개발했기 때문일까, 데모데이에 더해 아쉬움이 있는 프로젝트였다. 챗봇 기능을 만들면서, 오히려 시간을 투자해 공부했던 랭체인 라이브러리 (tools 등)를 제대로 활용하지 못했다. 랭체인 기능을 잘 활용을 못하니 당연하게도 챗봇 기능이 불안정했다.
다행히 팀원의 도움으로 해당 기능은 완성이 됐지만, 내 습관을 조금만 고쳤으면 더 빠르게 안정적인 기능이 되지 않았을까 싶다. <strong>기능이 불안정했을 때, 다른 레퍼런스들을 뒤져서라도 찾아봤어야 했는데, 과제했던 습관이 남아있어서 그런지 나 혼자만의 힘으로 개발해야 한다는 생각이 깊었다. 그래서 불필요한 시간이 많이 들어갔다.</strong>
이 아쉬움을 극복하기 위해 부트캠프에서 진행하는 프로젝트들은 기능 구현을 하기 전에 일단 레퍼런스를 찾아보는 습관이 생겼다.</p>
<h3 id="부트캠프-시작">부트캠프 시작</h3>
<p>임베디드 분야로 취업을 하고자 동아리나 스터디를 찾아봤는데... 내가 들어갈 수 있는 곳이 없었다. 대부분 타학교의 중앙동아리였다. 그래서 부트캠프라도 들어가야겠다고 생각했다.
기업 주관 부트캠프를 기다릴 시간의 여유가 없어서, 후기가 좋고 취업 지원까지 하는 국비 ROS2 자율주행 부트캠프에 등록했다. 
프로젝트 주제, 학원의 관심도, 하드웨어 환경을 중점적으로 학원을 골랐고, 지금 지원한 부트캠프가 IoT/딥러닝/ROS2 프로젝트가 있고 로봇을 직접 제어할 수 있다는 점이 마음에 들었다. 학원에서 학생들을 많이 케어해준다는 리뷰도 한 몫 했다.</p>
<h2 id="910월-권태와-인내-⏳">9~10월: 권태와 인내 ⏳</h2>
<h3 id="부트캠프에서의-한-달">부트캠프에서의 한 달</h3>
<p>비전공자도 있어서 그런지 첫 한 달은 너무 기초적인 내용이었다. 그래서 C++ 스터디를 진행하며 알고리즘이 아닌 실제 프로젝트에서의 활용법을 배웠다. 
모든 게 학부 수업에서 들었던 내용이라 자극이 부족했고, 새로운 지식을 배우지 못하니 수업이 재미없었다. 그래도 강사님께 예의를 지키려 억지로라도 버텼다.</p>
<h2 id="1112월-재미의-재발견-✨">11~12월: 재미의 재발견 ✨</h2>
<p>EDA 프로젝트는 이미 아는 내용의 반복이라 크게 인상적이지 않았다. 그래도 streamlit으로 대시보드를 만들어 완성도를 높였다.
<a href="https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%84%9C%EC%9A%B8%EC%8B%9C-%EC%83%81%EA%B6%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EC%97%85%EC%A2%85%EB%B3%84-%EB%A7%A4%EC%B6%9C-%EB%B6%84%EC%84%9D-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%ED%9A%8C%EA%B3%A0">EDA 프로젝트 회고</a></p>
<h3 id="iot-프로젝트-진행과-마무리">IoT 프로젝트 진행과 마무리</h3>
<p>IoT 프로젝트를 시작하면서 전환점이 왔다. 9~10월 스터디 내용을 실전에 적용하고, 7월에 애먹었던 모듈 연결도 요령이 생겼다(보드는 한 번 태웠지만). 순서도를 그리며 미리 설계하고, 설계와 결과물을 비교하며 스스로를 피드백하는 과정이 재미있었다.
<a href="https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%99%88-IoT-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8">IoT 프로젝트 회고</a></p>
<h3 id="딥러닝-프로젝트-시작">딥러닝 프로젝트 시작</h3>
<p>딥러닝 프로젝트도 처음엔 걱정이었다. 전처리는 반복 작업, 모델 학습은 그저 라이브러리를 쓰는 것뿐이라 재미없다고 생각했다.</p>
<p>하지만 여러 모델을 실시간으로 돌려야 하는 상황이 되면서 리소스 제약을 고민하게 됐다. 8월 데모데이 때 못했던 리소스 최적화를 지금에서라도 하니 재밌었다. 서버 반응 속도와 리소스를 테스트하며 문제를 해결해나가는 과정이 즐거웠다.
<a href="https://velog.io/@hyoin_0219/%EB%AA%A8%EB%8D%B8-%EC%84%A4%EA%B3%84-6GB-GPU%EC%97%90%EC%84%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-AI-3%EC%A2%85-%EB%8F%8C%EB%A6%AC%EA%B8%B0">딥러닝 프로젝트: 리소스 제약 기반 설계</a></p>
<h2 id="돌아보며-🌟">돌아보며 🌟</h2>
<p>2025년은 발견의 시기였다.</p>
<p>여러 시도 끝에 내가 정말 하고 싶은 개발이 무엇인지 발견했다. 단순한 개발이 아닌 아닌 제약 속에서 최적화를 고민하는 개발. 컴퓨터/핸드폰을 초월해 사람과 직접적인 상호작용이 있는 소프트웨어를 만들고 싶다는 초심을 다시 확인했다.</p>
<p><strong>아직 배워야 할 게 많지만, 내가 무엇을 좋아하고 무엇을 싫어하는지 확실히 알게 되니 흔들림이 줄었다.</strong></p>
<p>여담으로, 큰 경험을 한 뒤엔 꼭 사진을 찍어야겠다는 생각이 든다. 일정은 잘 적어둬서 빠진 경험은 없지만, 사진을 안 찍으니 회고록에 글밖에 없다 ㅠㅠ...</p>
<h2 id="2026년을-향해-🚀">2026년을 향해 🚀</h2>
<h3 id="1월-목표">1월 목표</h3>
<p>취업뿐만 아니라, 2025년에 아쉽다고 느꼈던 점들을 보완하기 위한 계획이다.</p>
<p><strong>프로젝트 🛠️</strong></p>
<ul>
<li>딥러닝 프로젝트 마무리<ul>
<li>서버의 응답 속도, 메모리 차지 여부 파악 및 최적화</li>
</ul>
</li>
<li>RTOS 개인 프로젝트 재도전<ul>
<li>7월에 어려움을 겪었던 부분 다시 시도</li>
<li>스프린트 방식으로 진행하여 1월 내 완료</li>
</ul>
</li>
</ul>
<p><strong>역량 강화 💪</strong></p>
<ul>
<li>코딩테스트 실력 향상<ul>
<li>지금까지 인턴/신입 지원에 있어 코딩테스트가 큰 벽이었다. 내 코딩테스트 실력을 객관적으로 따져보면, 아무리 알고리즘 수업을 들었다곤 해도 실력이 좋다곤 못한다. 까먹은 개념도 많다.</li>
<li>그래서 인프런의 코딩테스트 강의를 수강하고 있다. 시간을 너무 끌면 안되니 이번달 내로 수강 및 복습을 끝내는 게 목표다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[모델 설계] 6GB GPU에서 실시간 AI 3종 돌리기]]></title>
            <link>https://velog.io/@hyoin_0219/%EB%AA%A8%EB%8D%B8-%EC%84%A4%EA%B3%84-6GB-GPU%EC%97%90%EC%84%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-AI-3%EC%A2%85-%EB%8F%8C%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@hyoin_0219/%EB%AA%A8%EB%8D%B8-%EC%84%A4%EA%B3%84-6GB-GPU%EC%97%90%EC%84%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-AI-3%EC%A2%85-%EB%8F%8C%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Sun, 28 Dec 2025 05:40:00 GMT</pubDate>
            <description><![CDATA[<p>베이커리 키오스크 시스템을 구축하는 중에 3개의 AI 모델(폭행 감지, 이동약자 지원, 낙상 감지)을 6GB GPU 메모리 내에서 동시에 돌려야 하는 상황이 생겼습니다.</p>
<p>이 글에선 그 중 <strong>폭행 감지 모델</strong>을 어떻게 설계하고 최적화했는지 공유합니다.</p>
<hr>
<h2 id="1-시스템-제약-조건">1. 시스템 제약 조건</h2>
<h3 id="요구사항">요구사항</h3>
<pre><code>목표: 베이커리에서 실시간 폭행 감지
- 모든 이벤트 로깅
- GPU: RTX 2060 (6GB VRAM)
- 30fps 유지 필요
- AI의 역할: 폭행 여부만 판정</code></pre><h3 id="gpu-메모리-배분">GPU 메모리 배분</h3>
<p>GPU 메모리 6GB가 우리가 가진 전부였습니다. 3개 모델을 동시에 돌려야 하니까:</p>
<pre><code>YOLOv8 Pose (낙상 감지):        최대 500MB
YOLOv8 Detection (보조기구):    최대 500MB
분류 모델 (폭행 판정):          100MB
─────────────────────────
총 1.1GB
</code></pre><p>남은 4.9GB로 다른 시스템을 감당할 수 있겠다고 봤습니다.</p>
<h3 id="속도-계산">속도 계산</h3>
<p>실시간 처리를 위해 속도도 확인했습니다:</p>
<pre><code>YOLOv8 Pose:        최대 33ms/프레임
YOLOv8 Detection:   최대 30ms/프레임
분류 모델:          최대 1.5ms/프레임
전처리/후처리:      5ms/프레임
─────────────────────
최대 70ms/프레임</code></pre><p>→ 30fps 달성 가능</p>
<hr>
<h2 id="2-분류-모델-선택">2. 분류 모델 선택</h2>
<p>분류 모델 선택 기준은:</p>
<ol>
<li>정확도 80% 이상</li>
<li>메모리 최소화</li>
<li>과적합 최소화</li>
</ol>
<p>후보를 비교해봤습니다:</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>정확도</th>
<th>메모리</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>SVM</td>
<td>60~82%</td>
<td>가장 높음</td>
<td>-</td>
</tr>
<tr>
<td>XGBoost</td>
<td>87~98%</td>
<td>39.7MB</td>
<td>과적합 위험 높음</td>
</tr>
<tr>
<td>RF</td>
<td>80.8~89.5%</td>
<td>23.8MB</td>
<td>안정적</td>
</tr>
</tbody></table>
<p><strong>선택: XGBoost + Random Forest의 soft voting</strong></p>
<p>이유는 두 모델의 약점을 서로 보완할 수 있기 때문입니다:</p>
<ul>
<li>XGBoost의 높은 정확도 + RF의 안정성</li>
<li>메모리: 약 63.5MB (충분)</li>
<li>예상 정확도: 82~85% (목표 80% 충분)</li>
</ul>
<hr>
<h2 id="3-학습-결과">3. 학습 결과</h2>
<h3 id="초기-성능">초기 성능</h3>
<pre><code>훈련 정확도:  99.90%
테스트 정확도: 83.30%
과적합 차이:  16.60% ← 심각함</code></pre><p><strong>Confusion Matrix:</strong></p>
<table>
<thead>
<tr>
<th>실제 \ 예측</th>
<th>비폭행</th>
<th>폭행</th>
</tr>
</thead>
<tbody><tr>
<td>비폭행</td>
<td>202 (TN)</td>
<td>39 (FN)</td>
</tr>
<tr>
<td>폭행</td>
<td>68 (FP)</td>
<td>265 (TP)</td>
</tr>
</tbody></table>
<h3 id="문제-과적합">문제: 과적합</h3>
<p>과적합을 해결하려면 보통은:</p>
<ul>
<li>더 큰 모델 사용 (메모리 초과)</li>
<li>모델 추가 (리소스 초과)</li>
<li>더 많은 데이터 수집 (시간 부족)</li>
</ul>
<p>이 모든 게 불가능했습니다. 메모리가 6GB로 정해져 있으니까요.</p>
<p>그 대신 <strong>정규화를 강화</strong>했습니다.</p>
<h3 id="정규화-강화">정규화 강화</h3>
<pre><code class="language-python"># Before
xgb_model = xgb.XGBClassifier(
    n_estimators=200,
    max_depth=10,
    learning_rate=0.1
)

# After
xgb_model = xgb.XGBClassifier(
    n_estimators=100,      # 연산량 감소
    max_depth=8,           # 트리 깊이 제한
    learning_rate=0.05,    # 느린 학습 (안정화)
    reg_lambda=1.0,        # L2 정규화 추가
    reg_alpha=0.5,         # L1 정규화 추가
)

rf_model = RandomForestClassifier(
    n_estimators=100,      # 200 → 100
    max_depth=12,          # 20 → 12
    min_samples_split=10,  # 5 → 10
    min_samples_leaf=5,    # 2 → 5
)</code></pre>
<h3 id="결과">결과</h3>
<pre><code>훈련 정확도:  99.90% → 90.49%
테스트 정확도: 83.30% → 81.36%
과적합 차이:  16.60% → 9.13% ✅
</code></pre><hr>
<h2 id="4-거짓-경보fp-감소">4. 거짓 경보(FP) 감소</h2>
<p>테스트 정확도는 81.36%인데, 거짓 경보가 68개나 됩니다.</p>
<p>AI 모델을 더 개선해보려 했습니다:</p>
<ul>
<li>하이퍼파라미터 조정 → FP: 68 (변화 없음)</li>
<li>정규화 강화 → FP: 68 (변화 없음)</li>
<li>데이터 리샘플링 → FP: 65 (3개만 감소)</li>
</ul>
<p>모델 자체의 한계인 것 같았습니다.</p>
<h3 id="해결책-임계값-조정">해결책: 임계값 조정</h3>
<p>AI는 &quot;폭행일 확률&quot;을 주고, 시스템에서 &quot;임계값&quot;을 결정하는 방식으로 바꿨습니다:</p>
<pre><code class="language-python">prediction = model.predict_proba(features)
# → [비폭행_확률, 폭행_확률]

CONFIDENCE_THRESHOLD = 0.55  # 조정 가능

if prediction_prob &gt;= CONFIDENCE_THRESHOLD:
    alert_level = &quot;VIOLENCE&quot;
elif 0.45 &lt;= prediction_prob &lt; 0.55:
    alert_level = &quot;SUSPICIOUS&quot;
else:
    alert_level = &quot;NORMAL&quot;</code></pre>
<h3 id="임계값별-성능">임계값별 성능</h3>
<pre><code>임계값 0.50: FP 68개, Recall 87%
임계값 0.55: FP 50개, Recall 85% ← 선택
임계값 0.60: FP 40개, Recall 83%
임계값 0.65: FP 30개, Recall 80%</code></pre><p><strong>0.55로 설정한 이유:</strong></p>
<ul>
<li>FP 26% 감소 (68 → 50)</li>
<li>Recall은 충분 (85%)</li>
<li>시스템 안정성 확보</li>
</ul>
<hr>
<h2 id="최종-성능">최종 성능</h2>
<pre><code>정확도:          81.36%
Precision:       79.58%
Recall:          87.17% (폭행 놓칠 확률 낮음)
ROC-AUC:         0.8766

거짓 경보(FP):   50개/일
메모리 사용:     ~1.1GB / 6GB (18%)
속도:            70ms/프레임 (30fps 달성)</code></pre><hr>
<h2 id="마치며">마치며</h2>
<p>사실 수업으로 AI 모델 학습에 대해 배웠을 땐, AI 모델 학습에 큰 재미를 가지진 못했습니다. </p>
<p>&quot;정확도를 높이기 위해 더 큰 모델을 쓴다&quot; &quot;데이터를 더 모은다&quot; 이런 식의 접근은 그냥 단순해 보였거든요.</p>
<p>하지만 이 프로젝트에서는 달랐습니다.</p>
<p><strong>&quot;6GB 메모리, RTX 2060, 30fps, 3개 모델을 동시에&quot;</strong>라는 제약이 주어졌을 때, 그 안에서 요구사항에 맞는 최적의 모델을 찾는 과정이 정말 재미있었습니다.</p>
<ul>
<li>&quot;메모리가 없으니 XGBoost를 쓰자&quot;</li>
<li>&quot;정확도를 더 높이고 싶지만, 과적합을 줄이는 게 현실적이겠다&quot;</li>
<li>&quot;AI 모델은 한계네. 그럼 시스템 레벨에서 해결하자&quot;</li>
</ul>
<p>이렇게 현실과 타협하면서 최선을 찾아가는 과정 자체가 개발의 매력이라고 느꼈습니다.</p>
<h2 id="출처">출처</h2>
<p><a href="https://www.kaggle.com/code/julnazz/model-training-testing-rf-svm-xgb">분류모델의 메모리 비교</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GCP] AWS와 GCP의 차이점]]></title>
            <link>https://velog.io/@hyoin_0219/GCP-AWS%EC%99%80-GCP%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@hyoin_0219/GCP-AWS%EC%99%80-GCP%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sun, 21 Dec 2025 13:23:56 GMT</pubDate>
            <description><![CDATA[<p>AWS를 미리 해본 만큼, GCP에도 빠르게 적응할 수 있으리라고 생각했는데, 막상 해보니 처음 보는 용어가 많더군요... 지금 GCP에서 할인이나 크레딧을 뿌리고 있어서 저같은 학생 개발자한테는 꽤 좋은 선택지가 된다고 생각합니다.</p>
<hr>
<h2 id="1-gcp의-기본-개념-프로젝트와-api">1. GCP의 기본 개념: 프로젝트와 API</h2>
<h3 id="프로젝트란-aws-account와의-차이">프로젝트란? (AWS Account와의 차이)</h3>
<p>GCP에서 <strong>프로젝트</strong>는 모든 클라우드 리소스를 담는 기본 단위입니다. AWS의 Account와 비슷해 보이지만, 실제로는 더 세밀한 격리 단위라고 볼 수 있습니다.</p>
<p><strong>핵심 차이점:</strong></p>
<ul>
<li>AWS는 Account가 청구의 최상위 단위이고, 그 안에서 리소스를 자유롭게 생성합니다</li>
<li>GCP는 프로젝트 단위로 청구, 권한, API, 리소스가 완전히 독립적으로 관리됩니다</li>
<li>하나의 조직 안에 여러 프로젝트를 만들어 개발/스테이징/프로덕션 환경을 분리하는 것이 일반적입니다</li>
</ul>
<h3 id="api-활성화가-필수인-이유">API 활성화가 필수인 이유</h3>
<p><strong>GCP의 가장 큰 특징 중 하나</strong>: 사용하고 싶은 서비스가 있으면 해당 API를 먼저 활성화해야 합니다.</p>
<p>AWS는 대부분의 서비스가 기본적으로 활성화되어 있어서, EC2든 S3든 바로 콘솔에 들어가서 사용할 수 있습니다. 하지만 GCP는 프로젝트 단위로 필요한 API만 켜서 사용하도록 설계되어 있습니다.</p>
<p><strong>왜 이렇게 할까?</strong></p>
<ul>
<li>보안: 사용하지 않는 서비스는 아예 접근 불가능</li>
<li>비용 관리: 실수로 불필요한 리소스를 생성하는 것을 방지</li>
<li>명확성: 프로젝트에서 어떤 서비스를 사용하는지 한눈에 파악 가능</li>
</ul>
<h3 id="주요-api-종류">주요 API 종류</h3>
<p>프로젝트를 시작할 때 자주 활성화하는 API들:</p>
<p><strong>컴퓨팅 리소스:</strong></p>
<ul>
<li>Compute Engine API: VM 인스턴스 생성</li>
<li>Kubernetes Engine API: GKE 클러스터 관리</li>
<li>Cloud Run API: 서버리스 컨테이너 실행</li>
</ul>
<p><strong>네트워킹:</strong></p>
<ul>
<li>VPC API: 네트워크 리소스 관리</li>
</ul>
<p><strong>스토리지:</strong></p>
<ul>
<li>Cloud Storage API: 객체 스토리지 사용</li>
</ul>
<p><strong>기타:</strong></p>
<ul>
<li>Cloud Logging API, Cloud Monitoring API 등</li>
</ul>
<h3 id="구글-api-vs-gcp-api">구글 API vs GCP API</h3>
<p>처음 GCP를 접하면 헷갈리는 부분이 있습니다. &quot;구글 API&quot;와 &quot;GCP API&quot;는 다릅니다.</p>
<ul>
<li><strong>구글 API</strong>: Gmail, Google Maps, YouTube 같은 구글 소비자 서비스용 API</li>
<li><strong>GCP API</strong>: 클라우드 인프라를 위한 API (Compute Engine, Cloud Storage 등)</li>
</ul>
<p>구글 API도 GCP 프로젝트에서 활성화할 수 있지만, 클라우드 컴퓨팅과는 별개의 영역입니다. 예를 들어 애플리케이션에서 Google Maps를 사용하고 싶다면 Maps API를 활성화하지만, 이것이 VM을 띄우는 것과는 무관합니다.</p>
<hr>
<h2 id="2-리소스-계층-구조와-iam">2. 리소스 계층 구조와 IAM</h2>
<h3 id="조직-→-폴더-→-프로젝트-→-리소스-구조">조직 → 폴더 → 프로젝트 → 리소스 구조</h3>
<p>GCP의 리소스는 계층 구조를 가집니다:</p>
<pre><code>조직 (Organization)
  └── 폴더 (Folder) - 선택사항
      └── 프로젝트 (Project)
          └── 리소스 (VM, 스토리지 등)</code></pre><p>이 구조의 핵심은 <strong>정책 상속</strong>입니다. 상위 레벨에서 설정한 IAM 정책이나 조직 정책이 하위로 자동으로 상속됩니다.</p>
<h3 id="aws-organizations와-비교">AWS Organizations와 비교</h3>
<p>AWS도 Organizations를 통해 Account를 계층적으로 관리할 수 있지만, 사용 방식이 다릅니다:</p>
<p><strong>AWS:</strong></p>
<ul>
<li>Account가 비용/권한의 기본 경계</li>
<li>OU(Organizational Unit)로 그룹핑</li>
<li>SCP(Service Control Policy)로 제어</li>
</ul>
<p><strong>GCP:</strong></p>
<ul>
<li>프로젝트가 비용/권한/리소스의 기본 경계</li>
<li>폴더로 프로젝트를 그룹핑</li>
<li>IAM 정책이 계층 상속</li>
</ul>
<h3 id="iam-모델의-차이점">IAM 모델의 차이점</h3>
<p>GCP IAM의 핵심 철학은 <strong>&quot;누가(Who) → 어떤 리소스에(Resource) → 어떤 역할을(Role) 갖는가&quot;</strong> 를 리소스 쪽에 붙여서 관리하는 것입니다.</p>
<p><strong>AWS IAM:</strong></p>
<ul>
<li>사용자/역할에 정책을 직접 연결 (Identity-based)</li>
<li>&quot;이 사용자는 이런 권한을 가진다&quot;는 접근</li>
</ul>
<p><strong>GCP IAM:</strong></p>
<ul>
<li>리소스에 바인딩을 설정 (Resource-based)</li>
<li>&quot;이 리소스는 이 사용자가 이렇게 접근할 수 있다&quot;는 접근</li>
<li>두 방식 모두 지원하지만, 리소스 중심 사고가 기본</li>
</ul>
<hr>
<h2 id="3-네트워킹-vpc부터-다시-이해하기">3. 네트워킹: VPC부터 다시 이해하기</h2>
<h3 id="gcp-vpc는-글로벌-서브넷은-리전">GCP VPC는 글로벌, 서브넷은 리전</h3>
<p>이것이 AWS에서 GCP로 넘어올 때 (개인적으로) 가장 헷갈리는 부분입니다.</p>
<p><strong>AWS:</strong></p>
<ul>
<li>VPC는 리전 단위 (예: us-east-1 VPC)</li>
<li>서브넷은 가용영역(AZ) 단위</li>
<li>다른 리전에 VPC를 만들려면 완전히 새로운 VPC 생성</li>
</ul>
<p><strong>GCP:</strong></p>
<ul>
<li>VPC는 글로벌 리소스 (전 세계에 하나의 VPC)</li>
<li>서브넷은 리전 단위</li>
<li>하나의 VPC 안에서 us-central1, asia-northeast3 등 여러 리전의 서브넷을 가질 수 있음</li>
</ul>
<h3 id="aws-vpc와의-핵심-차이">AWS VPC와의 핵심 차이</h3>
<p><strong>시작점의 차이:</strong></p>
<ul>
<li>AWS: VPC와 서브넷을 먼저 만들어야 인스턴스 배포 가능</li>
<li>GCP: 기본 VPC가 이미 존재하며, 바로 리소스 생성 가능</li>
</ul>
<p><strong>IP 범위:</strong></p>
<ul>
<li>AWS: VPC 생성 시 CIDR 블록 지정 필수</li>
<li>GCP: 서브넷별로 IP 범위 지정, VPC 레벨에서는 명시하지 않음</li>
</ul>
<p><strong>연결성:</strong></p>
<ul>
<li>AWS: Internet Gateway, NAT Gateway를 명시적으로 구성</li>
<li>GCP: 기본적으로 라우팅이 단순화되어 있고, Cloud NAT 사용</li>
</ul>
<h3 id="방화벽-규칙-security-group과-다른-점">방화벽 규칙 (Security Group과 다른 점)</h3>
<p>AWS Security Group에 익숙하다면, GCP Firewall Rules는 약간 다르게 느껴질 것입니다.</p>
<p><strong>AWS Security Group:</strong></p>
<ul>
<li>인스턴스(ENI)에 직접 연결</li>
<li>Stateful: 응답 트래픽 자동 허용</li>
</ul>
<p><strong>GCP Firewall Rules:</strong></p>
<ul>
<li>VPC 레벨에서 관리</li>
<li>네트워크 태그 기반으로 타겟 지정</li>
<li>Stateful이 기본이지만, 명시적으로 규칙 작성</li>
<li>Ingress/Egress 규칙 분리</li>
</ul>
<p>VM에 웹 서버를 띄운다면,</p>
<ul>
<li><p>AWS: Security Group을 만들어 인스턴스에 붙이고, 80/443 포트 열기</p>
</li>
<li><p>GCP: VM에 &quot;web-server&quot; 태그 지정, Firewall Rule에서 &quot;web-server&quot; 태그를 가진 인스턴스에 80/443 허용</p>
</li>
</ul>
<h3 id="private-google-access란">Private Google Access란?</h3>
<p>GCP만의 독특한 기능으로, <strong>외부 IP 없이 구글 서비스에 접근</strong>할 수 있게 해줍니다.</p>
<p>AWS의 VPC Endpoint와 비슷한 개념이지만:</p>
<ul>
<li>AWS: S3, DynamoDB 등 서비스별로 Endpoint 생성 필요</li>
<li>GCP: Private Google Access를 서브넷에서 활성화하면, Cloud Storage, BigQuery 등 대부분의 구글 서비스에 자동 접근</li>
</ul>
<p>외부 IP가 없는 VM에서 Cloud Storage에 파일을 업로드해야 한다면, Private Google Access만 켜면 바로 가능합니다. 추가 구성이 거의 없습니다.</p>
<hr>
<h2 id="4-컴퓨팅-서비스">4. 컴퓨팅 서비스</h2>
<h3 id="compute-engine-ec2와-비교">Compute Engine (EC2와 비교)</h3>
<p><strong>Compute Engine</strong>은 GCP의 IaaS VM 서비스로, AWS EC2와 동일한 역할입니다.</p>
<h4 id="커스텀-머신-타입">커스텀 머신 타입</h4>
<p>AWS와의 가장 큰 차이는 <strong>머신 타입의 유연성</strong>입니다.</p>
<p><strong>AWS EC2:</strong></p>
<ul>
<li>정해진 인스턴스 타입 선택 (t3.medium, c5.large 등)</li>
<li>vCPU와 메모리 비율이 고정</li>
</ul>
<p><strong>GCP Compute Engine:</strong></p>
<ul>
<li>미리 정의된 머신 타입도 있지만</li>
<li><strong>커스텀 머신 타입</strong>: vCPU 개수와 메모리를 독립적으로 조절 가능</li>
<li>예: vCPU 4개 + 메모리 10GB 같은 조합 가능</li>
</ul>
<h3 id="컨테이너-실행-옵션">컨테이너 실행 옵션</h3>
<p>GCP는 컨테이너 실행에 있어 AWS보다 더 다양하고 유연한 옵션을 제공하고 있습니다.</p>
<h4 id="cloud-run">Cloud Run</h4>
<p><strong>Cloud Run</strong>은 GCP의 주력 서비스 중 하나로, AWS Fargate와 Lambda의 중간 지점입니다.</p>
<p><strong>특징:</strong></p>
<ul>
<li>컨테이너를 서버리스로 실행</li>
<li>HTTP/gRPC 엔드포인트 자동 생성</li>
<li>요청이 올 때만 실행되어 과금 (0→1, 1→0 자동 스케일)</li>
<li>인프라 관리 전혀 필요 없음</li>
</ul>
<p><strong>AWS 대비:</strong></p>
<ul>
<li>Fargate보다 설정이 훨씬 간단 (VPC, subnet, security group 등 불필요)</li>
<li>Lambda보다 유연 (최대 실행 시간, 메모리 제한이 덜 엄격)</li>
</ul>
<p><strong>예시:</strong></p>
<pre><code class="language-bash"># Docker 이미지만 있으면 한 줄로 배포
gcloud run deploy my-service --image gcr.io/my-project/my-image --platform managed</code></pre>
<h4 id="gke-google-kubernetes-engine">GKE (Google Kubernetes Engine)</h4>
<p>GKE는 AWS EKS에 해당하는 관리형 쿠버네티스입니다.</p>
<p>GKE의 두 가지 모드:</p>
<ol>
<li><p>Autopilot 모드</p>
<ul>
<li>노드 관리 완전 자동화</li>
<li>Pod 단위로만 과금</li>
<li>운영 부담 최소화</li>
<li>AWS에는 동급 서비스가 없음 (Fargate for EKS와 비슷하지만 더 강력)</li>
</ul>
</li>
<li><p>Standard 모드</p>
<ul>
<li>노드 풀 직접 관리</li>
<li>EKS와 유사한 경험</li>
<li>세밀한 제어 가능</li>
</ul>
</li>
</ol>
<h3 id="cloud-functions-lambda와-비교">Cloud Functions (Lambda와 비교)</h3>
<p><strong>Cloud Functions</strong>는 AWS Lambda와 거의 동일한 서버리스 함수 서비스입니다.</p>
<p><strong>유사점:</strong></p>
<ul>
<li>이벤트 기반 실행</li>
<li>자동 스케일</li>
<li>사용한 만큼 과금</li>
</ul>
<p><strong>차이점:</strong></p>
<ul>
<li>GCP는 2세대 Cloud Functions에서 더 긴 실행 시간 지원</li>
<li>Cloud Run과의 통합이 자연스러움 (실제로 2세대는 Cloud Run 기반)</li>
</ul>
<p><strong>선택 기준:</strong></p>
<ul>
<li>단순 함수 로직 → Cloud Functions</li>
<li>HTTP API나 더 긴 실행 시간 필요 → Cloud Run</li>
</ul>
<hr>
<h2 id="5-스토리지와-데이터베이스">5. 스토리지와 데이터베이스</h2>
<h3 id="cloud-storage-s3와-비교">Cloud Storage (S3와 비교)</h3>
<p>Cloud Storage는 GCP의 객체 스토리지로, AWS S3와 거의 동일한 역할입니다.</p>
<p><strong>유사점:</strong></p>
<ul>
<li>무제한 용량</li>
<li>버킷(Bucket) 개념</li>
<li>스토리지 클래스 (Standard, Nearline, Coldline, Archive)</li>
<li>버전 관리, 라이프사이클 정책</li>
</ul>
<p><strong>차이점:</strong></p>
<ol>
<li><p><strong>버킷 네이밍:</strong></p>
<ul>
<li>AWS S3: 리전 내에서 고유</li>
<li>GCP: 전역적으로 고유 (전 세계에서 유일한 이름)</li>
</ul>
</li>
<li><p><strong>리전 복제:</strong></p>
<ul>
<li>AWS: Cross-Region Replication 설정 필요</li>
<li>GCP: Multi-region, Dual-region 등 다양한 위치 옵션</li>
</ul>
</li>
<li><p><strong>접근 제어:</strong></p>
<ul>
<li>AWS: IAM + Bucket Policy + ACL</li>
<li>GCP: IAM 중심 (더 단순)</li>
</ul>
</li>
</ol>
<hr>
<h2 id="이외의-차이점">이외의 차이점</h2>
<h3 id="기본-설정-차이">기본 설정 차이</h3>
<h4 id="public-ip-할당">Public IP 할당</h4>
<ul>
<li><strong>AWS:</strong> 대부분의 경우 자동 할당 (VPC 설정에 따라)</li>
<li><strong>GCP:</strong> 명시적으로 &quot;외부 IP&quot; 옵션을 선택해야 함</li>
</ul>
<p>처음 VM을 만들 때 외부 접속이 안 된다면, 외부 IP를 할당했는지 확인해 봐야겠습니다.</p>
<h4 id="디스크-삭제">디스크 삭제</h4>
<ul>
<li><strong>AWS:</strong> 인스턴스 종료 시 루트 볼륨 자동 삭제 (기본값)</li>
<li><strong>GCP:</strong> 인스턴스 삭제 후에도 디스크 유지 (기본값)</li>
</ul>
<p>GCP에서 테스트용 VM을 만들고 삭제했는데, 디스크는 남아서 계속 과금되는 경우가 있을 것 같습니다... 인스턴스 삭제 시 &quot;디스크도 삭제&quot; 옵션을 체크하거나, 나중에 수동으로 삭제해야겠습니다.</p>
<h4 id="방화벽-기본값">방화벽 기본값</h4>
<ul>
<li><strong>AWS:</strong> Security Group이 기본적으로 모든 트래픽 차단</li>
<li><strong>GCP:</strong> 기본 VPC에는 일부 방화벽 규칙이 미리 설정되어 있음 (SSH, RDP, ICMP 등)</li>
</ul>
<h3 id="비용-모델-차이">비용 모델 차이</h3>
<h4 id="초-단위-과금">초 단위 과금</h4>
<ul>
<li><strong>AWS:</strong> 대부분 시간 단위 (일부 서비스는 초 단위)</li>
<li><strong>GCP:</strong> 모든 컴퓨팅 리소스가 초 단위 과금 (최소 1분)</li>
</ul>
<p>짧은 시간 동안만 리소스를 사용한다면 GCP가 유리한 것 같습니다.</p>
<h4 id="네트워크-비용">네트워크 비용</h4>
<ul>
<li><strong>AWS:</strong> 같은 리전 내 AZ 간 트래픽도 과금</li>
<li><strong>GCP:</strong> 같은 리전 내 zone 간 트래픽은 무료</li>
</ul>
<hr>
<h2 id="출처">출처</h2>
<p><a href="https://docs.cloud.google.com/resource-manager/docs?hl=ko">GCP Resource Manager 공식 문서</a>
<a href="https://cloud.google.com/vpc">GCP VPC 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Arduino] 스텝모터 라이브러리 비교: Stepper vs AccelStepper]]></title>
            <link>https://velog.io/@hyoin_0219/Arduino-%EC%8A%A4%ED%85%9D%EB%AA%A8%ED%84%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90-Stepper-vs-AccelStepper</link>
            <guid>https://velog.io/@hyoin_0219/Arduino-%EC%8A%A4%ED%85%9D%EB%AA%A8%ED%84%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B9%84%EA%B5%90-Stepper-vs-AccelStepper</guid>
            <pubDate>Sun, 14 Dec 2025 05:53:12 GMT</pubDate>
            <description><![CDATA[<p>IoT 프로젝트를 하면서 스텝모터를 제어해보다 보니 생각보다 복잡했습니다. 처음에는 다른 모듈처럼 라이브러리 선택하고 코드짜는 게 문제일 줄 알았는데, 정작 배선할 때 보니 드라이버도 있고, 라이브러리도 여러 가지였습니다.</p>
<p>그래서 이 글에서는 제가 알아본 두 가지 라이브러리의 차이점과, 왜 AccelStepper를 선택하게 됐는지 정리했습니다.</p>
<h2 id="스텝모터와-드라이버">스텝모터와 드라이버</h2>
<p>잠깐, 라이브러리 얘기 전에 드라이버부터 짚고 넘어가야 할 것 같습니다.</p>
<p>스텝모터는 정해진 방향으로 무한히 도는 모터입니다. 원리 자체가 강한 전자석 자기장을 만들어 그거에 맞춰서 회전하는 거죠. 그러다 보니 강한 전류가 필요합니다.
아두이노 MCU에서 드라이버 없이 직접 제어하려고 하면 당연히 전류 부족으로 모터가 제대로 안 돌거나, 최악의 경우 MCU에 손상을 입힙니다.</p>
<p>드라이버는 이런 문제들을 해결해줍니다:</p>
<ul>
<li><strong>고전류 공급</strong>: MCU는 논리 신호(5V, LOW/HIGH)만 보내고, 드라이버가 외부 전원에서 모터에 필요한 고전류를 공급합니다.</li>
<li><strong>정밀한 스텝 시퀀스 제어</strong>: MCU 신호를 정확한 펄스로 변환합니다.</li>
<li><strong>전압 최적화</strong>: 모터에 맞는 전압을 공급합니다.</li>
<li><strong>과전류/과열 보호</strong>: MCU와 시스템을 보호하는 회로가 내장되어 있습니다.</li>
</ul>
<p>이제 드라이버가 준비됐다면, 이 모터를 제어할 라이브러리를 골라야 합니다. 라이브러리를 사용할 때 핀을 설정해 줘야 하는데, 핀 정보는 드라이버와 연결되어 있는 핀이어야 합니다.</p>
<h2 id="stepper-vs-accelstepper-뭐가-다른가">Stepper vs AccelStepper: 뭐가 다른가?</h2>
<p>&quot;아두이노 스텝모터&quot;를 검색하면 가장 먼저 나오는 게 기본 <strong>Stepper 라이브러리</strong>입니다. 간단하고 바로 쓸 수 있어서 좋지만... 실제 프로젝트에 사용해보면 한계가 보입니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Stepper</th>
<th>AccelStepper</th>
</tr>
</thead>
<tbody><tr>
<td><strong>동작 방식</strong></td>
<td>블로킹(Blocking)</td>
<td>논블로킹(Non-blocking)</td>
</tr>
<tr>
<td><strong>속도 제어</strong></td>
<td>고정 속도만 가능</td>
<td>최대속도, 가속/감속 지원</td>
</tr>
<tr>
<td><strong>지원 모터</strong></td>
<td>주로 4선 스텝모터</td>
<td>2/3/4선 다양한 인터페이스</td>
</tr>
<tr>
<td><strong>모션 품질</strong></td>
<td>급격한 시작/정지</td>
<td>부드러운 가속/감속</td>
</tr>
</tbody></table>
<h3 id="블로킹-vs-논블로킹이-뭐길래-중요한가">블로킹 vs 논블로킹이 뭐길래 중요한가?</h3>
<p>이것이 두 라이브러리의 가장 큰 차이점인데, 실제로 코드를 짜보면 정말 중요합니다. 특히 여러 모듈이 상호작용하는 프로젝트일 수록, loop 주기에 딜레이가 최소화되어야 합니다.</p>
<p><strong>블로킹 방식 (Stepper)</strong>:</p>
<pre><code>모터 제어 함수 호출
  ↓
함수가 끝날 때까지 기다림
  ↓
다른 코드 실행 가능</code></pre><p>Stepper 라이브러리의 <code>step()</code> 함수를 호출하면, 정해진 스텝만큼 이동할 때까지 다른 코드를 실행할 수 없습니다. 만약 100스텝을 이동하는 데 1초가 걸린다면, 그 1초 동안 센서를 읽을 수도 없고, 다른 모터를 제어할 수도 없습니다. <strong>시스템의 주기성이 깨지기 쉽습니다.</strong></p>
<p><strong>논블로킹 방식 (AccelStepper)</strong>:</p>
<pre><code>제어 함수 호출
  ↓
함수 즉시 반환
  ↓
loop()에서 계속 run() 호출하면서 배경에서 동작</code></pre><p>AccelStepper는 <code>moveTo()</code>로 목표를 지정하고, <code>loop()</code>에서 계속 <code>run()</code>을 호출하기만 하면 됩니다. 그 사이에 센서도 읽고, LED도 깜빡이고, 다른 모터도 제어할 수 있습니다.</p>
<p><strong>실제 차이를 느낀 순간:</strong>
처음에 Stepper로 모터를 제어하다가, 동시에 다른 센서를 읽어야 하는 상황이 생겼습니다. 그때 모터가 움직이는 동안 센서 데이터가 밀렸고, 시스템이 예측 불가능하게 동작했습니다. AccelStepper로 바꾸니까 깔끔하게 해결됐습니다.</p>
<h2 id="accelstepper-라이브러리-제대로-쓰기">AccelStepper 라이브러리 제대로 쓰기</h2>
<p>AccelStepper를 선택했다면, 이제 어떻게 써야 할지 알아야 합니다.</p>
<h3 id="1-기본-설정-생성자--초기화">1. 기본 설정 (생성자 &amp; 초기화)</h3>
<pre><code class="language-cpp">// 28BYJ-48 스텝모터를 사용하는 경우
const int STEP_INT1 = 2;
const int STEP_INT2 = 3;
const int STEP_INT3 = 5;
const int STEP_INT4 = 6;

AccelStepper stepper(AccelStepper::FULL4WIRE, STEP_INT1, STEP_INT3, STEP_INT2, STEP_INT4);

void setup() {
  stepper.setMaxSpeed(1000);      // 최대 1000 step/s
  stepper.setAcceleration(500);   // 가속도 500 step/s²
  stepper.setCurrentPosition(0);  // 현재 위치를 0으로 초기화
}</code></pre>
<p><code>interface</code> 파라미터가 중요한데, 모터와 드라이버 방식에 따라 달라집니다.</p>
<h3 id="2-인터페이스-타입-가장-많이-쓰는-3가지">2. 인터페이스 타입 (가장 많이 쓰는 3가지)</h3>
<table>
<thead>
<tr>
<th>타입</th>
<th>숫자</th>
<th>모터/드라이버 유형</th>
<th>언제 쓸까?</th>
</tr>
</thead>
<tbody><tr>
<td><code>DRIVER</code></td>
<td>1</td>
<td>STEP/DIR 드라이버 (A4988, DRV8825 등)</td>
<td>스텝/방향 신호로 제어하는 드라이버</td>
</tr>
<tr>
<td><code>FULL4WIRE</code></td>
<td>4</td>
<td>4선 풀스텝 모터</td>
<td>강하고 빠른 회전이 필요할 때</td>
</tr>
<tr>
<td><code>HALF4WIRE</code></td>
<td>8</td>
<td>4선 하프스텝 (28BYJ-48 등)</td>
<td>정밀 제어가 필요할 때, 더 부드러운 움직임</td>
</tr>
</tbody></table>
<p><strong>추가 인터페이스:</strong></p>
<ul>
<li><code>FULL2WIRE</code> (2): 2선 풀스텝 모터</li>
<li><code>FULL3WIRE</code> (3): 3선 풀스텝 (HDD 스핀들 등)</li>
<li><code>HALF3WIRE</code> (6): 3선 하프스텝</li>
</ul>
<p>대부분은 <code>DRIVER</code> 또는 <code>HALF4WIRE</code>를 쓰는데, <strong>반드시 모터 데이터시트를 확인해야 합니다.</strong> 핀 개수나 순서가 맞지 않으면 모터가 경련하거나 역방향으로 돕니다.</p>
<h3 id="3-움직임-제어-함수">3. 움직임 제어 함수</h3>
<pre><code class="language-cpp">// 절대 위치로 이동 (가장 자주 쓰는 함수)
stepper.moveTo(2048);  // 2048 스텝 위치로 이동

// 상대적으로 이동
stepper.move(100);  // 현재 위치에서 100스텝만큼 더 이동

// loop()에서 주기적으로 호출 (핵심!)
stepper.run();  // 한 스텝 실행, 가속 프로파일 자동 적용</code></pre>
<h3 id="4-상태-확인">4. 상태 확인</h3>
<pre><code class="language-cpp">// 목표 위치에 도착했는지 확인
if (stepper.distanceToGo() == 0) {
  Serial.println(&quot;도착!&quot;);
  stepper.moveTo(0);  // 다시 처음 위치로 이동
}

// 현재 위치 확인
long pos = stepper.currentPosition();

// 현재 동작 중인지 확인
if (stepper.isRunning()) {
  Serial.println(&quot;움직이는 중...&quot;);
}</code></pre>
<h3 id="5-실제-사용-패턴">5. 실제 사용 패턴</h3>
<p>전 28BYJ-48 모터를 풀스텝으로 사용했습니다.</p>
<pre><code class="language-cpp">const int STEP_INT1 = 2;
const int STEP_INT2 = 3;
const int STEP_INT3 = 5;
const int STEP_INT4 = 6;

AccelStepper stepper(AccelStepper::FULL4WIRE, STEP_INT1, STEP_INT3, STEP_INT2, STEP_INT4);

void setup() {
  Serial.begin(9600);

  stepper.setMaxSpeed(1000);      // 최대 속도
  stepper.setAcceleration(500);   // 가속도
  stepper.setCurrentPosition(0);  // 처음 위치 설정
}

void loop() {
  // 도착하면 다음 목표 지정
  if (stepper.distanceToGo() == 0) {
    static int target = 2048;
    stepper.moveTo(target);
    target = (target == 2048) ? 0 : 2048;  // 왕복
  }

  // 핵심: loop()에서 계속 호출
  stepper.run();

  // 모터가 움직이면서 시리얼 출력
  Serial.println(stepper.currentPosition());
  delay(10);
}</code></pre>
<h2 id="주의할-점">주의할 점</h2>
<ol>
<li><p><strong>반드시 <code>run()</code>을 loop()에서 호출하세요</strong></p>
<ul>
<li>AccelStepper는 비동기로 동작하기 때문에 <code>run()</code>을 계속 호출해야 움직입니다.</li>
</ul>
</li>
<li><p><strong>데이터시트 확인은 필수입니다</strong></p>
<ul>
<li>핀 순서가 틀리면 모터가 &quot;떨림&quot; 증상을 보입니다. 모터가 경련하듯 움직이거나 특정 방향으로만 돈다면 핀 순서를 의심해보세요.</li>
</ul>
</li>
<li><p><strong><code>runToPosition()</code>은 블로킹입니다</strong></p>
<ul>
<li>빠른 테스트용으로는 좋지만, 프로젝트에서는 <code>moveTo() + run()</code>을 써야 논블로킹 방식으로 동작합니다.</li>
</ul>
</li>
<li><p><strong>가속/감속 값 조절</strong></p>
<ul>
<li>너무 큰 가속도는 모터가 따라가지 못합니다. 작은 값부터 시작해서 조정하는 것이 좋습니다.</li>
</ul>
</li>
</ol>
<h2 id="결론">결론</h2>
<p>Stepper 라이브러리는 간단한 프로젝트에는 충분하지만, 프로젝트가 조금만 복잡해지면 한계가 보입니다. AccelStepper는 처음엔 조금 더 복잡해 보이지만, 일단 익숙해지면 훨씬 더 강력하고 안정적입니다.
혹시 라이브러리 선택으로 고민하고 있다면, 그냥 AccelStepper로 시작하는 것을 추천합니다. Stepper 라이브러리와 크게 다르지도 않으니까요!</p>
<h3 id="출처">출처</h3>
<p><a href="https://www.airspayce.com/mikem/arduino/AccelStepper/index.html">AccelStep 공식문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 스마트홈 IoT 프로젝트]]></title>
            <link>https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%99%88-IoT-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%99%88-IoT-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Sun, 07 Dec 2025 12:38:23 GMT</pubDate>
            <description><![CDATA[<p>구현한 공동현관의 일부 기능 - 인가된 태그 인식:
<img src="https://velog.velcdn.com/images/hyoin_0219/post/e5a3a1db-1941-4eec-831a-7751ab2ab284/image.gif" alt="구현한 공동현관의 일부 기능 - 인가된 태그 인식"></p>
<h1 id="스마트홈-iot-프로젝트-회고-일주일의-소중한-경험들">스마트홈 IoT 프로젝트 회고: 일주일의 소중한 경험들</h1>
<h2 id="1-들어가며">1. 들어가며</h2>
<p>드디어 스마트홈 IoT 프로젝트가 끝났다! 기간은 고작 일주일이었지만, 정말 많은 것을 배우고 느낀 프로젝트였다. </p>
<p>이 프로젝트에서 나는 <strong>공동현관 기능</strong>을 맡았는데, 스테퍼 모터(28BYJ-48)로 슬라이딩 도어를 제어하고, RFID(MFRC522)로 태그를 인식하고, 초음파 센서로 장애물을 감지하는 부분을 담당했다. 그리고 Flask로 서버를 구축해서 PyQt6로 만든 대시보드와 HTTP 통신을 하는 부분도 구현했다.</p>
<p>아두이노(C++), Python(Flask, PyQt6), 그리고 RDS를 사용했고, 이 과정에서 예상하지 못한 문제들도 많았지만, 그만큼 배운 것도 정말 많았다. 그 이야기들을 나눠보려고 한다.</p>
<hr>
<h2 id="2-설계">2. 설계</h2>
<h3 id="프로토콜-설계">프로토콜 설계</h3>
<p>여러 개의 아두이노 보드가 Flask 서버로 시리얼 메시지를 계속 보내야 했다. 그런데 어떤 아두이노가 보낸 메시지인지, 그리고 그 메시지가 무엇을 의미하는지 구분할 방법이 필요했다. 특히 아두이노 보드 간의 통신도 필요했기에 중앙 제어 서버를 두는 걸 선택했다.</p>
<p>이 때문에 팀에서 함께 <strong>통신 프로토콜을 설계</strong>했는데, 메시지 형식의 기본 구조는 이렇게 했다:</p>
<pre><code>[DEVICE_ID],[DATA_TYPE],[VALUE]</code></pre><p>예를 들어 <code>MAIN,RFID_VALID,12A34B56</code> 이런 식이다. 각 아두이노의 ID, 어떤 센서의 데이터인지, 그리고 실제 값을 구분하는 것이다.</p>
<p>사실 실무에서는 바이트 개수를 기준으로 파싱한다고 하는데, 시간이 부족해서 빠르게 진행하기 위해 <strong>콤마를 구분자로 사용</strong>했다. 이건 나중에 개선하고 싶은 부분이다.</p>
<h3 id="아키텍처-설계-초반">아키텍처 설계: 초반</h3>
<p>처음 계획은 이렇게 단순했다:</p>
<ul>
<li>관제탑 역할을 하는 <strong>Python 프로세스 1개</strong>: 모든 아두이노로부터 시리얼 데이터를 받아서 처리</li>
<li>대시보드 <strong>Python 프로세스 1개</strong>: 사용자 인터페이스 담당</li>
</ul>
<p>&quot;두 프로세스가 같은 포트(시리얼 포트)를 공유할 수 있지 않을까?&quot; 라는 뇌피셜로 시작했다(사실 타이트한 일정이라 제발 됐으면 좋겠다 라는 생각이었다) 
하지만 당연하게도... 한 프로세스가 포트를 점유하면 다른 프로세스는 접근할 수 없었다. 😅</p>
<hr>
<h2 id="3-기능-구현">3. 기능 구현</h2>
<h3 id="공동현관-모듈-제어">공동현관 모듈 제어</h3>
<p>공동현관이 제대로 작동하려면 세 가지 센서가 조화롭게 움직여야 했다.</p>
<p><strong>RFID 태그 인식:</strong></p>
<ul>
<li>인가된 태그를 인식하면 → 도어 오픈</li>
<li>비인가된 태그를 인식하면 → 아무것도 안 함</li>
<li>태그 UID를 대시보드에 표시</li>
</ul>
<p><strong>스테퍼 모터로 도어 제어:</strong></p>
<ul>
<li>RFID가 유효한 태그를 감지하면 28BYJ-48 모터가 작동해서 슬라이딩 도어를 열어준다.</li>
</ul>
<p><strong>초음파 센서로 안전성 확보:</strong></p>
<ul>
<li>문이 열려있을 때 초음파 센서가 장애물을 감지하면 → 문이 닫혀서는 안 된다!</li>
<li>이건 사람이나 물체가 끼일 수 있으니까 중요한 부분이었다.</li>
</ul>
<p>다만 아쉬웠던 점이 하나 있다. 지금은 태그의 UID를 그대로 대시보드에 표시하는데, 실제 서비스라면 사용자 이름이나 라벨이 나와야 할 텐데 시간 관계상 못했다. 다음엔 데이터베이스에서 태그 ID를 조회해서 실제 사용자 정보를 표시해야겠다.</p>
<h3 id="대시보드와-flask-서버-http-통신">대시보드와 Flask 서버: HTTP 통신</h3>
<p>처음엔 관제탑 역할의 Python 프로세스가 시리얼 통신만 담당했다. 하지만 <strong>포트 점유 문제</strong>가 터지면서 급하게 리팩토링이 필요했다.</p>
<p><strong>해결책: Flask 서버로 전환</strong></p>
<ul>
<li>관제탑 Python 프로세스 → <strong>Flask 서버</strong>로 리팩토링</li>
<li>아두이노에서 보내는 시리얼 데이터를 수신하고 처리</li>
<li>대시보드는 <strong>HTTP 요청</strong>으로 Flask 서버와 통신</li>
</ul>
<p>대시보드에서는 이제 이렇게 통신한다:</p>
<pre><code>GET/POST http://localhost:5000/door/control</code></pre><p>Flask 서버가 요청을 받으면 아두이노로 명령을 내려준다.</p>
<p><strong>대시보드 상태 표시:</strong></p>
<ul>
<li>&quot;공동현관 열림&quot; / &quot;공동현관 닫힘&quot; 같은 상태를 실시간으로 표시</li>
<li>수동으로 열기 버튼도 있어서 대시보드에서 직접 제어 가능</li>
</ul>
<h3 id="모듈-간-제어-공동현관과-엘리베이터">모듈 간 제어: 공동현관과 엘리베이터</h3>
<p>흥미로운 부분이었다. 공동현관이 열림 상태가 되면, <strong>자동으로 엘리베이터도 호출</strong>되도록 했다. 이건 Flask 서버를 통해서 다른 아두이노 보드에 명령을 전달하는 식으로 구현했다.</p>
<hr>
<h2 id="4-예상-밖의-문제들-그리고-배운-점">4. 예상 밖의 문제들 (그리고 배운 점)</h2>
<h3 id="아두이노-보드가-터지다">아두이노 보드가 터지다</h3>
<p>프로젝트 초반, 나는 <strong>큰 실수</strong>를 했다. 아두이노 보드를 항상 노트북에 연결시켜 뒀다.</p>
<p>컴퓨터를 끄면 USB에 연결된 기기들도 전기가 끊긴다고 생각했다... 근데 그게 아니었다. 컴퓨터를 끄고 나서도 아두이노 보드는 계속 전원을 받고 있었다. 스테퍼 모터 드라이버, RFID 모듈까지 달아둔 상태로 말이다.</p>
<p>며칠 동안 계속 전원을 받던 보드의 온도가 올라가거나 하는 신호는 없었다. 그래서 과부하가 있다는 걸 전혀 몰랐다.</p>
<p><strong>통합 테스트 전</strong>: 발표 전, 대시보드까지 합쳐서 통합 테스트를 하려고 보드를 켰는데... 먹통이 되어버렸다. 컴퓨터에서도 인식을 못 했다. </p>
<p>아두이노 내부 회로가 손상된 거다. 여러 개의 전자 부품들이 계속 전력을 소비하다 보니 보드 자체가 과부하를 견디지 못한 거다. (거의 일주일 동안 끊임없이 돌렸으니 그럴만도 하다...)</p>
<p><strong>다행히:</strong> 팀 친구들이 여분의 아두이노 보드를 빌려줘서 발표는 무사히 진행할 수 있었다. </p>
<h3 id="포트-점유-문제와-급발진-리팩토링">포트 점유 문제와 급발진 리팩토링</h3>
<p>앞서 언급했던 포트 점유 문제로 돌아와서.</p>
<p>처음엔 &quot;관제탑 Python 프로세스&quot;와 &quot;대시보드 Python 프로세스&quot; 두 개를 동시에 실행하려고 했다. 근데 시리얼 포트는 한 번에 하나의 프로세스만 점유할 수 있다.</p>
<p><strong>문제를 깨닫던 순간:</strong></p>
<pre><code>serial port is busy</code></pre><p>이 에러 메시지를 본 순간 내 얼굴은 파랗게 질렸다. 🫠</p>
<p><strong>해결 과정:</strong></p>
<ol>
<li>급하게 관제탑 역할을 Flask 서버로 리팩토링</li>
<li>대시보드는 Flask 서버와 HTTP 통신으로 변경</li>
<li>아두이노는 Flask 서버에만 시리얼 연결</li>
</ol>
<p><strong>운이 좋았던 이유:</strong>
처음부터 <strong>프로토콜을 잘 정의해뒀기</strong> 때문에, 아두이노 쪽 코드는 수정할 필요가 거의 없었다. 대시보드에서 시리얼로 처리하던 것들을 그대로 서버에 보내면 됐다.</p>
<p>이 경험에서 <strong>설계가 얼마나 중요한지</strong>를 뼈저리게 깨달았다. 미리 잘 정의해둔 프로토콜 덕분에, 예상 밖의 문제가 터졌을 때도 빠르게 대응할 수 있었다.</p>
<hr>
<h2 id="5-아쉬웠던-점과-앞으로-개선할-것">5. 아쉬웠던 점과 앞으로 개선할 것</h2>
<h3 id="1-태그-uid-대신-사용자-정보-표시하기">1. 태그 UID 대신 사용자 정보 표시하기</h3>
<p>지금: <code>12A34B56</code> 같은 UID가 그대로 표시됨
개선: 데이터베이스에서 태그 ID를 조회해서 <code>김철수</code> 같은 실제 사용자명 표시</p>
<p>RDS를 이미 연결했으니 구현이 어렵지 않을 텐데, 시간이 부족했다. 다음 버전에서 꼭 개선해야겠다.</p>
<h3 id="2-바이트-기반-파싱으로-업그레이드하기">2. 바이트 기반 파싱으로 업그레이드하기</h3>
<p>지금: 콤마를 구분자로 파싱 (<code>DEVICE_ID,DATA_TYPE,VALUE</code>)
실무: 바이트 개수를 기준으로 고정된 형식으로 파싱</p>
<p>바이트 기반 파싱이 더 안정적이고 효율적이라고 들었다. 이것도 다음 프로젝트의 과제로 삼아야겠다.</p>
<h3 id="3-아두이노-보드-관리-쉬는-시간-주기">3. 아두이노 보드 관리 (쉬는 시간 주기)</h3>
<p>이건... 기술적 개선이라기보단 휴먼 에러 예방이다.</p>
<ul>
<li>프로젝트 기간 중에도 규칙적으로 보드를 쉬게 하기</li>
<li>필요 없을 땐 항상 USB를 뽑기</li>
<li>보드의 온도나 상태를 수시로 확인하기 (사실 뜨거워졌는데 내가 인식을 못했던 것일 수도 있으니...)</li>
</ul>
<h3 id="4-아키텍처-설계의-중요성-다시-한-번">4. 아키텍처 설계의 중요성 다시 한 번</h3>
<p>이 프로젝트에서 배운 가장 중요한 교훈은 이거다:</p>
<p><strong>&quot;좋은 설계가 있으면, 예상 밖의 문제가 터져도 빨리 해결할 수 있다.&quot;</strong></p>
<p>프로토콜을 미리 잘 정의했기 때문에, 아키텍처를 완전히 바꿔야 할 상황이 와도 빠르게 대응할 수 있었다. 반대로 설계를 대충 했다면, 발표 전날의 그 난리는 정말 감당할 수 없었을 거다.</p>
<hr>
<h2 id="6-마치며">6. 마치며</h2>
<p>일주일이라는 짧은 기간이었지만, 정말 밀도 있는 시간이었다.</p>
<p><strong>이 프로젝트에서 배운 가장 큰 것:</strong></p>
<ul>
<li>기술만큼 설계가 중요하다.</li>
<li>예상 밖의 문제는 항상 생긴다.</li>
<li>하지만 좋은 기초가 있으면 빠르게 극복할 수 있다.</li>
</ul>
<p>다음 IoT 프로젝트를 할 땐:</p>
<ol>
<li>더 견고한 설계로 시작하기</li>
<li>하드웨어 소중히 다루기 (특히 보드...)</li>
<li>충분한 테스트 시간 확보하기</li>
<li>배운 교훈들을 적용해서 더 완성도 있는 프로젝트 만들기</li>
</ol>
<p>이 경험이 앞으로의 IoT 프로젝트에 큰 밑거름이 될 거라고 믿는다. 그리고 그때는 아두이노 보드도 편안하게 쉬게 해줄 거다. </p>
<p><a href="https://github.com/addinedu-ros-11th/iot-repo-3">프로젝트 소스코드는 여기!</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 서울시 상권 데이터로 요식업 업종/상권 비교하기]]></title>
            <link>https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%84%9C%EC%9A%B8%EC%8B%9C-%EC%83%81%EA%B6%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EC%97%85%EC%A2%85%EB%B3%84-%EB%A7%A4%EC%B6%9C-%EB%B6%84%EC%84%9D-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hyoin_0219/%ED%9A%8C%EA%B3%A0-%EC%84%9C%EC%9A%B8%EC%8B%9C-%EC%83%81%EA%B6%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EC%97%85%EC%A2%85%EB%B3%84-%EB%A7%A4%EC%B6%9C-%EB%B6%84%EC%84%9D-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 27 Nov 2025 08:42:20 GMT</pubDate>
            <description><![CDATA[<p>부트캠프에서의 첫 프로젝트가 끝났습니다! 사실 끝난지는 좀 됐지만(이제 2번째 프로젝트 시작할 정도로 시간이 지났습니다) 이제라도 회고를 작성해보네요.
이번 프로젝트의 큰 주제는 EDA였습니다. 데이터를 직접 수집하든 이미 만들어진 데이터를 사용하든 해서, 데이터의 흐름과 특징을 파악하는 게 과제였습니다.</p>
<h2 id="프로젝트-소개">프로젝트 소개</h2>
<p>&quot;어떤 상권에서 창업하면 좋을까?&quot; 예비 창업자라면 누구나 한 번쯤 고민하는 질문입니다. 이번 프로젝트에서는 서울시 공공데이터를 활용해 상권별, 업종별 매출 패턴을 분석하고, 시간대와 요일에 따른 특성을 시각화한 대시보드를 만들었습니다.</p>
<p>추천을 위해 ai 모델을 사용하기보다는 데이터의 추세와 패턴을 명확하게 보여주는 것에 집중했습니다. 실제 창업을 고려하는 사람들이 직관적으로 데이터를 탐색하고, 자신에게 맞는 상권을 찾을 수 있도록 돕는 것이 목표였습니다.</p>
<hr>
<h2 id="데이터-개요">데이터 개요</h2>
<p>서울시 열린데이터광장에서 제공하는 2024년 분기별 공공데이터를 활용했습니다. 다음과 같은 데이터셋을 사용했습니다.</p>
<h3 id="사용한-데이터셋">사용한 데이터셋</h3>
<p><strong>상권 기본정보</strong></p>
<ul>
<li>영역-상권: 서울시 1,650개 상권 (골목상권, 발달상권, 전통시장, 관광특구)</li>
<li>점포-상권: 상권별 업종별 점포 수</li>
</ul>
<p><strong>추정매출</strong></p>
<ul>
<li>시간대별/요일별/연령별/성별 매출 데이터</li>
<li>업종별 매출 세분화</li>
</ul>
<p><strong>유동인구</strong></p>
<ul>
<li>길단위 유동인구</li>
<li>상주인구</li>
<li>직장인구</li>
</ul>
<p><strong>소득 데이터</strong></p>
<ul>
<li>행정동 단위 월평균소득</li>
<li>구매력 지표 (총지출, 음식지출 등)</li>
</ul>
<p>다른 팀들이 데이터 수집과 크롤링에 시간을 많이 쏟는 동안, 저희 팀은 이미 정제되어 있는 공공데이터를 활용할 수 있었습니다. 이 덕분에 시간적 여유가 생겨 심층 분석과 대시보드 구현에 집중할 수 있었고, 최종적으로 다른 팀들과 달리 실제 작동하는 대시보드까지 완성할 수 있었습니다.</p>
<h2 id="데이터-전처리">데이터 전처리</h2>
<p>여러 데이터셋을 상권 단위로 통합하는 작업이 필요했습니다. 주요 전처리 과정은 다음과 같습니다.</p>
<p><strong>통합 작업</strong></p>
<ul>
<li>상권, 업종, 점포수, 매출, 인구, 소득 데이터를 상권명 기준으로 병합</li>
<li>행정동 단위 소득 데이터를 상권 단위로 매핑</li>
<li>상권명 정규화</li>
</ul>
<p>통합 작업은 미리 ERD를 그리고, ERD 테이블 정보에 맞춰서 데이터를 DB에 저장했습니다</p>
<p><strong>집계 및 파생변수 생성</strong></p>
<ul>
<li>상권별 총 매출, 점포당 매출 계산</li>
<li>시간대별 (점심/저녁/심야), 요일별 (주중/주말) 매출 집계</li>
<li>연령대별, 성별 매출 분포 계산</li>
<li>유동인구 패턴 (시간대별, 요일별) 계산</li>
</ul>
<hr>
<h2 id="분석-내용">분석 내용</h2>
<h3 id="1-상권-기본-정보-분석">1. 상권 기본 정보 분석</h3>
<p>서울시 1,650개 상권의 업종 분포를 분석한 결과, 상권마다 뚜렷한 특징이 나타났습니다.</p>
<p><strong>주요 발견</strong></p>
<ul>
<li>한식음식점 비중이 평균 40%로 압도적으로 높음</li>
<li>이태원: 양식업 점포 비중 30% 초과</li>
<li>홍대, 신촌 등 유흥지역: 호프-간이주점 20% 초과</li>
<li>연남동, 가산디지털단지, 문정역: 커피-음료 업종 밀집 (직장인/감성카페)</li>
</ul>
<p>상권별 점포 분포 지도와 업종별 점포 비중 그래프를 통해 이러한 특성을 시각화했습니다.</p>
<h3 id="2-매출-정보-분석">2. 매출 정보 분석</h3>
<p>총매출과 점포당 매출을 함께 분석하여 유망 상권을 선별했습니다.</p>
<p><strong>분석 방법</strong></p>
<ul>
<li>점포당 매출 = 총매출 ÷ 점포수 (최소 점포수 30개 이상 상권만 대상)</li>
<li>총매출 vs 점포당 매출의 상관관계 분석</li>
</ul>
<p><strong>결과</strong></p>
<ul>
<li>상위 매출 지역은 점포수 분포와 대체로 유사</li>
<li>총매출과 점포당 매출의 순위 일관성 높음 (Pearson r = 0.553, Spearman ρ = 0.863)</li>
<li>두 지표를 모두 고려하면 실질적으로 유망한 상권 선별 가능</li>
</ul>
<p><strong>시간대별 매출 패턴</strong></p>
<ul>
<li>주중/주말, 점심/저녁/심야 매출 패턴 확인</li>
<li>관광특구 (명동, 여의도)와 직장 밀집지 (가산디지털단지, 강남역)의 특징적 패턴 발견</li>
</ul>
<p><strong>성별·연령대별 매출</strong></p>
<ul>
<li>남성: 한식 &gt; 호프-간이주점 &gt; 커피-음료</li>
<li>여성: 한식 &gt; 커피-음료 &gt; 호프-간이주점</li>
<li>10-20대: 홍대, 건대, 연남동 상권에서 높음</li>
<li>30-60대: 명동, 종로, 여의도 상권에서 높음</li>
</ul>
<h3 id="3-인구와-매출-상관관계">3. 인구와 매출 상관관계</h3>
<p>인구 유형별로 매출에 미치는 영향을 분석했습니다.</p>
<p><strong>영향력 순서</strong>
유동인구 &gt;&gt;&gt; 직장인구 &gt;&gt; 상주인구</p>
<p>직장인구와 유동인구가 집중된 상권일수록 매출이 높았습니다.</p>
<p><strong>유동인구 패턴</strong></p>
<ul>
<li>U자형: 망리단길 등 (출퇴근 시간대 피크)</li>
<li>역U자형: 명동, 강남역 등 (점심-저녁 시간대 피크)</li>
</ul>
<p><strong>요일별 변화</strong></p>
<ul>
<li>직장 밀집 상권: 주말 매출 하락</li>
<li>주거형 상권: 주말 매출 소폭 상승</li>
</ul>
<h3 id="4-외식업-업종별-특성">4. 외식업 업종별 특성</h3>
<p>업종별로 요일과 시간대에 따른 매출 패턴이 뚜렷하게 구분되었습니다.</p>
<p><strong>요일별 특성</strong></p>
<ul>
<li>한식음식점: 일요일 매출 가장 낮음</li>
<li>금·토 매출 상승: 호프-간이주점, 양식음식점, 패스트푸드</li>
</ul>
<p><strong>시간대별 특성</strong></p>
<ul>
<li>한식: 모든 시간대에서 높은 매출 유지</li>
<li>커피-음료: 점심 시간대 피크</li>
<li>호프-간이주점: 21-24시, 새벽까지 높은 매출</li>
</ul>
<h3 id="5-소득지출과-매출-상관관계">5. 소득/지출과 매출 상관관계</h3>
<p>행정동 단위 소득 및 지출 데이터를 상권 매출과 비교 분석했습니다.</p>
<p><strong>상관관계 분석 결과</strong></p>
<ul>
<li>동 평균 소득 ↔ 상권 매출: <strong>약함</strong> (Pearson r = 0.132, Spearman ρ = 0.152)</li>
<li>총지출 ↔ 총매출: <strong>강함</strong> (r = 0.692, ρ = 0.755)</li>
<li>음식지출 ↔ 총매출: <strong>매우 강함</strong> (r = 0.841, ρ = 0.836)</li>
</ul>
<p><strong>시사점</strong>
입지 평가 시 소득보다는 총지출과 음식지출을 중심으로 시장 규모를 평가하는 것이 효과적입니다.</p>
<h3 id="6-개폐업율과-경쟁강도">6. 개폐업율과 경쟁강도</h3>
<p>상권의 경쟁 환경이 매출에 미치는 영향을 분석했습니다.</p>
<p><strong>분석 결과</strong></p>
<ul>
<li>개업률/폐업률 ↔ 매출: 거의 상관 없음</li>
<li>경쟁강도 (유사점포수, 전체점포수) ↔ 매출: <strong>강한 양의 상관</strong> (r ≈ 0.75)</li>
</ul>
<p><strong>해석</strong>
수요가 큰 상권일수록 점포가 집중되어 있으며, 오히려 매출 효율이 향상되는 경향을 보였습니다. 다만 입점 시 차별화와 포지셔닝이 필수적입니다.</p>
<h3 id="7-창업-유망-상권-도출">7. 창업 유망 상권 도출</h3>
<p>시간대별 특화 상권을 분석하여 Top 20 상권을 도출했습니다.</p>
<p><strong>시간 구분</strong></p>
<ul>
<li>점심: 11-14시</li>
<li>저녁: 17-21시</li>
<li>심야+새벽: 21-24시 + 00-06시</li>
</ul>
<p>주중 점심, 주중 저녁, 심야, 주말 각 시간대별로 매출이 높은 상권을 선별하여, 대시보드에서 업종별로 확인할 수 있도록 구현했습니다.</p>
<h2 id="streamlit-대시보드-구현">Streamlit 대시보드 구현</h2>
<p>분석 결과를 직관적으로 탐색할 수 있도록 Streamlit으로 대시보드를 구현했습니다.</p>
<h3 id="주요-기능">주요 기능</h3>
<p><strong>상권 탐색 기능</strong></p>
<ul>
<li>상권 선택 필터</li>
<li>업종별 매출 비교 차트</li>
<li>시간대별/요일별 매출 패턴 시각화</li>
<li>상권의 시간대별/요일별 유동인구 시각화</li>
</ul>
<p><strong>카테고리별 유망 상권 비교</strong></p>
<ul>
<li>시간대별 매출이 높은 상권 Top</li>
<li>점포 수 비교</li>
</ul>
<p>대시보드는 Streamlit Cloud에 배포하여 팀원들과 다른 학생들이 자유롭게 접근할 수 있도록 했습니다.</p>
<hr>
<h2 id="분석-과정에서-마주한-도전">분석 과정에서 마주한 도전</h2>
<h3 id="한식-음식점-데이터-편향">한식 음식점 데이터 편향</h3>
<p>데이터를 탐색하면서 예상치 못한 문제를 발견했습니다. 업종 카테고리 중 <strong>&#39;한식 음식점&#39;의 비중이 압도적으로 높아서</strong>, 매출액을 비교할 때마다 한식이 다른 업종들을 가려버리는 것이었습니다.</p>
<p>발표 자료를 준비하면서 카페, 편의점, 치킨집 등 다른 업종들의 상권별 차이를 보여주고 싶었는데, 한식을 포함하면 그래프에서 패턴을 읽기가 어려웠습니다. 결국 분석 목적에 따라 <strong>한식 음식점을 제외한 그래프를 따로 그려서</strong> 인사이트를 도출했습니다.</p>
<p>이상적으로는 대시보드에 &quot;한식 제외&quot; 필터 기능을 넣었으면 좋았겠지만, 프로젝트 기간 내에는 분석과 발표 준비에 집중했습니다.</p>
<p><strong>교훈</strong></p>
<ul>
<li>데이터의 컬럼뿐만 아니라, 분포까지 먼저 확인하고 분석 전략을 수립해야 함</li>
<li>극단값이나 편향된 카테고리가 있을 때는 별도 처리 필요</li>
</ul>
<h3 id="공공데이터의-한계">공공데이터의 한계</h3>
<p>프로젝트를 진행하면서 아쉬웠던 점은 서울시에서 제공하는 데이터 자체가 <strong>이미 어느 정도 가공되어 있었다</strong>는 점입니다. 원본 raw 데이터에 접근할 수 없어서, 우리가 원하는 방식으로 세밀하게 분석하기에는 한계가 있었습니다.</p>
<p>예를 들어 매출 데이터는 추정치로 제공되었고, 정확한 산출 방식이 명시되지 않아 해석에 주의가 필요했습니다. 또한 일부 상권은 데이터가 누락되어 있거나 업데이트가 늦는 경우도 있었습니다.</p>
<p><strong>시사점</strong>
공공데이터를 활용할 때는 데이터가 어떤 기준으로 가공되었는지, 어떤 정보가 제외되었는지를 먼저 확인하는 것이 중요합니다. 이는 분석 결과를 해석할 때 반드시 고려해야 할 사항입니다.</p>
<hr>
<h2 id="프로젝트-성공-요인">프로젝트 성공 요인</h2>
<p>이번 프로젝트가 순조롭게 진행될 수 있었던 이유를 돌이켜보면 크게 세 가지였습니다.</p>
<h3 id="1-명확한-주제-설정">1. 명확한 주제 설정</h3>
<p><strong>서울시 상권, 음식점</strong>이라는 구체적이고 적당한 범위의 주제 덕분에 프로젝트 방향성이 흔들리지 않았습니다. 너무 크지도, 작지도 않은 스코프였기에 팀원 모두가 목표를 명확히 이해하고 작업할 수 있었습니다.</p>
<h3 id="2-기존-데이터-활용">2. 기존 데이터 활용</h3>
<p>다른 팀들이 데이터 수집과 전처리에 시간을 쏟는 동안, 우리는 <strong>서울 열린데이터광장의 정제된 데이터를 활용</strong>해 분석과 대시보드 구현에 집중할 수 있었습니다. 이 시간 여유가 대시보드까지 완성하고 심층 분석을 수행할 수 있었던 핵심이었습니다.</p>
<h3 id="3-일일-회의를-통한-긴밀한-협업">3. 일일 회의를 통한 긴밀한 협업</h3>
<p><strong>거의 매일 팀 회의를 가지며</strong> 진행 상황과 분석 결과를 공유했습니다. 덕분에 문제를 빠르게 발견하고, 방향을 조정할 수 있었습니다. 짧더라도 자주 소통하는 것이 프로젝트 성공의 중요한 요소였습니다.</p>
<h2 id="마치며">마치며</h2>
<p>이번 프로젝트는 복잡한 모델링 없이도 <strong>데이터의 추세와 패턴을 효과적으로 보여줄 수 있다</strong>는 것을 확인한 경험이었습니다. 또한 공공데이터 활용의 장점과 한계를 동시에 체험할 수 있었고, 팀 협업의 중요성을 다시 한번 느낄 수 있었습니다.</p>
<p>데이터 분석을 해보니 <strong>데이터를 제대로 이해하고, 적절한 전처리를 수행하며, 인사이트를 명확하게 전달하는 것</strong>이 더 중요하다는 것을 배웠습니다.</p>
<hr>
<p><strong>프로젝트 정보</strong></p>
<ul>
<li><strong>기간</strong>: 2024년 3분기</li>
<li><strong>팀 구성</strong>: 3명</li>
<li><strong>기술 스택</strong>: Python, Pandas, Streamlit, Plotly</li>
<li><strong>GitHub 저장소</strong>: <a href="https://github.com/addinedu-ros-11th/eda-repo-4">[addinedu-ros-11th
eda-repo-4]</a></li>
</ul>
<p><strong>관련 포스트</strong></p>
<ul>
<li><a href="https://velog.io/@hyoin_0219/Streamlit-streamlit-cloud%EB%B0%B0%ED%8F%AC-%EC%A4%91-import-%EC%97%90%EB%9F%AC">Streamlit Cloud 배포 중 트러블 슈팅</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Arduino] 일일 프로젝트 회고 - 엘리베이터 호출 구현]]></title>
            <link>https://velog.io/@hyoin_0219/Arduino-%EC%9D%BC%EC%9D%BC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%97%98%EB%A6%AC%EB%B2%A0%EC%9D%B4%ED%84%B0-%ED%98%B8%EC%B6%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@hyoin_0219/Arduino-%EC%9D%BC%EC%9D%BC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%97%98%EB%A6%AC%EB%B2%A0%EC%9D%B4%ED%84%B0-%ED%98%B8%EC%B6%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 23 Nov 2025 04:04:00 GMT</pubDate>
            <description><![CDATA[<p>학원에서 아두이노 일일 프로젝트로 엘리베이터 호출을 그대로 구현하는 프로젝트를 진행했다.</p>
<h1 id="프로젝트-소개">프로젝트 소개</h1>
<h2 id="user-requirements">User Requirements</h2>
<ul>
<li><p>오른쪽 초록 불빛은 호출 상태를 나타낸다. 호출을 한다면 불빛이 커지고, 엘베가 도착하면 불빛을 꺼야 한다.</p>
</li>
<li><p>왼쪽 빨간/노란 불빛은 엘베 위치를 나타낸다. 천천히 올라가는 걸 시각화 해야 한다. 엘베가 도착하면 해지해야 한다. 엘리베이터는 초록 불빛이 있는 곳(층)에서 멈춰야 한다. </p>
</li>
<li><p>스위치는 층에 있는 엘리베이터 버튼이다. 호출 취소도 나름의 방법으로 구현해야 한다. (나는 스위치를 2번 누르면 호출 취소로 구현했다)</p>
</li>
</ul>
<h2 id="system-requirements">System Requirements</h2>
<h3 id="sr_01-호출-기능">SR_01 호출 기능</h3>
<ul>
<li><p>각 층의 엘리베이터 호출 버튼을 누르면, 엘리베이터가 해당 층으로 이동한다. * 각 층의 엘리베이터 호출 버튼은 동시에 누를 수 있다.</p>
<h3 id="sr_02-호출-취소-기능">SR_02 호출 취소 기능</h3>
</li>
<li><p>호출 이후, 한 번 더 호출 버튼을 누르면 호출이 취소된다.</p>
<h3 id="sr_03-호출상태-표시-기능">SR_03 호출상태 표시 기능</h3>
</li>
<li><p>각 층의 엘리베이터 호출 버튼을 누르면 호출상태 LED (초록)이 켜진다.</p>
</li>
<li><p>엘리베이터가 도착하거나 호출이 취소되면 호출상태 LED는 꺼진다.</p>
<h3 id="sr_04-엘리베이터-이동">SR_04 엘리베이터 이동</h3>
</li>
<li><p>엘리베이터 시스템이 시작될 땐 1층에서 시작한다.</p>
</li>
<li><p>대기 중 호출이 발생하면 해당 층으로 이동한다.</p>
</li>
<li><p>대기 중 호출이 동시에 발생하면, 엘리베이터 위치에서 가까운 순서로 우선순위를 정한다.</p>
</li>
<li><p>이동 중 다른 호출이 발생하고, 대기 상태가 아닌 경우엔 다음의 규칙을 따른다:</p>
<ul>
<li>이동 방향 외에 위치한다면, 목적지로 설정</li>
<li>이동 방향 내에 위치한다면, 경유지로 설정</li>
</ul>
</li>
<li><p>이동 중, 호출이 취소되면 다음의 규칙을 따른다:</p>
<ul>
<li>호출 횟수가 큰 층에서 대기한다.</li>
<li>호출 횟수가 동일하다면, 가장 가까운 층에서 대기한다.</li>
</ul>
</li>
<li><p>대기 중인 경우, 다음 규칙을 따른다:</p>
<ul>
<li>기본적으로 호출 횟수가 큰 층에서 대기한다.</li>
<li>호출 횟수가 모두 동일하다면, 대기 중인 층에서 대기한다.</li>
<li>대기 중인 경우 호출이 들어온다면, 더이상 이동하지 않고 바로 호출 위치로 이동한다.</li>
</ul>
</li>
<li><p>엘리베이터는 각 층 사이에서 대기할 수 없다.</p>
<h3 id="sr_05-엘리베이터-위치-표시-기능">SR_05 엘리베이터 위치 표시 기능</h3>
</li>
<li><p>엘리베이터의 현재 위치에 해당하는 LED가 켜진다.</p>
</li>
<li><p>엘리베이터가 이동한 뒤, 이전 LED는 꺼져야 한다.</p>
</li>
<li><p>엘리베이터의 이동 속도는 1초로 한다.</p>
</li>
</ul>
<h2 id="하드웨어-구조">하드웨어 구조</h2>
<p><img src="https://velog.velcdn.com/images/hyoin_0219/post/b8744745-1a78-4186-b739-4c38d10f9823/image.png" alt=""></p>
<h2 id="loop-함수-구성">loop 함수 구성</h2>
<p>러프하게 그렸던 Flow chart다.</p>
<ul>
<li>다음 타겟 위치:<code>last_target_floor</code></li>
<li>호출을 기다리는 층:<code>up_request_list</code>, <code>down_request_list</code></li>
<li>엘리베이터 이동 방향: <code>cur_elev_mode</code></li>
<li>핀 정보</li>
</ul>
<p>위 변수들은 전역으로 선언한 뒤, 여러 함수들에서 전역 변수를 수정하는 방식으로 구현했다.</p>
<p>사용되는 중심 함수는 이렇다:</p>
<ul>
<li>엘리베이터 위치/이동 방향, 다음 타겟 결정: <code>find_next_floor_with_dir()</code>,<code>find_next_floor_with_idle()</code></li>
<li>엘리베이터가 이동하는 led 설정: <code>write_elev_led()</code></li>
<li>유일한 input인 스위치 값 읽어오기, 층 led 설정: <code>check_elevator_call()</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/hyoin_0219/post/c0aecb94-de96-4295-a673-67a47264aa72/image.png" alt=""></p>
<h1 id="프로젝트-후기">프로젝트 후기</h1>
<p>하루만 집중하면 끝날 줄 알았는데 생각보다 시간을 많이 썼다. 실제론 이틀 정도 집중해야 했다.</p>
<h2 id="겪었던-어려움">겪었던 어려움</h2>
<h3 id="1-설계의-모호함">1. 설계의 모호함</h3>
<p>지금까지의 경험상, 설계를 탄탄하게 하고 구현하는 게 훨씬 효율적이었다. 
특히 여러 변수가 상호작용하는 이번 프로젝트는 더더욱 그랬다.</p>
<p>테스트케이스를 설계 단계에서 미리 검증했어야 했는데, 
&quot;일단 구현하고 보자&quot;는 마음이 앞서서 설계를 대충 넘어갔다. 
그 결과 loop 함수에서 예상과 다르게 동작하는 함수들이 생겼다.</p>
<p><strong>교훈</strong>: IoT 개발에서는 <strong>탑다운 방식</strong>으로 접근해야겠다. 
loop 함수의 전체 구조를 먼저 설계하고, 그 다음 세부 함수로 내려가자.</p>
<h3 id="2-자유로운-만큼-어려운-컨벤션">2. 자유로운 만큼 어려운 컨벤션</h3>
<p>스프링이나 장고 같은 프레임워크는 정해진 컨벤션이 있어서 
그것만 따르면 &quot;일단 돌아갈 것&quot;이라는 믿음이 있었다. 
디버깅 도구도 잘 되어있고...</p>
<p>하지만 아두이노는 너무 자유로워서 오히려 혼란스러웠다. 
어디서 버그가 생긴 건지 찾기도 어렵고, 내가 짠 코드가 맞는 건지 확신이 서지 않았다.
디버깅이 시리얼로만 가능한 것도 답답한 점이었다.</p>
<h2 id="잘한-점">잘한 점</h2>
<h3 id="tdd-적용">TDD 적용</h3>
<p>이번 프로젝트에서 처음으로 테스트케이스를 먼저 작성하고 
구현하는 방식을 시도해봤다.</p>
<p>확실히 효과가 있었다! 테스트를 통과한 함수들은 
loop에 통합했을 때도 안정적으로 작동했다.</p>
<p>그런데 이번엔 내가 따로 함수에 값을 넣어보며 직접 테스트해본 것이다. 앞으로 있을 팀 프로젝트에선 테스트 툴을 이용해야 할 것 같다. </p>
<h2 id="느낀-점">느낀 점</h2>
<p>IoT 개발이 웹 개발과는 또 다른 재미가 있다는 걸 느꼈다. 
LED가 실제로 켜지고 엘리베이터가 움직이는 걸 보니 
성취감이 더 컸던 것 같다.</p>
<p>복잡한 상태 관리와 우선순위 로직을 직접 구현하면서 
알고리즘적 사고의 중요성도 다시 한번 깨달았다.</p>
<p>시간이 더 있었다면 실제 엘리베이터처럼 내부 버튼(목적지 선택)도 
구현해보고 싶었다. 여러 대의 엘리베이터가 있는 시스템으로 
확장하는 것도 재미있을 것 같다.</p>
<h2 id="다음-프로젝트를-위한-다짐">다음 프로젝트를 위한 다짐</h2>
<ol>
<li><strong>설계부터 탄탄하게</strong>: 상태 다이어그램을 먼저 그리고 시작하기 + 테스트케이스가 통과할지 눈으로 1차 파악하기</li>
<li><strong>TDD 습관화</strong>: 테스트케이스를 먼저 작성하는 습관 들이기</li>
<li><strong>탑다운 접근</strong>: 전체 구조 → 세부 구현 순서로 진행하기</li>
</ol>
<p>일일 프로젝트였지만 많은 걸 배운 시간이었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MCU] RFID를 이용한 태그 인식]]></title>
            <link>https://velog.io/@hyoin_0219/MCU-RFID%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%83%9C%EA%B7%B8-%EC%9D%B8%EC%8B%9D</link>
            <guid>https://velog.io/@hyoin_0219/MCU-RFID%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%83%9C%EA%B7%B8-%EC%9D%B8%EC%8B%9D</guid>
            <pubDate>Sun, 16 Nov 2025 11:53:56 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 <strong>RFID</strong> 기술과 <strong>MCU(마이크로컨트롤러)</strong>에서 RFID를 다루는 방법을 정리해 봤다. 
특히, <strong>MFRC522 모듈</strong>을 이용해 RFID 태그를 읽고 쓰는 실습을 중심으로 작성했다.</p>
<hr>
<h2 id="1-rfid란">1. RFID란?</h2>
<p>RFID(Radio-Frequency IDentification)는 <strong>주파수를 이용해 ID를 식별</strong>하는 기술이다.
RFID 시스템은 크게 <strong>리더(Reader)</strong>와 <strong>태그(Tag)</strong>로 구성된다.</p>
<ul>
<li>리더: 태그의 ID를 읽고 데이터를 주고받는 장치  </li>
<li>태그: 리더가 인식할 수 있는 고유 ID와 데이터를 담고 있는 소형 칩  </li>
</ul>
<p>태그를 리더에 딱 맞춰서 붙일 필요는 없다. 비스듬히 놓여도 인식이 되지만, 너무 각도가 심하면 UID를 못 읽는 경우가 있다.</p>
<hr>
<h2 id="2-rfid-태그-구조와-메모리">2. RFID 태그 구조와 메모리</h2>
<p>태그에는 <strong>UID(Unique ID)</strong>와 <strong>사용자 데이터</strong>를 저장할 수 있는 메모리가 있다.
MFRC522 기준 메모리 구조는 아래와 같다:</p>
<ul>
<li><strong>한 Sector</strong>: 4개의 Block  </li>
<li><strong>한 Block</strong>: 16 Byte  </li>
<li>총 16 Sector → 16 × 4 = 64 Block  </li>
<li><strong>보안 영역</strong>: Key A/B 없으면 읽기/쓰기 불가  </li>
<li>UID는 첫 번째 섹터 첫 번째 블록에 저장되어 변경 불가  </li>
</ul>
<blockquote>
<p>사용자는 <strong>나머지 블록</strong>에 데이터를 저장하거나 읽을 수 있다. 데이터 읽기/쓰기 전에는 반드시 인증 과정을 거쳐야 한다.</p>
</blockquote>
<p>MFRC522의 메모리 정리:</p>
<table>
<thead>
<tr>
<th>Sector</th>
<th>Block</th>
<th>인증 필요</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>0</td>
<td>X</td>
<td>UID 저장 블록, 읽기만 가능</td>
</tr>
<tr>
<td>0</td>
<td>1~3</td>
<td>O</td>
<td>Key A/B 필요, 보안 관련 데이터</td>
</tr>
<tr>
<td>1~15</td>
<td>0~3</td>
<td>O</td>
<td>Key A/B 필요, 사용자 지정 데이터</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-mcu와-rfid-통신-spi">3. MCU와 RFID 통신: SPI</h2>
<p>RFID 리더 모듈은 많은 데이터를 빠르게 주고받아야 하기 때문에 <strong>SPI(Serial Peripheral Interface)</strong> 통신을 사용한다.</p>
<h3 id="spi-기본-구조">SPI 기본 구조</h3>
<table>
<thead>
<tr>
<th>핀</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>MOSI</td>
<td>MCU → 모듈 데이터 전송</td>
</tr>
<tr>
<td>MISO</td>
<td>모듈 → MCU 데이터 전송</td>
</tr>
<tr>
<td>SCK</td>
<td>클록 신호, 데이터 타이밍 맞춤</td>
</tr>
<tr>
<td>SS</td>
<td>통신할 슬레이브 선택</td>
</tr>
</tbody></table>
<p>그리고 MFRC522에는 <strong>RST 핀</strong>이 있어 리셋과 통신 초기화를 담당한다.</p>
<hr>
<h2 id="4-mfrc522-실습">4. MFRC522 실습</h2>
<h3 id="핀-연결">핀 연결</h3>
<p>MFRC522 모듈과 Arduino Uno 연결 예시:</p>
<ul>
<li>RST: 9  </li>
<li>SS: 10  </li>
<li>MOSI: 11  </li>
<li>MISO: 12  </li>
<li>SCK: 13  </li>
<li>3.3V: Vin 핀  </li>
</ul>
<blockquote>
<p><strong>참고:</strong> RST와 SS 외에는 모듈 내부적으로 핀이 이미 정해져 있으므로 변경할 필요가 없다. Arduino Uno가 아니라면 Arduino IDE에서 지정된 핀을 확인해야 한다.</p>
</blockquote>
<h3 id="클래스-사용법-mfrc522">클래스 사용법: MFRC522</h3>
<p>여기에서 언급되는 클래스는 Arduino IDE에 있는 MFRC522.h에 정의된 클래스를 말한다. </p>
<ol>
<li><strong>모듈 초기화</strong></li>
</ol>
<pre><code class="language-cpp">#include &lt;MFRC522.h&gt;

const int RST_PIN = 9;
const int SS_PIN = 10;
MFRC522 rc522(SS_PIN, RST_PIN);</code></pre>
<p>위에서 언급한 대로, 사용자가 직접 변경할 수 있는 핀은 SS, RST다 핀이다. 그러니 클래스에서 설정해 줘야 한다.</p>
<ol start="2">
<li><p><strong>태그 감지 및 UID 읽기</strong></p>
<pre><code class="language-cpp">if (!rc522.PICC_IsNewCardPresent()) return;
if (!rc522.PICC_ReadCardSerial()) return;</code></pre>
<p>각각 새로운 카드가 존재하는지 여부, UID가 있는지를 확인하는 함수다. 이 코드가 있어야 MFRC522를 인식할 수 있다.</p>
</li>
<li><p><strong>블록 인증</strong></p>
<pre><code class="language-cpp">status = rc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, index, &amp;key, &amp;(rc522.uid));
if (status != MFRC522::STATUS_OK) 
{
 Serial.print(&quot;Authentication Failed: &quot;);
 Serial.println(rc522.GetStatusCodeName(status));
 return;
}</code></pre>
<p>write/read 함수를 쓰려면 인증이 필수적이다. index 위치의 key를 지정해주자.
참고로 따로 key를 설정하지 않았으면, key 값은 0xFF다.</p>
</li>
</ol>
<ol start="4">
<li><p><strong>데이터 쓰기</strong></p>
<pre><code class="language-cpp">status = rc522.MIFARE_Write(index, (byte*)&amp;data, length);
if (status != MFRC522::STATUS_OK) 
{
 Serial.print(&quot;Write Failed: &quot;);
 Serial.println(rc522.GetStatusCodeName(status));
 return;
}</code></pre>
<p>index 위치에서 length만큼 byte 단위로 작성한다. 아두이노 우노의 경우, 한 Block이 16Byte니 16byte로 설정했다.</p>
</li>
<li><p><strong>데이터 읽기</strong></p>
<pre><code class="language-cpp">byte buffer[18];
byte length = 18;
</code></pre>
</li>
</ol>
<p>status = rc522.MIFARE_Read(index, buffer, &amp;length);</p>
<p>if (status != MFRC522::STATUS_OK)
{
    Serial.print(&quot;Read Failed: &quot;);
    Serial.println(rc522.GetStatusCodeName(status));
}</p>
<pre><code>index 블록에서 length 만큼의 데이터를 읽어 buffer에 저장한다. 
일반적으로 buffer는 체크섬/CRC 때문에 length보다 2~3Byte 더 크게 잡기에 18로 length를 지정했다.


## 5. 실습 시 주의사항
* 태그를 너무 세게 밀거나 각도가 심하면 인식 실패 가능
* 데이터 읽기/쓰기 전 반드시 인증 필요
* SPI 속도, 전원 연결 상태, 모듈 초기화 여부 체크</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[아두이노] Chrome Dino 게임 자동화 🦖]]></title>
            <link>https://velog.io/@hyoin_0219/%EC%95%84%EB%91%90%EC%9D%B4%EB%85%B8-Chrome-Dino-%EA%B2%8C%EC%9E%84-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@hyoin_0219/%EC%95%84%EB%91%90%EC%9D%B4%EB%85%B8-Chrome-Dino-%EA%B2%8C%EC%9E%84-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Sun, 09 Nov 2025 06:58:22 GMT</pubDate>
            <description><![CDATA[<p>크롬 오프라인 시 나오는 공룡게임을 대신 플레이해주는 아두이노 장치를 만들어 봤다.
크롬에서 <code>chrome://dino/</code> 를 검색하면 나온다.</p>
<p><strong>기술 스택:</strong> Arduino Uno, 조도 센서, 서보 모터</p>
<hr>
<h2 id="1-공룡게임-분석">1. 공룡게임 분석</h2>
<p>먼저, 공룡게임의 기본 동작을 관찰했다.</p>
<ul>
<li>장애물을 피해 최대한 오래 달리는 것이 목표다.  </li>
<li>장애물은 <strong>나무</strong>와 <strong>새</strong>, 두 종류다.  </li>
<li>나무는 바닥에 고정되어 있고, 새는 세 가지 높이(바닥/ 몸/ 머리 위)로 날아온다.  </li>
<li>중요한 점은, <strong>모든 장애물은 점프만으로 피할 수 있다.</strong><br>몸쪽으로 오는 새도 점프 타이밍, 조도 센서 위치만 잘 맞추면 통과 가능하다.  </li>
<li>약 <strong>700점</strong>이 넘어가면 게임의 색상이 반전되어 <strong>밤 모드</strong>로 전환된다.</li>
</ul>
<p>이 관찰 결과, “빛의 변화로 장애물을 인식하고, 일정 조건에서 점프(스페이스바)를 트리거하면 된다”는 결론을 얻었다.</p>
<hr>
<h2 id="2-구현-방법">2. 구현 방법</h2>
<h3 id="1-장애물-인식">1. 장애물 인식</h3>
<p>조도 센서를 이용해 빛의 밝기를 지속적으로 측정한다.<br>게임 화면에서 장애물이 등장하면 화면이 순간적으로 바뀌기 때문에, 조도값에 차이가 발생하면 이를 장애물 등장으로 판단한다.</p>
<h3 id="2-공룡-조작">2. 공룡 조작</h3>
<p>서보모터를 이용해 점프 동작을 구현했다.<br>기본적으로 서보모터의 축이 일정 각도로 회전하도록 설정했고,<br>장애물이 감지되면 모터가 빠르게 회전 → 스페이스바 입력으로 이어지는 구조다.</p>
<h3 id="3-하드웨어-구조">3. 하드웨어 구조</h3>
<p><img src="https://velog.velcdn.com/images/hyoin_0219/post/282f1ab7-cb85-4f49-b2bd-caf4a721df68/image.png" alt="">
조도 센서는 모니터에 밀착시켜야 하므로 따로 전선을 빼뒀다.</p>
<hr>
<h2 id="3-시행착오">3. 시행착오</h2>
<h3 id="1-조도-센서-연결-문제">1. 조도 센서 연결 문제</h3>
<p>위치를 조정하다가 조도 센서 선이 끊기는 일이 많았다.<br>절연테이프로 고정해봤지만 접촉 불량이 잦았다... 계속 조심히 다뤄야 했다.</p>
<h3 id="2-인식-범위-조정">2. 인식 범위 조정</h3>
<p>센서를 화면에 너무 밀착시키면 시야가 좁아져서 나무와 새를 각각 인식하지 못했다.<br>화면에서 약간 띄워 설치하니 훨씬 안정적으로 인식됐다.</p>
<h3 id="3-하드웨어-튜닝의-어려움">3. 하드웨어 튜닝의 어려움</h3>
<p>결국 가장 많은 시간을 쓴 건 코드가 아니라 하드웨어 조정이었다....
센서 각도, 밝기 민감도, 서보모터의 반응속도 같은 물리적 요소를 잡는 데 꽤 애를 먹었다.</p>
<hr>
<h2 id="4-소스-코드">4. 소스 코드</h2>
<p>처음에 생각해 뒀던 &#39;밝기 변화로 장애물 인식&#39;, &#39;서보 모터 조정&#39;을 구현했다. 
위 구상만으로 실제 실행을 해보면, 센서 값이 일정치 않은 문제 때문에 장애물이 없는 상황(점프를 뛰어서 공룡이 공중에 있는 상황)에서도 서보모터가 동작하곤 했다. 그래서 쿨다운 시간을 추가했다.</p>
<pre><code class="language-cpp">#include &lt;Arduino.h&gt;
#include &lt;Servo.h&gt;

const int LIGHT_PIN = A0;
const int DELTA_THRESHOLD = 80;
const unsigned long COOLDOWN_MS = 200;

Servo servo;
int pos = 0;
int prev_light = 0;
bool prev_is_tree = false;
unsigned long last_trigger_time = 0;

void setup() {
  Serial.begin(9600);
  servo.attach(9);
  servo.write(0);

  prev_light = analogRead(LIGHT_PIN);
}

void loop() {
  unsigned long now_time = millis();
  int light = analogRead(LIGHT_PIN);
  int diff = abs(light - prev_light);

  bool is_tree = (diff &gt; DELTA_THRESHOLD);

  if (is_tree &amp;&amp; !prev_is_tree &amp;&amp; (now_time - last_trigger_time &gt; COOLDOWN_MS)) {
    Serial.println(&quot;Detected!!&quot;);
    pos += 45;
    servo.write(pos);
    delay(100);
    pos -= 45;
    servo.write(pos);

    last_trigger_time = now_time;
  }

  prev_is_tree = is_tree;
  prev_light = light;

  delay(10);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MCU 개발] PlatformIO IDE를 써보세요]]></title>
            <link>https://velog.io/@hyoin_0219/MCU-%EA%B0%9C%EB%B0%9C-PlatformIO-IDE%EB%A5%BC-%EC%8D%A8%EB%B3%B4%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@hyoin_0219/MCU-%EA%B0%9C%EB%B0%9C-PlatformIO-IDE%EB%A5%BC-%EC%8D%A8%EB%B3%B4%EC%84%B8%EC%9A%94</guid>
            <pubDate>Sun, 02 Nov 2025 07:59:18 GMT</pubDate>
            <description><![CDATA[<p>웹개발쟁이한테 Arduino IDE, STM32Cube 등의 MCU 전용 IDE는 너무 불편했다</p>
<p>부트캠프에서 IoT 프로젝트를 진행하고 있다. 막상 코드를 작성하다 보니 Arduino IDE가 너무 불편하다는 걸 느꼈다.
예전에 사용했던 STM32Cube가 오히려 선녀였을 정도로...!! 정말정말 불편했다.</p>
<p>Arduino IDE가 불편했던 점은 세 가지였다:
      1.    Git 연동이 안 된다.
      2.    자동완성이나 함수로 바로가기 기능이 거의 없다.
      3.    여러 파일을 나눠서 관리하기가 정말 불편하다.</p>
<p>나는 웹 프로그래밍 위주의 편한 IDE로 공부해왔던 터라, 이런 비직관적인 환경이 더 답답하게 느껴졌다.
학생이라 비싼 상용 IDE를 쓰기도 부담스러워서, 결국 VSCode의 PlatformIO IDE 확장 프로그램으로 갈아탔다.</p>
<hr>
<h2 id="platformio로-갈아타기">PlatformIO로 갈아타기</h2>
<p>설치는 간단하다.
VSCode에서 PlatformIO IDE 확장 프로그램을 설치한 뒤, 보드와 프레임워크만 선택해주면 된다.
<img src="https://velog.velcdn.com/images/hyoin_0219/post/98b03f8d-d496-4fd9-a1b2-6a707a07c6c9/image.png" alt=""></p>
<p>보드도 Arduino, STM32 보드를 포함해 총 1500건 이상의 보드가 세팅되어 있다. </p>
<h3 id="정상-설치가-안-된-경우-pio-명령어가-인식이-안-되는-경우">정상 설치가 안 된 경우 (pio 명령어가 인식이 안 되는 경우)</h3>
<p>집 컴퓨터에 확장 프로그램을 설치하고 프로젝트를 만들어 보니까 렉이 꽤 걸렸다. 혹시 문제가 생길까 불안했는데 아니나다를까 프로젝트에서 Arduino.h 가 인식이 안 됐다. 내가 해온 시도들을 정리해 봤다:</p>
<ol>
<li><p>PlatformIO IDE 설치 확인하기 (pio 명령어)</p>
<pre><code>pio --version</code></pre><p>정상 설치가 된 경우, pio 명령어가 자동으로 사용이 되어야 한다. 인식을 못하면 설치가 잘못된 것이다.</p>
</li>
<li><p>설치가 안 된 경우, 따로 설치하기 (나는 python 가상환경 위에다가 설치했다)</p>
<pre><code>pip install -U platformio</code></pre></li>
</ol>
<p>일단 여기까지면 셋팅은 완료된다!</p>
<hr>
<h2 id="platformio-ide의-장점">PlatformIO IDE의 장점</h2>
<ol>
<li><p><strong>Git 연동 완벽 지원</strong>
VSCode 기반이라 Git 연동이 당연히 된다.
코드 버전 관리, 커밋, 브랜치 전환 모두 가능하다!</p>
</li>
<li><p><strong>자동완성과 바로가기가 직관적</strong>
함수, 변수 이름 자동완성은 물론이고
Ctrl+클릭으로 선언부로 바로 이동도 된다.</p>
</li>
<li><p><strong>여러 파일 관리가 편하다</strong>
이게 진짜 최고 장점이다.
Arduino IDE는 <code>.ino</code> 파일 하나에 전부 코드를 몰아넣기 때문에, 규모가 조금만 커져도 정리하기가 힘들다. 한 창에서 여러 파일을 한꺼번에 보기 어려운 것도 단점이었다.
PlatformIO는 폴더 구조 자체가 프로젝트 단위로 잡혀 있어서 <code>src/</code>, <code>lib/</code>, <code>include/</code> 디렉터리로 깔끔하게 나뉜다.</p>
</li>
</ol>
<hr>
<h2 id="주의할-점--라이브러리-관리">주의할 점 — 라이브러리 관리</h2>
<p>다만 한 가지 차이점이 있다.
Arduino IDE는 자주 쓰는 기본 라이브러리(Servo.h, Wire.h, SPI.h 등)가 이미 포함되어 있어서 따로 추가할 필요가 없다.
하지만 PlatformIO는 직접 의존성을 명시해야 한다.</p>
<p>예를 들어, Servo 라이브러리를 쓰고 싶다면
platformio.ini 파일에 이렇게 적어줘야 한다.</p>
<pre><code>[env:uno]
platform = atmelavr
board = uno
framework = arduino
lib_deps =
    arduino-libraries/Servo</code></pre><p>이렇게만 설정하면 PlatformIO가 자동으로 설치해준다.</p>
<h3 id="설치가-안-되는-경우">설치가 안 되는 경우</h3>
<p>나 같은 경우엔, 중간에 PlatformIO IDE 자체가 잘못 설치 됐어서 그런 건지, 위 방법으로 Servo 라이브러리를 추가해도 include가 안 됐다.</p>
<ol>
<li><p>설치된 (사용 가능한) 라이브러리 확인하기</p>
<pre><code>pio lib list</code></pre></li>
<li><p>라이브러리 설치하기</p>
<pre><code>pio lib install &quot; {라이브러리 이름} &quot;
pio lib install &quot;arduino-libraries/Servo@1.2.1&quot;</code></pre></li>
<li><p>빌드 및 클린</p>
<pre><code>pio run --target clean</code></pre></li>
</ol>
<hr>
<h2 id="결론">결론</h2>
<p>처음엔 단순히 “IDE가 불편해서” 갈아탔지만, 막상 써보니까 PlatformIO는 단순한 대체품이 아니라 진짜 개발 환경이었다. Git 연동, 자동완성, 폴더 구조까지 전부 만족한다
라이브러리 추가 정도의 불편함은 감수할 만한 리스크다. 스프링 생각하면 크게 낯설지도 않다!</p>
<hr>
<h3 id="출처">출처</h3>
<p><a href="https://docs.platformio.org/en/latest/core/quickstart.html#setting-up-the-project">PlatformIO IDE 공식 문서 - Quick start</a>
<a href="https://docs.platformio.org/en/latest/core/userguide/lib/cmd_list.html">PlatformIO IDE 공식 문서 - pio lib 명령어</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Streamlit] streamlit cloud 배포 중 import 에러]]></title>
            <link>https://velog.io/@hyoin_0219/Streamlit-streamlit-cloud%EB%B0%B0%ED%8F%AC-%EC%A4%91-import-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@hyoin_0219/Streamlit-streamlit-cloud%EB%B0%B0%ED%8F%AC-%EC%A4%91-import-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Sun, 26 Oct 2025 11:16:34 GMT</pubDate>
            <description><![CDATA[<p>Streamlit은 간단하게 구현하고 배포할 수 있는 웹 대시보드 라이브러리입니다.<br>부트캠프에서 데이터 분석 프로젝트를 짧게 진행했습니다. 상권 분석 데이터를 기반으로, 창업 위치와 업종을 추천하는 웹 대시보드를 만들었습니다.  </p>
<p>발표할 때 학생들이 바로 웹에 접근할 수 있도록 Streamlit Cloud로 빠르게 배포했는데, import 에러를 발견했습니다. 다른 라이브러리는 잘 import되지만, 유독 mysqlclient만 에러가 발생했습니다.</p>
<h2 id="문제-상황">문제 상황</h2>
<ul>
<li>환경: Python, Streamlit Cloud, MySQL</li>
<li>증상: Streamlit Cloud로 배포하던 중, mysqlclient만 설치가 안 되는 문제 발생</li>
</ul>
<h2 id="원인">원인</h2>
<p>mysqlclient는 C 기반의 클라이언트를 파이썬 인터페이스로 확장한 라이브러리입니다. 설치할 때 C 언어의 소스를 빌드해야 합니다.
그러나 Streamlit Cloud는 파이썬 환경이고, OS 레벨은 막혀있습니다. 파이썬 환경인 만큼 C 컴파일러가 없으니(설치도 불가능), C 기반인 mysqlclient는 설치가 안 된 것이죠.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>C 기반의 라이브러리가 안 된다면 <strong>순수 Python 라이브러리</strong>를 사용하면 됩니다:</p>
<ol>
<li><code>mysql-connector-python</code></li>
<li><code>PyMySQL</code> </li>
</ol>
<h3 id="추가">추가</h3>
<p>mysqlclient 뿐만 아니라, mysql 도 설치가 안 됩니다. mysql 라이브러리는 mysqlclient 라이브러리에 의존하기 때문이죠.</p>
<h3 id="회고">회고</h3>
<p>지금까지 배포는 Docker로 이미지를 만들고 인스턴스에서 실행하는 방식으로 진행했습니다. docker 이미지가 얼마나 소중한 기능인지 삽질 2시간으로 확실히 느꼈습니다...</p>
<h3 id="출처">출처</h3>
<ul>
<li><a href="https://docs.streamlit.io/deploy/streamlit-community-cloud/deploy-your-app/app-dependencies">Stack Overflow: What&#39;s the difference between MySQLdb, mysqlclient and MySQL connector/Python?</a></li>
<li><a href="https://stackoverflow.com/questions/43102442/whats-the-difference-between-mysqldb-mysqlclient-and-mysql-connector-python?utm_source=chatgpt.com">Streamlit 공식 문서</a></li>
<li><a href="https://discuss.streamlit.io/t/problem-installing-mysqlclient-can-not-find-valid-pkg-config-name/47108/9">Streamlit Discussion</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>