Skip to main content
Summary

조각들이 하나의 로봇이 되는 순간

자율주행 로봇을 처음 배울 때는 모든 것이 따로 움직이는 것처럼 보인다. LiDAR는 /scan을 발행하고, 바퀴 엔코더는 /odom을 만들고, URDF는 로봇 모양을 설명하고, SLAM은 지도를 만들고, Nav2는 목표 지점까지 이동하라고 명령한다. RViz는 그 모든 것을 화면에 그려 준다. 처음에는 이것들이 서로 다른 세계의 부품처럼 느껴진다.

하지만 완성된 이동 로봇 시스템에서 이 부품들은 하나의 파이프라인으로 이어진다. 센서는 세상을 보고, 오도메트리는 로봇이 얼마나 움직였는지 추정하며, TF는 모든 좌표계를 연결한다. SLAM은 낯선 공간을 지도로 만들고, AMCL은 저장된 지도 위에서 현재 위치를 추정한다. Nav2는 지도와 센서 데이터를 바탕으로 경로를 계획하고, 장애물을 피해 속도 명령을 만든다. 마지막으로 하드웨어 제어 노드는 /cmd_vel을 받아 바퀴를 실제로 돌린다.

ROS는 로봇 애플리케이션을 만들기 위한 소프트웨어 라이브러리와 도구의 집합이며, 드라이버부터 알고리즘, 개발 도구까지 연결하는 기반을 제공한다.1 ROS2는 여기서 한 걸음 더 나아가 현대적인 로봇 시스템, 제품 수준의 이동 로봇, 다중 로봇, 산업 현장과 연구 환경을 염두에 두고 설계된 프레임워크로 발전했다.2 그래서 우리가 만든 자율주행 로봇도 단순히 예제 몇 개를 실행한 결과가 아니다. 여러 노드가 느슨하게 연결되어 있지만, 데이터의 약속은 엄격하게 지키는 하나의 분산 시스템이다.

완성된 로봇을 가장 단순한 문장으로 표현하면 이렇다.

로봇은 센서로 세상을 읽고, TF로 자신과 세상의 위치 관계를 정리하고, 지도 위에서 자기 위치를 추정한 뒤, Nav2를 통해 안전한 속도 명령을 생성한다.

이 한 문장을 실제 ROS2 시스템으로 풀면 다음과 같은 흐름이 된다.

LiDAR / Camera / IMU / Encoder
        ↓
Sensor Driver Nodes
        ↓
/scan, /image, /imu, /joint_states, /odom
        ↓
TF + robot_state_publisher + odometry
        ↓
SLAM 또는 AMCL
        ↓
/map, map → odom, odom → base_link
        ↓
Nav2 Planner / Controller / Costmap / Behavior Tree
        ↓
/cmd_vel
        ↓
Motor Driver / ros2_control / MCU
        ↓
Robot Motion

이 흐름이 끊기지 않으면 로봇은 움직인다. 어느 한 곳이라도 끊기면 로봇은 멈추거나, 엉뚱한 곳으로 가거나, RViz 안에서만 멋지게 움직인다. 이제 중요한 것은 "무엇을 배웠는가"가 아니라 "어떻게 하나로 묶을 것인가"다.

자율주행 로봇 시스템 완성

자율주행 파이프라인 다시 그리기

완성된 이동 로봇은 여러 계층으로 나누어 바라보는 것이 좋다. 계층을 나누면 문제가 생겼을 때 어디를 의심해야 하는지 빠르게 판단할 수 있다.

가장 아래에는 하드웨어 계층이 있다. 모터, 배터리, 모터 드라이버, 엔코더, LiDAR, IMU, 카메라가 여기에 속한다. 이 계층은 물리적인 세계와 직접 만난다. 전원이 불안정하면 센서 값이 튀고, 바퀴가 미끄러지면 오도메트리가 틀어진다. 자율주행 문제처럼 보이던 것이 사실은 느슨한 케이블이나 낮은 배터리 전압 때문인 경우도 많다.

그 위에는 드라이버 계층이 있다. LiDAR 드라이버는 /scan을 발행하고, 카메라 드라이버는 /image를 발행한다. 모터 제어 노드는 /cmd_vel을 받아 바퀴 속도로 변환한다. 엔코더를 읽는 노드는 바퀴 회전량을 바탕으로 /odom을 만든다. 이 계층의 핵심은 "하드웨어의 언어를 ROS2 메시지로 바꾸는 것"이다.

그다음은 상태 표현 계층이다. robot_state_publisher는 URDF 또는 xacro로 정의한 로봇 모델을 읽고, 링크와 조인트 사이의 관계를 TF로 발행한다. 오도메트리 노드는 odom → base_link 변환을 만든다. SLAM이나 AMCL은 map → odom 변환을 만든다. 결과적으로 map → odom → base_link → laser_link 같은 좌표계 사슬이 완성된다.

그 위에는 인식과 위치 추정 계층이 있다. SLAM은 /scan과 /odom, TF를 이용해 지도를 만들고 로봇의 위치를 추정한다. 이미 지도가 있다면 AMCL이 /map, /scan, /odom, TF를 이용해 지도 위의 현재 위치를 추정한다. 이 계층의 결과가 안정적이지 않으면 Nav2는 아무리 잘 설정해도 좋은 경로를 만들 수 없다.

마지막으로 계획과 제어 계층이 있다. Nav2는 지도, costmap, 현재 위치, 목표 위치를 바탕으로 전역 경로를 만들고, 지역 장애물을 피해 속도 명령을 생성한다. Nav2는 단순히 A에서 B로 가는 기능만 제공하는 것이 아니라, 위치 추정, 경로 계획, 제어, 장애물 표현, 행동 트리 기반의 복구 동작까지 포함하는 이동 로봇 내비게이션 프레임워크다.3

이 계층들을 하나의 표로 정리하면 다음과 같다.

계층주요 구성 요소대표 토픽·TF실패하면 나타나는 현상
하드웨어모터, LiDAR, IMU, 엔코더전원, USB, 시리얼, CAN센서 끊김, 모터 무응답, 값 튐
드라이버LiDAR 노드, 모터 노드, 카메라 노드/scan, /cmd_vel, /image, /imu토픽 없음, 메시지 주기 불안정
로봇 상태URDF, xacro, robot_state_publisher, 오도메트리/tf, /tf_static, /joint_states, /odomRViz에서 로봇이 깨짐, TF 오류
위치 추정SLAM, AMCL, EKF/map, /amcl_pose, map → odom로봇 위치가 튐, 납치된 것처럼 이동
내비게이션Planner, Controller, Costmap, BT Navigator/plan, /cmd_vel, costmap경로 없음, 회피 실패, 제자리 회전
검증 도구RViz, rosbag, CLIros2 topic, ros2 run tf2_ros tf2_echo원인 분석 어려움

이 표는 단순한 요약이 아니다. 실제 프로젝트에서 문제가 생겼을 때 위에서 아래로, 또는 아래에서 위로 원인을 좁히는 지도다. /cmd_vel이 나오는데 로봇이 움직이지 않으면 Nav2보다 모터 드라이버를 먼저 본다. /scan은 나오는데 costmap에 장애물이 보이지 않으면 센서 frame과 costmap 설정을 확인한다. RViz에서 로봇 모델은 보이는데 LiDAR가 엉뚱한 곳에 붙어 있으면 URDF와 TF를 본다.

프로젝트는 패키지 구조에서 시작된다

로봇 프로젝트가 커질수록 코드보다 구조가 먼저 무너진다. 처음에는 my_robot이라는 패키지 하나에 launch 파일, URDF, Python 노드, 지도, Nav2 설정, RViz 설정을 모두 넣어도 괜찮아 보인다. 하지만 SLAM을 추가하고, 시뮬레이션을 붙이고, 실제 로봇을 연결하고, 지도 파일이 여러 개가 되면 금세 길을 잃는다.

좋은 ROS2 프로젝트는 역할별로 패키지를 나눈다. 패키지를 나눈다는 것은 파일을 예쁘게 정리하는 일이 아니다. 책임의 경계를 명확히 하는 일이다.

예를 들어 2륜 차동 구동 로봇 프로젝트를 다음과 같이 구성할 수 있다.

my_mobile_robot_ws/
└── src/
    ├── my_robot_description/
    │   ├── urdf/
    │   │   └── my_robot.urdf.xacro
    │   ├── meshes/
    │   ├── rviz/
    │   └── launch/
    │       └── display.launch.py
    │
    ├── my_robot_bringup/
    │   ├── launch/
    │   │   ├── robot.launch.py
    │   │   ├── sensors.launch.py
    │   │   └── bringup.launch.py
    │   └── config/
    │       ├── robot_params.yaml
    │       └── sensor_params.yaml
    │
    ├── my_robot_simulation/
    │   ├── worlds/
    │   ├── models/
    │   ├── launch/
    │   │   └── simulation.launch.py
    │   └── config/
    │
    ├── my_robot_navigation/
    │   ├── launch/
    │   │   ├── slam.launch.py
    │   │   ├── localization.launch.py
    │   │   └── navigation.launch.py
    │   ├── config/
    │   │   ├── nav2_params.yaml
    │   │   ├── amcl_params.yaml
    │   │   └── slam_toolbox_params.yaml
    │   └── behavior_trees/
    │       └── navigate_w_replanning_and_recovery.xml
    │
    ├── my_robot_maps/
    │   ├── maps/
    │   │   ├── office.yaml
    │   │   └── office.pgm
    │   └── README.md
    │
    └── my_robot_interfaces/
        ├── msg/
        ├── srv/
        └── action/

description 패키지는 로봇의 몸을 설명한다. URDF, xacro, mesh, RViz 표시 설정이 여기에 들어간다. 이 패키지는 가능하면 하드웨어와 시뮬레이션에서 함께 사용해야 한다. 실제 로봇과 Gazebo 로봇의 크기, 바퀴 위치, LiDAR 위치가 다르면 같은 소프트웨어를 돌려도 결과가 달라진다.

bringup 패키지는 실제 로봇을 켜는 입구다. 센서 드라이버, 모터 드라이버, robot_state_publisher, 필요한 정적 TF, 하드웨어 관련 파라미터를 실행한다. 로봇을 현장에서 켤 때 가장 먼저 실행하는 launch 파일이 이 패키지에 있어야 한다.

simulation 패키지는 가상 세계를 담당한다. Gazebo 월드, 시뮬레이션용 로봇 spawn, 가상 센서 설정, 시뮬레이션 전용 플러그인이 들어간다. 핵심은 시뮬레이션에서만 필요한 것을 이곳에 격리하는 것이다. 가상 LiDAR 플러그인은 simulation에 있어도 되지만, LiDAR가 달린 위치 자체는 description에 있어야 한다.

navigation 패키지는 SLAM, AMCL, Nav2 설정을 관리한다. nav2_params.yaml, slam_toolbox_params.yaml, amcl_params.yaml, Behavior Tree XML, 내비게이션 launch 파일이 이 패키지의 중심이다. 이 패키지는 로봇의 지능형 이동을 담당한다.

maps 패키지는 저장된 지도를 보관한다. 지도를 소스 코드처럼 다루는 것이 중요하다. 어느 건물의 어느 층에서 만든 지도인지, 해상도는 얼마인지, 언제 저장했는지, 어떤 로봇 설정으로 만들었는지 기록해 두면 나중에 큰 도움이 된다.

config 디렉터리는 여러 패키지 안에 나뉘어 있을 수 있다. 중요한 것은 설정 파일의 주인이 분명해야 한다는 점이다. 센서 드라이버 설정은 bringup에, Nav2 설정은 navigation에, 시뮬레이션 플러그인 설정은 simulation에 두는 식으로 책임을 나눈다.

같은 로봇, 두 개의 세계

좋은 자율주행 프로젝트는 시뮬레이션과 실제 로봇을 완전히 따로 만들지 않는다. 두 세계는 최대한 같은 이름과 구조를 공유해야 한다. 그래야 Gazebo에서 성공한 설정을 실제 로봇으로 가져올 수 있고, 실제 로봇에서 발생한 문제를 다시 시뮬레이션에서 재현할 수 있다.

공유해야 할 것은 다음과 같다.

  • 로봇 모델: base_link, base_footprint, laser_link, wheel_left_link 같은 링크 이름과 위치

  • 토픽 이름: /scan, /odom, /cmd_vel, /tf, /joint_states 같은 인터페이스

  • 프레임 이름: map, odom, base_link, base_footprint, laser_link

  • Nav2 파라미터 구조: global costmap, local costmap, planner, controller, AMCL 설정의 기본 뼈대

  • RViz 설정: 같은 Fixed Frame, 같은 표시 항목, 같은 토픽 이름

반대로 교체되어야 할 것은 하드웨어에 직접 닿는 부분이다. 시뮬레이션에서는 Gazebo 플러그인이 /scan과 /odom을 만들 수 있다. 실제 로봇에서는 LiDAR 드라이버와 모터 제어 노드가 그 역할을 맡는다. Nav2 입장에서는 누가 /scan을 만들었는지보다, 메시지 타입과 frame_id가 올바른지가 더 중요하다.

이를 launch 구조로 표현하면 다음과 같다.

공통
- robot_state_publisher
- URDF/xacro
- Nav2 파라미터
- RViz 설정
- 지도 파일

시뮬레이션 전용
- Gazebo 실행
- 가상 로봇 spawn
- 가상 LiDAR, IMU, 엔코더 플러그인
- use_sim_time:=true

실제 로봇 전용
- LiDAR 드라이버
- 모터 드라이버
- IMU 드라이버
- 배터리 상태 노드
- use_sim_time:=false

launch 파일은 이 차이를 인자로 관리하면 좋다.

# 개념 예시
DeclareLaunchArgument('use_sim_time', default_value='false')
DeclareLaunchArgument('mode', default_value='real')
DeclareLaunchArgument('map', default_value='office.yaml')

mode:=sim이면 Gazebo와 가상 센서를 실행하고, mode:=real이면 실제 센서 드라이버를 실행한다. 하지만 두 경우 모두 /scan, /odom, /tf, /cmd_vel이라는 약속은 유지한다. 이것이 재사용성과 이식성의 핵심이다.

이 원칙을 지키면 로봇을 바꾸는 일도 쉬워진다. 작은 실내 로봇에서 시작해 더 큰 플랫폼으로 옮겨 가더라도, 센서 위치와 footprint, 속도 제한, costmap 크기만 조정하면 기존 구조를 상당 부분 유지할 수 있다. 실제 공개 프로젝트들도 SLAM, Nav2, 센서 융합, 시뮬레이션, 실제 제어를 하나의 프레임워크 안에 묶는 방식으로 시스템을 구성한다.4

실행 순서는 로봇의 호흡이다

완성된 시스템은 실행 순서가 중요하다. 모든 노드를 한 번에 켜는 것도 가능하지만, 디버깅할 때는 단계별로 켜야 한다. 로봇이 살아나는 과정을 천천히 확인해야 어디서 문제가 생겼는지 알 수 있다.

첫 번째 단계는 기본 bringup이다. 실제 로봇이라면 센서와 모터 드라이버를 켜고, 시뮬레이션이라면 Gazebo에 로봇을 띄운다. 이때 확인할 것은 로봇의 생체 신호다.

ros2 topic list
ros2 topic echo /scan --once
ros2 topic echo /odom --once
ros2 topic echo /joint_states --once
ros2 run tf2_ros tf2_echo odom base_link

/scan이 나오고, /odom이 움직임에 따라 변하고, odom → base_link TF가 존재하면 로봇은 최소한 자신이 움직인다는 사실을 ROS2 세계에 말하고 있는 것이다.

두 번째 단계는 로봇 모델과 TF 확인이다. RViz를 열고 Fixed Frame을 odom 또는 map으로 설정한 뒤 RobotModel, TF, LaserScan을 표시한다. LiDAR 점들이 로봇의 앞쪽에 자연스럽게 나타나는지 확인한다. 로봇은 정면을 보고 있는데 LiDAR 스캔이 옆으로 누워 있다면 Nav2는 세상을 옆으로 보고 운전하게 된다.

세 번째 단계는 지도 작성이다. SLAM을 실행하고 로봇을 천천히 움직인다. 좋은 지도는 빠른 주행에서 나오지 않는다. 벽을 따라 부드럽게 움직이고, 같은 장소를 여러 방향에서 다시 관찰해야 한다. 지도 작성 중에는 /map, /tf, /scan, /odom을 함께 본다.

ros2 launch my_robot_navigation slam.launch.py use_sim_time:=true
ros2 bag record /scan /odom /tf /tf_static /cmd_vel

rosbag을 함께 기록하면 실험을 되돌릴 수 있다. 한 번 실패한 실험도 bag으로 남아 있으면 훌륭한 교재가 된다. 지도 품질이 나빴던 이유가 빠른 회전 때문인지, LiDAR frame 오류 때문인지, 오도메트리 드리프트 때문인지 나중에 다시 확인할 수 있다.

네 번째 단계는 지도 저장이다.

ros2 run nav2_map_server map_saver_cli -f office

저장된 office.yaml과 office.pgm 또는 office.png는 프로젝트의 자산이다. 파일 이름은 대충 짓지 않는 것이 좋다. map1.yaml보다 office_2f_2026_05_14.yaml이 훨씬 낫다. 지도는 시간이 지나면 헷갈린다. 같은 복도라도 가구 배치가 바뀌면 다른 지도가 된다.

다섯 번째 단계는 AMCL 기반 위치 추정이다. 저장된 지도를 map server로 불러오고 AMCL을 실행한다. RViz에서 초기 위치를 지정한 뒤, 로봇을 조금 움직여 입자들이 수렴하는지 확인한다.

확인해야 할 것은 명확하다.

  • /map이 정상적으로 발행되는가

  • map → odom → base_link TF 사슬이 이어지는가

  • /amcl_pose가 로봇 이동에 따라 자연스럽게 변하는가

  • RViz의 LaserScan이 지도 벽과 겹치는가

  • 로봇을 회전시켜도 위치가 과도하게 튀지 않는가

여섯 번째 단계는 Nav2 목표 이동이다. RViz에서 Nav2 Goal을 찍으면 Nav2는 전역 경로를 만들고, local costmap을 보며 속도 명령을 만든다.

ros2 topic echo /cmd_vel
ros2 topic echo /plan --once

이때 /cmd_vel이 나오는데 로봇이 움직이지 않으면 모터 드라이버나 하드웨어 인터페이스를 본다. /cmd_vel이 나오지 않으면 Nav2 내부 상태, goal 수락 여부, costmap, TF, 위치 추정을 본다. 경로가 생성되지 않으면 global costmap과 지도, 목표 지점의 점유 상태를 본다. 경로는 있는데 로봇이 망설이면 local costmap과 controller 설정을 본다.

일곱 번째 단계는 동적 장애물 회피다. 사람이나 상자를 로봇 경로에 천천히 가로지르게 하고, local costmap에 장애물이 표시되는지 본다. 로봇은 멈추거나, 돌아가거나, 기다렸다가 다시 진행해야 한다. 이때 중요한 것은 로봇이 "무조건 빠르게" 가는 것이 아니라 "예측 가능한 방식으로 안전하게" 행동하는 것이다.

전체 실행 절차를 체크리스트로 정리하면 다음과 같다.

단계실행 내용반드시 확인할 것
1bringup 또는 simulation 실행/scan, /odom, /tf, /cmd_vel 인터페이스
2로봇 모델 표시RobotModel, TF tree, LiDAR 위치
3수동 주행/cmd_vel 입력에 따른 실제 이동, 오도메트리 변화
4rosbag 기록/scan, /odom, /tf, /tf_static, /cmd_vel 저장
5SLAM 실행/map, map → odom, 지도 왜곡 여부
6지도 저장YAML/이미지 파일, 해상도, origin
7AMCL 실행/amcl_pose, 입자 수렴, LaserScan과 지도 정합
8Nav2 실행global/local costmap, /plan, /cmd_vel
9목표 이동경로 생성, 회피, 도착 판정
10동적 장애물 실험local costmap 반영, 정지·회피·재진입

이 체크리스트는 한 번만 쓰는 문서가 아니다. 로봇을 새 장소로 옮길 때, 센서를 교체할 때, Nav2 파라미터를 바꿀 때마다 다시 사용할 수 있는 운용 절차다.

launch는 시스템의 스위치보드다

완성된 프로젝트에서 launch 파일은 단순히 노드를 실행하는 스크립트가 아니다. 시스템 전체의 스위치보드다. 어떤 모드로 실행할지, 어떤 지도를 쓸지, 시뮬레이션 시간을 쓸지, RViz를 함께 켤지, SLAM 모드인지 localization 모드인지 결정한다.

예를 들어 최상위 launch 파일은 다음과 같은 역할을 할 수 있다.

bringup.launch.py
- robot_state_publisher 실행
- 실제 센서 드라이버 실행
- 모터 드라이버 실행
- 정적 TF 실행

simulation.launch.py
- Gazebo 실행
- 로봇 spawn
- 가상 센서 실행
- robot_state_publisher 실행

slam.launch.py
- bringup 또는 simulation 포함
- slam_toolbox 실행
- RViz 실행

localization.launch.py
- map_server 실행
- AMCL 실행
- lifecycle manager 실행

navigation.launch.py
- localization 포함
- Nav2 planner/controller/bt_navigator/costmap 실행
- RViz 실행

처음에는 launch 파일을 많이 나누는 것이 번거롭게 느껴질 수 있다. 그러나 프로젝트가 커질수록 이 구조가 시간을 아껴 준다. SLAM만 테스트하고 싶을 때 navigation 전체를 켤 필요가 없다. 센서 드라이버만 확인하고 싶을 때 AMCL과 Nav2를 함께 실행할 필요도 없다.

좋은 launch 구조는 다음 질문에 바로 답할 수 있어야 한다.

  • 실제 로봇과 시뮬레이션을 어떤 인자로 바꾸는가

  • 지도 파일 경로는 어디서 전달되는가

  • use_sim_time은 모든 노드에 일관되게 적용되는가

  • RViz는 선택적으로 켤 수 있는가

  • SLAM 모드와 localization 모드는 동시에 켜지지 않도록 되어 있는가

  • Nav2 lifecycle 노드들은 자동으로 활성화되는가

특히 use_sim_time은 자주 사고를 일으킨다. Gazebo나 rosbag 재생에서는 use_sim_time:=true가 필요하지만, 실제 로봇에서는 일반적으로 false여야 한다. 어떤 노드는 시뮬레이션 시간을 보고, 어떤 노드는 시스템 시간을 보면 TF와 메시지 시간이 어긋난다. 그러면 겉으로는 토픽이 다 나오는데 RViz와 Nav2는 데이터를 오래된 것으로 판단할 수 있다.

완성도를 높이는 튜닝 순서

자율주행 로봇은 "실행된다"와 "쓸 만하다" 사이에 긴 거리가 있다. 목표 지점까지 한 번 도착했다고 끝난 것이 아니다. 같은 목표를 여러 번 반복해도 안정적으로 도착해야 하고, 장애물이 있어도 과도하게 흔들리지 않아야 하며, 좁은 공간에서 벽을 긁지 않아야 한다. 이 차이를 만드는 것이 튜닝이다.

튜닝은 한꺼번에 하면 안 된다. footprint, inflation, costmap, planner, controller, AMCL, QoS를 동시에 바꾸면 무엇 때문에 좋아졌는지 알 수 없다. 좋은 튜닝은 한 번에 하나씩 바꾸고, 실험을 기록하고, 결과를 비교하는 과정이다.

먼저 footprint부터 맞춘다. footprint는 로봇이 실제로 차지하는 바닥 면적이다. 원형 로봇이라면 robot_radius로 시작할 수 있고, 직사각형 또는 돌출부가 있는 로봇이라면 다각형 footprint가 낫다. footprint가 실제보다 작으면 로봇은 벽에 너무 가까이 붙는다. 실제보다 크면 갈 수 있는 길도 못 간다고 판단한다.

그다음은 inflation radius다. inflation layer는 장애물 주변에 위험 영역을 부풀리는 역할을 한다. 이 값이 너무 작으면 로봇이 장애물에 바짝 붙고, 너무 크면 복도가 막힌 것처럼 보인다. 좁은 실내 로봇에서는 몇 센티미터 차이로 주행 느낌이 크게 달라진다.

global costmap과 local costmap은 역할이 다르므로 따로 봐야 한다. global costmap은 큰 길을 보는 지도다. 저장된 static map이 중요하고, 목표 지점까지 갈 수 있는 전역 경로를 만드는 데 쓰인다. local costmap은 로봇 주변의 현재 상황을 보는 눈이다. 갑자기 나타난 사람, 의자, 상자 같은 장애물은 local costmap에서 즉시 반영되어야 한다.

planner 튜닝은 경로의 성격을 바꾼다. 경로가 벽에 너무 붙는지, 불필요하게 돌아가는지, 큰 로봇이 회전하기 어려운 경로를 만드는지 본다. controller 튜닝은 로봇의 움직임을 바꾼다. 너무 흔들리는지, 회전이 과한지, 목표 근처에서 계속 맴도는지, 장애물 앞에서 멈춘 뒤 다시 출발하지 못하는지 확인한다.

AMCL 튜닝은 위치 추정 안정성을 높인다. 입자 수가 너무 적으면 위치 추정이 불안정하고, 너무 많으면 계산량이 커진다. LiDAR 센서 모델이 실제 환경과 맞지 않으면 벽이 많은 복도에서 위치가 잘 맞다가, 넓은 홀에서는 갑자기 흔들릴 수 있다. 로봇이 빠르게 회전할수록 오도메트리 오차와 센서 업데이트 주기의 영향도 커진다.

마지막으로 QoS와 주기를 점검한다. LiDAR 데이터가 너무 늦게 들어오거나, 오도메트리 주기가 낮거나, /tf가 끊기면 Nav2는 오래된 세상을 보고 운전한다. 특히 무선 네트워크, USB 대역폭, CPU 부하가 높은 환경에서는 토픽이 "있다"는 사실만으로 충분하지 않다. 주기와 지연을 함께 확인해야 한다.

튜닝 실험은 다음처럼 기록하면 좋다.

실험 이름: office_nav_test_2026_05_14_A
지도: office_2f_2026_05_14.yaml
로봇: my_robot_v1
변경 파라미터: inflation_radius 0.35 → 0.45
목표: 복도 끝 회의실 앞
결과: 벽과의 거리는 안정적이지만 좁은 문 앞에서 경로 실패 2회
기록 bag: bags/office_nav_test_A
메모: doorway 통과를 위해 local costmap 폭과 controller 속도 제한 재검토 필요

이런 기록이 쌓이면 로봇은 감이 아니라 데이터로 좋아진다.

문제가 생겼을 때 보는 순서

자율주행 로봇 디버깅의 핵심은 원망할 대상을 빨리 고르는 것이 아니다. 의심할 범위를 차분히 줄이는 것이다. 로봇이 목표 지점으로 가지 못한다고 해서 곧바로 Nav2 파라미터를 뜯어고치면 안 된다. 문제는 배터리일 수도 있고, TF일 수도 있고, 지도 파일 경로일 수도 있다.

가장 안전한 순서는 아래에서 위로 올라가는 것이다.

먼저 하드웨어를 본다. 전원은 충분한가. 센서는 연결되어 있는가. 모터 드라이버는 에러 상태가 아닌가. 로봇이 바닥에서 미끄러지지 않는가.

다음으로 토픽을 본다. /scan, /odom, /tf, /cmd_vel이 있는가. 메시지가 한 번만 나오고 멈춘 것은 아닌가. 주기는 충분한가.

그다음 TF를 본다. map → odom → base_link → laser_link가 이어지는가. frame 이름이 설정 파일과 일치하는가. 같은 변환을 두 노드가 동시에 발행해 TF 충돌을 만들고 있지는 않은가.

그다음 위치 추정을 본다. AMCL 입자가 지도 위에 모여 있는가. LaserScan이 벽과 잘 겹치는가. 로봇을 조금 움직였을 때 위치가 부드럽게 따라오는가.

마지막으로 Nav2를 본다. global costmap에 지도가 보이는가. local costmap에 장애물이 보이는가. 목표 지점이 점유 영역 안에 찍힌 것은 아닌가. controller가 속도 명령을 만들고 있는가.

증상별로 보면 더 빠르다.

증상먼저 볼 곳자주 있는 원인
RViz에 로봇이 안 보임RobotModel, /robot_description, TFxacro 오류, Fixed Frame 오류
LiDAR가 엉뚱한 위치에 보임laser_link, URDF, frame_id센서 위치 오기입, static TF 누락
지도는 보이는데 로봇 위치가 틀림AMCL, 초기 위치, /scan 정합초기 pose 오류, LiDAR 방향 오류
경로가 생성되지 않음global costmap, map server목표가 장애물 안, 지도 미로드
경로는 있는데 움직이지 않음controller, /cmd_vel, local costmaplocal costmap 장애물 과다, 속도 제한
/cmd_vel은 나오는데 실제 로봇이 정지모터 드라이버, 안전 정지, remap토픽 이름 불일치, E-stop, 하드웨어 오류
장애물을 못 피함local costmap, sensor sourceobstacle layer 설정 누락, QoS 불일치

디버깅을 잘하는 사람은 명령어를 많이 아는 사람이 아니라, 어떤 순서로 확인해야 하는지 아는 사람이다.

나만의 로봇으로 확장하기

이제 기본 자율주행 로봇의 뼈대는 완성되었다. 여기서부터는 선택의 영역이다. 어떤 로봇을 만들 것인지는 목적에 따라 달라진다. 사무실 안내 로봇, 창고 운반 로봇, 병원 물류 로봇, 농업용 주행 플랫폼, 연구용 TurtleBot, 교육용 소형 로봇은 모두 다른 제약을 가진다. 하지만 기본 구조는 크게 다르지 않다.

새로운 로봇을 만들 때는 다음 순서로 생각하면 좋다.

  • 로봇의 구동 방식은 무엇인가: 차동 구동, 4륜 구동, 메카넘, Ackermann

  • 주행 환경은 어디인가: 실내 복도, 창고, 실외 포장도로, 비포장 환경

  • 주 센서는 무엇인가: 2D LiDAR, 3D LiDAR, RGB-D 카메라, IMU, GNSS

  • 지도 방식은 무엇인가: 2D 점유 격자 지도, 3D 지도, 마커 기반 지도, GNSS 기반 지도

  • 안전 요구사항은 어느 정도인가: 저속 교육용, 사람과 함께 일하는 로봇, 산업 현장 로봇

구동 방식이 바뀌면 controller 설정이 바뀐다. 센서가 바뀌면 costmap source와 TF가 바뀐다. 환경이 바뀌면 지도 해상도, inflation, 속도 제한이 바뀐다. 하지만 노드, 토픽, TF, SLAM, AMCL, Nav2라는 큰 골격은 유지된다.

더 나아가고 싶다면 센서 융합을 공부할 수 있다. 바퀴 오도메트리와 IMU를 EKF로 융합하면 회전 중 오도메트리 드리프트를 줄일 수 있다. 실외에서는 GNSS와 IMU, wheel odometry를 함께 써야 할 수도 있다. 사람을 따라가는 로봇을 만들고 싶다면 perception 노드를 추가하고, Nav2의 목표를 동적으로 갱신하면 된다. 여러 목표를 순서대로 방문하고 싶다면 waypoint follower나 액션 클라이언트를 사용할 수 있다.

중요한 것은 확장할 때도 기존 인터페이스를 존중하는 것이다. 새로운 센서를 추가해도 frame_id를 정확히 붙이고, 토픽 이름을 명확히 정하고, RViz에서 확인하고, rosbag으로 기록한다. 새로운 알고리즘을 넣어도 입력과 출력이 무엇인지 분명히 한다. 좋은 로봇 시스템은 똑똑한 알고리즘 하나가 아니라, 작은 약속들이 끝까지 지켜지는 구조에서 나온다.

완성된 시스템의 기준

자율주행 로봇이 완성되었다고 말하려면 몇 가지 기준을 통과해야 한다. 단순히 데모 영상 한 번을 찍었다고 완성은 아니다. 로봇은 반복해서, 예측 가능하게, 안전하게 움직여야 한다.

완성 기준은 다음처럼 잡을 수 있다.

  • 전원을 켠 뒤 정해진 launch 파일 하나로 기본 시스템이 올라온다.

  • RViz에서 로봇 모델, TF, 지도, LaserScan, costmap이 일관되게 보인다.

  • 수동 주행 시 /cmd_vel, /odom, TF가 정상적으로 반응한다.

  • SLAM으로 만든 지도를 저장하고, 같은 지도를 AMCL에서 다시 사용할 수 있다.

  • 초기 위치를 지정하면 AMCL이 안정적으로 수렴한다.

  • Nav2 목표를 주면 전역 경로가 생성되고 로봇이 local costmap을 보며 이동한다.

  • 경로 위 장애물이 나타나면 멈추거나 회피하고, 가능하면 다시 목표로 향한다.

  • 실패했을 때 rosbag과 로그, RViz 화면을 통해 원인을 추적할 수 있다.

  • 시뮬레이션과 실제 로봇이 같은 모델, 토픽 이름, TF 규칙을 공유한다.

  • 설정 변경과 실험 결과가 기록되어 다음 실험에서 재현 가능하다.

이 기준을 만족하는 로봇은 더 이상 장난감 예제가 아니다. 아직 거칠고 부족한 부분이 있더라도, 시스템으로서 성장할 준비가 된 로봇이다.

마지막으로, 로봇은 연결의 기술이다

ROS2를 배우는 동안 우리는 많은 이름을 만났다. 노드, 토픽, 서비스, 액션, 파라미터, launch, TF, URDF, xacro, RViz, Gazebo, rosbag, SLAM, AMCL, Nav2, costmap, Behavior Tree. 처음에는 낯선 단어들이 줄지어 서 있는 것처럼 보였지만, 이제는 각각이 어디에 놓이는지 알 수 있다.

노드는 일을 나누는 단위다. 토픽은 데이터가 흐르는 길이다. TF는 좌표계 사이의 약속이다. URDF는 로봇의 몸을 설명한다. SLAM은 낯선 공간을 지도와 위치로 바꾼다. AMCL은 저장된 지도 위에서 현재 위치를 찾는다. Nav2는 목표를 향해 경로와 속도 명령을 만든다. RViz와 rosbag은 보이지 않는 시스템을 보고 되돌릴 수 있게 해 준다.

자율주행 로봇을 만든다는 것은 이 모든 것을 완벽하게 외우는 일이 아니다. 센서에서 시작해 바퀴까지 이어지는 데이터의 길을 이해하는 일이다. 그리고 그 길이 막혔을 때 어디서 막혔는지 찾아내는 일이다.

처음 만든 로봇은 비틀거릴 것이다. 지도는 삐뚤어지고, 위치는 튀고, Nav2는 가끔 고집스럽게 회전만 할지도 모른다. 하지만 이제 그 장면은 막막한 실패가 아니다. /scan을 보고, TF를 보고, costmap을 보고, /cmd_vel을 보고, bag을 재생하면 된다. 로봇은 거짓말을 잘하지 않는다. 다만 자신이 아픈 곳을 로그와 토픽과 좌표계로 말할 뿐이다.

이제 남은 일은 직접 만드는 것이다. 작은 바퀴 두 개와 LiDAR 하나로 시작해도 충분하다. 시뮬레이션에서 먼저 달려 보고, bag으로 기록하고, 지도를 저장하고, AMCL을 맞추고, Nav2로 목표를 보내 보자. 그다음 센서를 바꾸고, 환경을 넓히고, 더 어려운 장애물을 놓고, 더 좋은 행동을 설계하면 된다.

자율주행 로봇은 한 번에 완성되는 물건이 아니다. 실험할수록 더 정확해지고, 기록할수록 더 안정해지고, 구조를 지킬수록 더 멀리 확장된다. ROS2는 그 과정을 버틸 수 있도록 도와주는 연결의 프레임워크다. 이제 그 연결을 사용해, 당신만의 이동 로봇 시스템을 완성할 차례다.

참고

1Robot Operating System 공식 소개

2From the Desks of ROS Maintainers: A Survey of Modern & Capable Mobile Robotics Algorithms in ROS 2

3Nav2 공식 문서

4ROS2 mobile manipulator example project

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기