Skip to main content

14. ROS 2로 센서와 모터를 연결하기

ROS 2로 센서와 모터를 연결하기

한 파일짜리 로봇을 졸업할 시간

처음 만든 로봇은 대개 단순합니다. 센서를 읽고, 조건문으로 판단하고, 모터를 움직입니다. 이 구조는 빠르게 시작하기에 좋습니다. 문제는 기능이 늘어날 때입니다. 카메라를 붙이고, 라이다를 붙이고, 키보드 조종도 넣고, 나중에는 음성 명령이나 자율주행까지 얹기 시작하면 코드가 금방 엉킵니다.

예를 들어 카메라 프레임을 읽는 함수 안에서 물체를 인식하고, 그 결과로 모터를 바로 돌리고, 동시에 로그도 저장한다고 해봅시다. 처음에는 편합니다. 하지만 카메라가 잠깐 느려지는 순간 모터 반응도 늦어지고, 라이다를 추가하려고 하면 기존 코드를 뜯어고쳐야 하며, 어디가 문제인지 찾기도 어려워집니다.

이 장의 목표는 분명합니다. 센서, 판단, 명령, 모터 제어를 서로 다른 노드로 나눠서, 실제 로봇 시스템처럼 연결하는 감각을 익히는 것입니다. ROS 2에서는 노드를 가능한 한 한 가지 역할 중심으로 쪼개는 것이 기본 철학입니다.1 이 감각이 생기면 로봇이 커져도 구조가 무너지지 않습니다.

먼저 전체 배선도를 머릿속에 그리자

ROS 2로 로봇을 짠다는 말은, 거대한 프로그램 하나를 만드는 것이 아니라 작은 프로그램 여러 개를 연결한다는 뜻에 가깝습니다. 이동 로봇을 예로 들면 전체 그림은 보통 이렇게 생깁니다.

[camera_node] ----> /camera/image_raw ---->
                                         \
                                          [perception_node] --\
                                                               \
[lidar_node] -----> /scan ---------------> [planner/safety_node] ---> /cmd_vel ---> [motor_control_node] ---> 바퀴
                                                               /
[teleop_node] ----> /cmd_vel_teleop ----/

[encoder/hardware] ---> /joint_states, /odom
[robot_state_publisher] ---> TF(base_link, camera_link, laser_frame ...)

핵심은 간단합니다. 카메라는 본 것을 보내고, 라이다는 잰 거리를 보내고, 판단 노드는 그 데이터를 받아 결정하고, 모터 제어 노드는 마지막에 실제 바퀴로 번역합니다. 각자 맡은 일이 다르기 때문에, 각자 따로 고치고 테스트할 수 있습니다.

이 구조가 좋은 이유는 "예쁘기 때문"이 아닙니다. 카메라를 바꿔도 주행 코드를 덜 건드리고, 주행 알고리즘을 바꿔도 모터 드라이버 코드를 덜 건드리게 되기 때문입니다. 로봇 소프트웨어에서 진짜 귀한 것은 성능만이 아니라 교체 가능성입니다.

카메라 노드는 보는 일만 맡긴다

카메라를 ROS 2에 연결할 때 초보자가 자주 하는 실수는, 카메라 노드 안에 판단 로직까지 같이 넣는 것입니다. 예를 들어 USB 카메라를 읽는 노드가 동시에 사람을 찾고, 사람을 찾으면 바퀴를 돌리게 만드는 식입니다.

처음에는 그렇게 해도 돌아갑니다. 하지만 곧 문제가 생깁니다. 카메라만 교체하려 해도 인식 코드가 같이 따라오고, 인식만 바꾸려 해도 카메라 드라이버를 건드리게 됩니다.

더 나은 방식은 이렇습니다. 카메라 노드는 그냥 영상을 발행합니다. 그다음 노드가 영상을 받아 물체 인식을 하든, 녹화를 하든, 화면 표시를 하든 각자 알아서 합니다. 즉 카메라 노드는 "센서 입력 공급자"이고, 인식 노드는 "해석기"입니다.

이렇게 나누면 아주 실용적인 장점이 생깁니다. 책상 위에서 웹캠으로 테스트하던 인식 노드를, 나중에는 라즈베리파이 카메라나 다른 카메라 드라이버에 그대로 붙일 수 있습니다. 카메라 하드웨어가 바뀌어도, 토픽 형식과 이름만 잘 유지하면 위쪽 로직을 그대로 재사용하기 쉽습니다.

라이다 노드는 거리를 재고, 의미는 다음 노드가 붙인다

라이다도 같은 원리입니다. 라이다 노드의 일은 "장애물이 있다"고 결론 내리는 것이 아니라, 우선 거리 데이터를 ROS 2 형식으로 내보내는 것입니다. 2D 라이다는 보통 sensor_msgs/LaserScan 형식으로 데이터를 발행하고, 이 메시지에는 거리 배열뿐 아니라 센서가 어떤 좌표 프레임에 붙어 있는지도 함께 담깁니다.2

이게 왜 중요할까요? 로봇은 단순히 숫자만 읽는 기계가 아니기 때문입니다. 앞쪽 1.2미터인지, 왼쪽 1.2미터인지가 중요합니다. 그래서 라이다 데이터에는 단순한 거리값뿐 아니라 "이 센서가 로봇 몸체 어디에 달려 있는가"라는 정보가 함께 맞물려야 합니다.

라이다 노드와 장애물 회피 노드를 분리해 두면, 아주 편해집니다. 저가형 2D 라이다를 쓰다가 더 좋은 제품으로 바꿔도, 새 드라이버가 /scan 같은 표준 토픽을 잘 내보내면 회피 로직은 크게 안 바꿔도 됩니다. 이것이 ROS 2의 진짜 맛입니다. 센서 제조사가 바뀌어도, 위쪽 로직은 웬만하면 그대로 둡니다.

명령 노드는 여러 의도를 한 줄로 정리한다

센서 노드와 판단 노드를 나눴다면, 다음으로 중요한 것은 누가 최종 명령을 내릴 것인가입니다. 실제 로봇에는 명령원이 여러 명일 수 있습니다. 키보드 조종이 있을 수 있고, 자율주행 노드가 있을 수 있고, 긴급 정지 노드도 있을 수 있습니다.

여기서 가장 위험한 구조는 여러 노드가 동시에 모터를 직접 만지는 것입니다. 키보드 노드도 바퀴를 돌리고, 장애물 회피 노드도 바퀴를 돌리고, 음성 명령 노드도 바퀴를 돌리면 충돌이 납니다. 사람으로 치면 운전대에 세 명이 동시에 손을 올리는 셈입니다.

그래서 실전 구조에서는 중간에 명령을 모으는 레이어를 둡니다. 예를 들어 키보드 조종은 /cmd_vel_teleop, 자율주행은 /cmd_vel_nav, 안전 회피는 /cmd_vel_safe_raw 같은 식으로 따로 내보내고, 마지막에 하나의 노드가 우선순위를 정해 최종 /cmd_vel을 만듭니다.

이 방식의 장점은 분명합니다. 지금 로봇이 누구 말을 듣고 있는지 알 수 있고, 긴급 정지가 들어오면 무조건 최종 명령을 0으로 덮어쓸 수 있습니다. 로봇은 똑똑한 것보다 먼저 말을 질서 있게 들어야 합니다.

모터 제어 노드는 마지막 번역기다

모터 제어 노드는 로봇 시스템의 끝단입니다. 위쪽에서 들어온 추상적인 움직임 명령을, 실제 하드웨어가 이해하는 신호로 바꿉니다. 예를 들어 /cmd_vel로 들어온 선속도와 각속도를 왼쪽 바퀴와 오른쪽 바퀴 속도로 바꾸고, 그것을 다시 시리얼 명령이나 CAN 메시지, PWM 신호로 바꿔 보내는 식입니다.

이 레이어를 분리해 두면 상위 로직은 모터 드라이버 칩의 세세한 차이를 몰라도 됩니다. 상위는 그냥 "앞으로 초당 0.2미터로 가라"고 말하고, 아래쪽이 "그럼 왼쪽 바퀴는 얼마, 오른쪽 바퀴는 얼마"로 풀어냅니다.

ROS 2에서 이런 구조를 표준화해 다루는 대표 도구가 ros2_control입니다. ros2_control은 로봇의 액추에이터와 센서를 ROS 2 방식으로 제어하기 위한 프레임워크입니다.3 핵심 아이디어는 하드웨어를 직접 다루는 부분과, 그 하드웨어를 어떻게 움직일지 계산하는 컨트롤러를 분리하는 것입니다.4

이 개념이 처음에는 조금 추상적으로 들릴 수 있습니다. 하지만 쉽게 말하면 이렇습니다. 바퀴를 돌리는 계산과, 실제 모터 컨트롤러에 명령을 보내는 일은 다른 문제입니다. 하나는 수학과 제어의 문제이고, 다른 하나는 통신과 하드웨어의 문제입니다. ros2_control은 이 둘을 갈라서 다루게 해 줍니다.

이동 로봇에서는 /cmd_vel이 중심축이 된다

바퀴형 이동 로봇에서는 보통 geometry_msgs/Twist 형식의 /cmd_vel 토픽이 중심축이 됩니다. 이 토픽에는 로봇 몸체 기준의 선속도와 각속도가 담깁니다. 그다음 단계에서 차동 구동 로봇이라면 이 속도를 왼쪽 바퀴와 오른쪽 바퀴 속도로 풀어냅니다.

실제 이동 로봇 구성에서 자주 쓰는 흐름은 이렇습니다. 상위 노드가 몸체 속도 명령을 보내고, diff_drive_controller가 그것을 바퀴 속도로 바꾸고, 하드웨어 인터페이스가 다시 실제 모터 컨트롤러 명령으로 변환합니다. 그리고 반대로 엔코더 값을 읽어 바퀴 상태와 주행 정보를 위쪽으로 올려보냅니다.5

이 구조가 좋은 이유는 주행 알고리즘이 모터 하드웨어를 몰라도 된다는 점입니다. 자율주행 노드는 "왼쪽 바퀴에 패킷 시리얼 CRC를 어떻게 붙일까?" 같은 걸 몰라도 됩니다. 그런 건 하드웨어 인터페이스의 일입니다.

혼자 개발할 때도 이 분리는 큰 차이를 만듭니다. 처음에는 아두이노와 간단한 모터 드라이버를 쓸 수 있고, 나중에는 엔코더 내장형 컨트롤러나 상용 모터 드라이버로 바꿀 수 있습니다. 이때 위쪽 노드를 다 갈아엎지 않으려면, 맨 아래쪽 번역기만 바꾸면 되도록 설계해야 합니다.

ros2_control을 쓰면 뭐가 좋아질까

작은 데모에서는 /cmd_vel을 구독하는 파이썬 노드 하나가 바로 시리얼 포트로 명령을 보내도 됩니다. 실제로 그 방식이 제일 빠르게 움직입니다. 다만 로봇이 커질수록 그 구조는 점점 버거워집니다.

ros2_control을 쓰면 얻는 장점은 크게 두 가지입니다. 첫째, 하드웨어를 추상화할 수 있습니다. 모터 종류나 통신 방식이 달라도 바깥에서는 명령 인터페이스와 상태 인터페이스라는 비슷한 형태로 보이게 만들 수 있습니다.4 둘째, 검증된 컨트롤러를 재사용할 수 있습니다. 예를 들어 차동 구동 로봇이라면 diff_drive_controller, 관절 상태를 퍼뜨릴 때는 joint_state_broadcaster처럼 이미 널리 쓰이는 구성 요소를 얹을 수 있습니다.3

쉽게 말해, 직접 모든 톱니바퀴를 깎는 대신 이미 잘 맞는 기어 세트를 가져다 쓰는 셈입니다.

물론 모든 프로젝트가 무조건 ros2_control이어야 하는 것은 아닙니다. 하지만 카메라, 라이다, 내비게이션, 시뮬레이션, 실제 하드웨어를 오가며 같은 구조를 유지하고 싶다면, 결국 이런 표준화의 힘이 커집니다.

TF를 빼먹으면 연결은 됐는데 이해는 못 한다

센서와 모터를 ROS 2로 연결할 때 초보자가 자주 놓치는 것이 하나 더 있습니다. 바로 TF입니다. TF는 로봇 안의 좌표 관계를 관리하는 체계입니다. 예를 들어 base_link, camera_link, laser_frame이 서로 어디에 붙어 있는지를 알려 줍니다.

카메라와 라이다를 각각 연결하는 데까지만 보면 TF는 귀찮아 보일 수 있습니다. 하지만 조금만 복잡한 일을 하려면 필수입니다. 라이다가 본 장애물이 로봇 기준 앞쪽인지 왼쪽인지 알려면, 센서 데이터가 어느 프레임 기준인지 알아야 합니다. 앞서 본 것처럼 LaserScan 메시지도 센서 프레임 정보를 함께 사용합니다.2

TF가 정확해야 센서 융합도 됩니다. 카메라와 라이다를 함께 쓰거나, 시뮬레이터에서 본 센서 데이터를 실제 ROS 2 그래프와 같은 방식으로 다루려면 프레임이 맞아야 합니다. 예를 들어 Isaac Sim은 ROS 2 브리지를 통해 라이다 데이터를 LaserScan이나 PointCloud2로 퍼블리시할 수 있고, 이때도 적절한 frameId를 맞추는 것이 중요합니다.6

즉 센서를 많이 달수록, 데이터 자체만큼 좌표계가 중요해집니다. 눈과 귀를 달았는데 몸 어디에 붙었는지 모르면 로봇은 계속 헷갈립니다.

실제로는 이런 식으로 나눈다

이제 책에서 계속 다뤄 온 작은 이동 로봇을 ROS 2 식으로 다시 배치해 보겠습니다. 목표는 카메라, 라이다, 명령 처리, 모터 제어를 깔끔하게 분리하는 것입니다.

  • camera_node: 카메라 이미지를 /camera/image_raw로 발행

  • lidar_node: 라이다 스캔을 /scan으로 발행

  • object_detector_node: 카메라 이미지를 받아 사람, 컵, 책, QR 코드 등을 인식

  • obstacle_avoider_node: /scan을 받아 가까운 장애물을 피하는 규칙 계산

  • teleop_node 또는 voice_cmd_node: 사람이 내린 조작 명령 생성

  • cmd_vel_mux 또는 safety_node: 여러 명령을 모아 최종 /cmd_vel 생성

  • diff_drive_controller 또는 motor_control_node: /cmd_vel을 바퀴 명령으로 변환

  • joint_state_broadcaster: 엔코더 기반 바퀴 상태 발행

  • robot_state_publisher: URDF를 기반으로 TF 발행

이렇게 나누면 흐름이 또렷해집니다. 카메라가 죽었을 때 물체 인식만 멈추고, 라이다 기반 장애물 회피는 살아 있을 수 있습니다. 반대로 라이다가 끊겨도 원격 조종 자체는 가능할 수 있습니다. 모든 기능이 하나의 파일에 들어 있을 때는 이런 분리가 거의 불가능합니다.

토픽 이름도 설계의 일부다

ROS 2에서는 코드만큼 이름도 중요합니다. 이름이 엉망이면 시스템이 커질수록 더 헷갈립니다. 개인 프로젝트라고 대충 지으면, 몇 주 뒤에 제일 먼저 고생하는 사람이 바로 자기 자신입니다.

처음부터 너무 거창할 필요는 없지만, 최소한 역할이 드러나는 이름을 쓰는 편이 좋습니다.

  • /camera/image_raw: 카메라 원본 이미지

  • /scan: 2D 라이다 스캔

  • /cmd_vel: 최종 속도 명령

  • /cmd_vel_teleop: 수동 조작 속도 명령

  • /joint_states: 관절 또는 바퀴 상태

  • /odom: 오도메트리 정보

이 정도만 지켜도 rqt_graph나 CLI로 시스템을 볼 때 이해 속도가 확 달라집니다. 이름은 설명서이기도 합니다.

bringup은 한 번에 켜지는 시작 버튼이다

노드를 잘 쪼갰다면, 이제 그것들을 한 번에 올리는 시작 스크립트가 필요합니다. ROS 2에서는 보통 launch 파일이 그 역할을 합니다.

실전에서는 센서 드라이버, robot_state_publisher, 컨트롤러 매니저, 각종 알고리즘 노드를 한 launch에서 묶습니다. 이렇게 해 두면 "카메라 먼저 켜고, 그다음 라이다 켜고, 그다음 모터 노드 켜고…"를 매번 손으로 하지 않아도 됩니다.

또 launch에 파라미터를 넣어 두면 테스트가 쉬워집니다. 예를 들어 실물 로봇에서는 실제 라이다 드라이버를 띄우고, 시뮬레이터에서는 가상 센서 브리지를 띄우는 식으로 구성을 바꿀 수 있습니다. 중요한 것은 노드 구조는 최대한 유지한 채, 아래쪽 구현만 바뀌게 하는 것입니다.

잘 연결됐는지는 어떻게 확인할까

ROS 2의 장점 중 하나는 연결 상태를 들여다보는 도구가 풍부하다는 점입니다. 로봇이 안 움직일 때 무작정 코드를 뜯기 전에, 먼저 그래프와 토픽부터 확인하는 습관이 좋습니다.

가장 기본적인 확인 순서는 이렇습니다.

  • ros2 node list로 노드가 다 살아 있는지 본다.1

  • ros2 topic list로 필요한 토픽이 생성됐는지 본다.

  • ros2 topic echo /scan 같은 명령으로 센서 데이터가 실제로 흐르는지 본다.

  • ros2 topic echo /cmd_vel로 최종 속도 명령이 나오는지 본다.

  • ros2 control list_controllers로 컨트롤러가 제대로 로드되고 활성화됐는지 본다.7

이 순서를 지키면 디버깅이 훨씬 쉬워집니다. 예를 들어 로봇이 안 움직여도 /cmd_vel이 잘 나오고 있다면, 문제는 상위 판단이 아니라 모터 제어 쪽일 가능성이 큽니다. 반대로 /scan 자체가 안 들어오면 회피 알고리즘을 의심할 필요가 없습니다. 센서부터 봐야 합니다.

로봇 디버깅의 핵심은 "어디서 끊겼는가"를 찾는 것입니다. ROS 2는 바로 그 끊긴 지점을 찾기 좋게 해 줍니다.

자주 하는 실수는 늘 비슷하다

처음 ROS 2로 센서와 모터를 엮을 때 많은 사람이 비슷한 함정에 빠집니다.

  • 센서 노드 안에서 모터를 직접 움직이게 만든다.

  • 여러 노드가 동시에 최종 /cmd_vel을 퍼블리시한다.

  • TF를 대충 넘겨서 카메라, 라이다, 몸체 좌표가 안 맞는다.

  • 상위 알고리즘 코드가 시리얼 포트, PWM, 모터 드라이버 세부 사항까지 알아버린다.

  • 긴급 정지를 마지막 단계가 아니라 중간 단계 어딘가에만 넣어 둔다.

이 실수들의 공통점은 역할 경계가 흐려진다는 것입니다. ROS 2의 구조는 기능을 많이 만들기 위한 장식이 아니라, 경계를 분명하게 긋기 위한 장치입니다.

이 장에서 꼭 가져가야 할 감각

이 장의 핵심은 기술 명령어보다 감각입니다. 카메라는 보고, 라이다는 재고, 명령 노드는 의도를 정리하고, 모터 제어 노드는 마지막 번역을 맡습니다. 그리고 TF는 이 모든 데이터를 같은 몸 안에서 이해 가능하게 묶습니다.

이렇게 나누면 로봇이 갑자기 똑똑해지지는 않습니다. 하지만 덜 망가지고, 덜 헷갈리고, 더 크게 키울 수 있게 됩니다. 그것이 한 파일짜리 장난감 코드를 실제 로봇 아키텍처로 바꾸는 첫걸음입니다.

다음 장에서 시뮬레이션을 다룰 때도 바로 이 구조가 이어집니다. 센서 노드와 모터 노드의 자리에 실제 하드웨어 대신 Gazebo나 Isaac Sim이 들어올 뿐입니다. 즉 구조를 잘 짜 두면, 현실과 가상의 차이는 생각보다 작아집니다.

정리

ROS 2로 센서와 모터를 연결한다는 것은 단순히 토픽 몇 개를 붙이는 일이 아닙니다. 로봇을 역할별 모듈로 나누고, 데이터 흐름을 정리하고, 하드웨어 의존성을 아래쪽으로 밀어내는 일입니다.

카메라 노드와 라이다 노드는 입력을 공급하고, 판단 노드는 그 입력을 해석하며, 명령 노드는 여러 의도를 정리하고, 모터 제어 노드는 최종적으로 물리 세계에 힘을 가합니다. ros2_control 같은 도구를 쓰면 이 구조를 더 표준적이고 재사용 가능하게 만들 수 있습니다.

중요한 것은 처음부터 완벽하게 짜는 것이 아닙니다. 다만 이제부터는 로봇 코드를 쓸 때, "이 기능은 어느 노드의 책임인가?"를 먼저 묻는 습관을 들이면 됩니다. 그 질문이 쌓일수록, 당신의 로봇은 점점 더 실제 시스템다운 모양이 됩니다.

참고

1Understanding nodes — ROS 2 Documentation

2Adding Lidar  Articulated Robotics

3ros2_control documentation

5ros2_control on the real robot

6RTX Lidar Sensors — Isaac Sim Documentation

7ros2_control Command Line Interface

책 목록

혼자 만드는 AI 로봇: 개인 개발자를 위한 현실적인 로봇 제작 로드맵

전체 목차 보기

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

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

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

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

참여하기