추상적인 노드가 코드가 되는 순간
지금까지 우리는 ros2 node list, ros2 topic list, ros2 run 같은 명령어로 ROS2 시스템을 바깥에서 관찰했다. 마치 유리창 너머로 로봇의 내부를 들여다본 셈이다. 이번 장에서는 드디어 그 유리창을 열고 안으로 들어간다. 직접 노드를 만들고, 실행하고, 로그를 남기고, ros2 node list에서 살아 움직이는 자신의 프로그램을 확인한다.
노드는 ROS2에서 독립적으로 실행되는 프로그램 단위다. 카메라를 읽는 노드, LiDAR 데이터를 처리하는 노드, 바퀴 속도를 계산하는 노드, 목표 지점까지의 경로를 계획하는 노드가 따로 존재할 수 있다. 이 장에서는 아직 센서나 모터를 다루지 않는다. 대신 가장 작은 노드를 만들면서 ROS2 프로그램이 어떤 순서로 태어나고, 실행되고, 종료되는지 몸으로 익힌다.
Python에서 ROS2 노드를 만들 때 가장 많이 사용하는 라이브러리는 rclpy다. rclpy는 Python 코드가 ROS2의 통신 시스템과 연결될 수 있도록 해 주는 공식 Python 클라이언트 라이브러리다.1 쉽게 말해, Python 프로그램을 ROS2 세계의 시민으로 등록해 주는 입구라고 생각하면 된다.
이번 장의 핵심 흐름은 다음 한 줄로 요약할 수 있다.
rclpy.init() → Node 생성 → rclpy.spin() → destroy_node() → rclpy.shutdown()이 흐름은 앞으로 퍼블리셔, 서브스크라이버, 서비스, 액션, 파라미터를 만들 때도 계속 반복된다. 처음에는 주문처럼 보이겠지만, 몇 번 직접 실행해 보면 로봇 프로그램의 기본 호흡처럼 느껴질 것이다.

실습 패키지 준비하기
이전 장에서 만든 워크스페이스가 ~/ros2_ws라고 가정하자. 아직 워크스페이스가 없다면 앞 장의 내용을 참고해 src 폴더까지 준비해야 한다. 이번 장에서는 Python 기반 패키지를 하나 만들고 그 안에 노드를 작성한다.
터미널을 열고 다음 명령을 실행한다.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python my_robot_nodes --dependencies rclpy std_msgs여기서 my_robot_nodes는 패키지 이름이다. 자율주행 로봇 프로젝트에서는 기능별로 패키지를 나누는 것이 좋지만, 첫 실습에서는 여러 노드를 한곳에 모아 구조를 익히는 편이 이해하기 쉽다.
패키지가 생성되면 대략 다음과 같은 구조가 생긴다.
my_robot_nodes/
├── package.xml
├── setup.cfg
├── setup.py
├── resource/
│ └── my_robot_nodes
└── my_robot_nodes/
├── __init__.py
└── ...주의할 점은 my_robot_nodes라는 폴더가 두 번 등장한다는 것이다. 바깥쪽 my_robot_nodes는 ROS2 패키지 폴더이고, 안쪽 my_robot_nodes는 Python 모듈 폴더다. 우리가 Python 노드 파일을 넣을 곳은 안쪽 폴더다.
cd ~/ros2_ws/src/my_robot_nodes/my_robot_nodes이제 이 안에 첫 번째 노드 파일을 만든다.
touch hello_function_node.py가장 작은 Python 노드 작성하기
먼저 클래스도, 타이머도, 토픽도 없는 단순한 노드를 만들어 보자. hello_function_node.py를 열고 다음 코드를 입력한다.
import rclpy
from rclpy.node import Node
def main(args=None):
rclpy.init(args=args)
node = Node('hello_robot_node')
try:
node.get_logger().info('Hello, ROS2 robot!')
node.get_logger().info('이 노드는 지금 살아 있습니다. Ctrl+C로 종료하세요.')
rclpy.spin(node)
except KeyboardInterrupt:
node.get_logger().info('사용자가 노드를 종료했습니다.')
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()이 코드는 짧지만 ROS2 노드의 기본 생명주기를 모두 담고 있다.
rclpy.init(args=args)는 Python 프로그램을 ROS2 시스템과 연결할 준비를 한다. 이 호출이 있어야 노드를 만들고, 토픽을 발행하고, 서비스를 열 수 있다. 아직 로봇을 움직이지 않았지만, 이 순간부터 프로그램은 ROS2 네트워크에 참여할 준비를 마친다.
Node('hello_robot_node')는 실제 노드 객체를 만든다. 여기서 'hello_robot_node'는 노드 이름이다. 파일 이름이 hello_function_node.py이고, 나중에 실행 파일 이름을 hello_node로 등록하더라도, ROS2 그래프에서 보이는 이름은 hello_robot_node다. 이 세 이름은 서로 다를 수 있다.
node.get_logger().info()는 ROS2 방식의 로그 출력이다. 단순히 print()를 써도 화면에는 글자가 나오지만, ROS2 로그는 시간, 노드 이름, 로그 레벨을 함께 관리할 수 있다. 여러 노드가 동시에 떠 있는 로봇 시스템에서는 이 차이가 매우 크다. 터미널에 수십 줄의 메시지가 쏟아질 때 "누가 말했는가"를 아는 것은 디버깅의 시작이다.
rclpy.spin(node)은 노드를 계속 실행 상태로 유지한다. 지금 예제에서는 주기적으로 할 일이 없지만, 나중에 타이머, 서브스크립션, 서비스가 추가되면 spin()이 콜백을 처리한다. 다시 말해, spin()은 노드가 ROS2 세계에서 계속 귀를 열고 있도록 만드는 함수다.
마지막으로 node.destroy_node()와 rclpy.shutdown()은 정리 단계다. 노드를 제거하고 ROS2 통신 자원을 종료한다. 실제 로봇에서는 종료 처리가 중요하다. 모터 제어 노드가 어설프게 죽거나, 센서 장치가 닫히지 않거나, 마지막 속도 명령이 남아 있으면 이상한 동작으로 이어질 수 있다.
setup.py에 실행 파일 등록하기
Python 파일을 만들었다고 해서 바로 ros2 run으로 실행할 수 있는 것은 아니다. ROS2에게 "이 Python 파일의 main() 함수를 실행 파일처럼 노출해 달라"고 알려 주어야 한다. 그 역할을 하는 곳이 setup.py의 console_scripts 항목이다. ROS2 공식 Python 튜토리얼에서도 Python 노드를 패키지 실행 파일로 등록할 때 이 entry point 방식을 사용한다.2
다음 파일을 연다.
cd ~/ros2_ws/src/my_robot_nodes
nano setup.pyentry_points 부분을 찾아 다음처럼 수정한다.
entry_points={
'console_scripts': [
'hello_node = my_robot_nodes.hello_function_node:main',
],
},왼쪽의 hello_node는 ros2 run에서 사용할 실행 이름이다. 오른쪽의 my_robot_nodes.hello_function_node:main은 Python 모듈 경로와 실행할 함수 이름이다.
이 관계를 한 번 더 분리해서 보자.
ros2 run에서 쓰는 이름 hello_node
Python 파일 이름 hello_function_node.py
ROS2 그래프의 노드 이름 hello_robot_node처음에는 이름이 여러 개라 헷갈릴 수 있다. 하지만 이 구분은 중요하다. 실행 파일 이름은 사용자가 터미널에서 선택하는 입구이고, 파일 이름은 코드가 저장된 위치이며, 노드 이름은 ROS2 시스템 안에서 식별되는 이름이다.
빌드하고 실행하기
이제 워크스페이스 루트로 돌아가 빌드한다.
cd ~/ros2_ws
colcon build --packages-select my_robot_nodes --symlink-install--packages-select my_robot_nodes는 이 패키지만 빌드하겠다는 뜻이다. 워크스페이스에 패키지가 많아질수록 빌드 시간을 줄이는 데 도움이 된다. --symlink-install은 Python 개발에서 특히 편리하다. 코드를 조금 수정했을 때 설치 공간에 복사본을 매번 새로 만들기보다 원본 파일을 참조하게 해 주기 때문이다. 다만 setup.py의 entry point를 바꾸었다면 다시 빌드하는 습관을 들이는 것이 안전하다.
빌드가 끝났다면 현재 터미널에 워크스페이스 환경을 적용한다.
source install/setup.bash이제 노드를 실행한다.
ros2 run my_robot_nodes hello_node터미널에는 대략 다음과 비슷한 출력이 보일 것이다.
[INFO] [시간] [hello_robot_node]: Hello, ROS2 robot!
[INFO] [시간] [hello_robot_node]: 이 노드는 지금 살아 있습니다. Ctrl+C로 종료하세요.여기서 눈여겨볼 부분은 [hello_robot_node]다. 이것이 ROS2 그래프에서의 노드 이름이다.
노드가 계속 실행 중인 상태에서 새 터미널을 하나 더 열어 보자. 새 터미널에도 ROS2 환경과 워크스페이스 환경이 적용되어 있어야 한다.
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 node list다음처럼 보이면 성공이다.
/hello_robot_node첫 번째 터미널로 돌아가 Ctrl+C를 누르면 종료 로그가 출력되고 노드가 내려간다. 다시 ros2 node list를 실행하면 목록에서 사라진 것을 확인할 수 있다. 아주 작은 코드지만, 방금 우리는 ROS2 시스템에 노드를 등록하고, 실행 상태를 확인하고, 정상 종료까지 경험했다.
함수형 노드와 클래스 기반 노드
방금 만든 노드는 단순 함수형 구조다. main() 함수 안에서 Node 객체를 직접 만들고, 로그를 찍고, spin()을 호출했다. 이런 방식은 "정말 최소한의 노드가 어떻게 생겼는가"를 이해하기에 좋다.
하지만 실제 로봇 소프트웨어는 곧 복잡해진다. 1초마다 상태를 확인해야 할 수도 있고, LiDAR 메시지를 받아야 할 수도 있고, /cmd_vel로 속도 명령을 보내야 할 수도 있다. 서비스를 통해 "지금 정지해도 되는가?" 같은 요청을 처리해야 할 수도 있다. 이런 기능이 하나둘 늘어나면 main() 함수 안에 모든 것을 몰아넣는 방식은 금세 지저분해진다.
그래서 ROS2 Python 노드는 보통 Node 클래스를 상속해서 만든다. 클래스 기반 구조는 노드가 가진 상태와 기능을 한곳에 묶어 준다. 타이머 객체, 퍼블리셔 객체, 카운터 변수, 콜백 함수를 모두 같은 클래스 안에 두면 코드가 자연스럽게 정리된다.
비유하자면 함수형 노드는 임시 작업대 위에 부품을 늘어놓고 실험하는 방식이다. 반면 클래스 기반 노드는 부품마다 자리를 정해 둔 공구함에 가깝다. 간단한 실험은 작업대가 빠르지만, 로봇 한 대를 끝까지 조립하려면 공구함이 필요하다.
타이머로 주기적인 작업 실행하기
이제 클래스 기반 노드를 만들어 보자. 이번 노드는 1초마다 "살아 있다"는 신호를 로그로 출력한다. 실제 로봇에서는 이런 신호를 heartbeat라고 부르기도 한다. 심장 박동처럼 주기적으로 상태를 알려 주는 메시지라는 뜻이다.
안쪽 Python 모듈 폴더로 이동해 새 파일을 만든다.
cd ~/ros2_ws/src/my_robot_nodes/my_robot_nodes
touch heartbeat_node.pyheartbeat_node.py에 다음 코드를 작성한다.
import rclpy
from rclpy.node import Node
class RobotHeartbeat(Node):
def __init__(self):
super().__init__('robot_heartbeat')
self.count = 0
self.timer = self.create_timer(1.0, self.timer_callback)
self.get_logger().info('Robot heartbeat node started.')
def timer_callback(self):
self.count += 1
self.get_logger().debug(f'내부 카운터 값: {self.count}')
self.get_logger().info(f'로봇 상태 확인 중... heartbeat={self.count}')
if self.count % 5 == 0:
self.get_logger().warn('5초 동안 heartbeat만 출력 중입니다. 아직 센서는 연결하지 않았습니다.')
if self.count == 12:
self.get_logger().error('예시 오류 로그: 가상의 센서 응답이 없다고 가정합니다.')
def main(args=None):
rclpy.init(args=args)
node = RobotHeartbeat()
try:
rclpy.spin(node)
except KeyboardInterrupt:
node.get_logger().info('Heartbeat node interrupted by user.')
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()여기서 가장 중요한 줄은 다음이다.
self.timer = self.create_timer(1.0, self.timer_callback)create_timer()는 일정한 주기마다 콜백 함수를 호출하는 타이머를 만든다. 첫 번째 인자 1.0은 1초를 의미하고, 두 번째 인자 self.timer_callback은 타이머가 만료될 때 실행할 함수다. rclpy의 Node API에서 타이머는 지정한 초 단위 주기마다 사용자가 제공한 콜백을 호출하도록 설계되어 있다.3
중요한 점은 타이머가 혼자서 저절로 실행되는 것이 아니라는 사실이다. rclpy.spin(node)이 실행되고 있어야 타이머 콜백도 처리된다. 만약 spin() 없이 프로그램이 끝나 버리면 타이머는 일할 기회를 얻지 못한다.
이제 setup.py에 실행 파일을 하나 더 등록한다.
entry_points={
'console_scripts': [
'hello_node = my_robot_nodes.hello_function_node:main',
'heartbeat_node = my_robot_nodes.heartbeat_node:main',
],
},다시 빌드하고 환경을 적용한다.
cd ~/ros2_ws
colcon build --packages-select my_robot_nodes --symlink-install
source install/setup.bash실행해 보자.
ros2 run my_robot_nodes heartbeat_node1초마다 로그가 출력될 것이다. 5초마다 경고 로그가 나오고, 12초째에는 예시 오류 로그가 한 번 출력된다. 실제 오류가 발생한 것은 아니다. 로그 레벨의 차이를 눈으로 보기 위한 장치다.
로그 레벨로 노드의 상태 읽기
ROS2 노드에서는 로그를 단순한 출력문이 아니라 상태 신호로 사용해야 한다. 특히 자율주행 로봇에서는 여러 노드가 동시에 움직인다. 위치 추정 노드, 경로 계획 노드, 제어 노드, 센서 노드가 각자 말을 하기 시작하면 터미널은 금세 시끄러운 관제실이 된다. 이때 로그 레벨을 잘 나누면 어떤 메시지를 먼저 봐야 할지 판단할 수 있다.
대표적인 로그 레벨은 다음과 같이 사용할 수 있다.
debug는 개발 중 내부 값을 자세히 볼 때 사용한다. 예를 들어 엔코더 tick, 필터 입력값, 임시 계산 결과처럼 평소에는 숨겨도 되는 정보에 적합하다.info는 정상적인 상태 변화를 알릴 때 사용한다. 노드 시작, 주기적인 상태 확인, 목표 수신 같은 메시지가 여기에 해당한다.warn은 지금 당장 실패는 아니지만 주의해야 하는 상황에 사용한다. 배터리가 낮거나, 센서 응답이 느리거나, 지도 품질이 의심스러운 경우가 예다.error는 기대한 동작이 실패했을 때 사용한다. 모터 명령 전송 실패, 필수 센서 미응답, 목표 경로 생성 실패처럼 조치가 필요한 상황에 어울린다.
기본 실행에서는 debug 로그가 보이지 않을 수 있다. 디버그 로그까지 보고 싶다면 실행할 때 로그 레벨을 지정한다.
ros2 run my_robot_nodes heartbeat_node --ros-args --log-level debug그러면 self.get_logger().debug()로 출력한 내부 카운터 값도 보인다. 이 기능은 로봇이 이상하게 움직일 때 매우 유용하다. 평소에는 info 이상의 로그만 보다가, 문제가 생겼을 때 특정 노드만 debug로 올려 자세한 내부 상태를 볼 수 있기 때문이다.
실제 로봇에서 좋은 로그는 블랙박스와 같다. 사고가 난 뒤 "왜 그랬을까?"를 추측하는 대신, 노드가 남긴 기록을 따라가며 원인을 좁힐 수 있다.
코드 흐름을 로봇 실행 흐름으로 읽기
클래스 기반 노드의 흐름을 다시 차분히 따라가 보자.
rclpy.init(args=args)
node = RobotHeartbeat()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()rclpy.init()에서 ROS2 통신 준비가 시작된다. 아직 노드는 없다. 무대의 조명이 켜지고 통신 장비가 준비되는 단계다.
RobotHeartbeat()에서 노드 객체가 만들어진다. 이때 __init__() 안의 코드가 실행되며 노드 이름이 등록되고 타이머가 생성된다. 로봇 관점에서는 "상태 감시 담당자가 자리에 앉는 순간"이다.
rclpy.spin(node)은 그 담당자가 계속 근무하도록 만든다. 이 줄이 실행되는 동안 타이머 콜백, 구독 콜백, 서비스 요청 콜백 같은 일들이 처리된다. 지금은 타이머만 있지만, 앞으로 토픽과 서비스가 추가되어도 같은 원리로 움직인다.
사용자가 Ctrl+C를 누르면 KeyboardInterrupt가 발생하고, finally 블록에서 정리 작업이 실행된다. destroy_node()는 노드를 제거하고, shutdown()은 ROS2 통신을 종료한다.
이 흐름을 이해하면 ROS2 코드가 훨씬 덜 낯설어진다. 복잡한 Nav2 노드도, SLAM 노드도, 센서 드라이버도 결국은 비슷한 생명주기를 가진다. 다만 그 안에 들어 있는 콜백과 상태가 훨씬 많을 뿐이다.
퍼블리셔와 서비스가 늘어나면 왜 클래스가 편할까
아직 본격적인 토픽 통신은 다음 장 이후에 자세히 다룰 것이다. 그래도 클래스 기반 구조가 왜 중요한지 감을 잡기 위해, 노드가 조금 더 커지는 상황을 상상해 보자.
예를 들어 이동 로봇에 robot_status_node라는 노드가 있다고 하자. 이 노드는 1초마다 배터리 상태를 확인하고, 상태 메시지를 토픽으로 발행하며, 외부에서 "현재 안전한가?"라는 서비스 요청이 들어오면 응답해야 한다. 함수형 구조라면 타이머, 퍼블리셔, 서비스 객체, 배터리 상태 변수, 콜백 함수가 여기저기 흩어지기 쉽다.
클래스 기반 구조에서는 이런 요소들이 자연스럽게 한 몸이 된다.
class RobotStatus(Node):
def __init__(self):
super().__init__('robot_status')
self.battery_percent = 100
self.timer = self.create_timer(1.0, self.timer_callback)
# self.publisher = self.create_publisher(...)
# self.service = self.create_service(...)
def timer_callback(self):
self.battery_percent -= 1
self.get_logger().info(f'battery={self.battery_percent}%')
def service_callback(self, request, response):
# 요청을 읽고 응답을 채우는 코드가 들어갈 수 있다.
return response아직 주석으로 남겨 둔 퍼블리셔와 서비스는 뒤에서 하나씩 실제 코드로 바뀔 것이다. 지금은 구조만 보면 된다. 노드가 가진 상태는 self.battery_percent처럼 멤버 변수로 보관하고, 주기적인 일은 timer_callback()에 두고, 요청을 처리하는 일은 service_callback()에 둔다. 기능이 늘어날수록 클래스 기반 노드는 가독성, 유지보수성, 확장성에서 유리해진다.
로봇 소프트웨어는 처음부터 거대하게 시작하지 않는다. 작은 노드 하나로 출발해 센서가 붙고, 토픽이 붙고, 서비스가 붙고, 파라미터가 붙는다. 그래서 첫 노드를 만들 때부터 확장 가능한 모양을 익혀 두는 것이 좋다.
여러 노드를 동시에 실행해 보기
ROS2의 진짜 힘은 여러 노드가 동시에 실행될 때 드러난다. 지금 만든 heartbeat_node 하나만 실행하면 단순한 프로그램처럼 보인다. 하지만 같은 실행 파일을 서로 다른 이름과 네임스페이스로 실행하면, 독립적인 노드들이 동시에 존재하는 모습을 확인할 수 있다.
첫 번째 터미널에서 다음 명령을 실행한다.
source ~/ros2_ws/install/setup.bash
ros2 run my_robot_nodes heartbeat_node --ros-args -r __node:=front_heartbeat -r __ns:=/robot1두 번째 터미널에서는 다음 명령을 실행한다.
source ~/ros2_ws/install/setup.bash
ros2 run my_robot_nodes heartbeat_node --ros-args -r __node:=rear_heartbeat -r __ns:=/robot1세 번째 터미널에서 노드 목록을 확인한다.
source ~/ros2_ws/install/setup.bash
ros2 node list다음과 비슷한 결과가 보인다.
/robot1/front_heartbeat
/robot1/rear_heartbeat여기서 __node:=front_heartbeat는 노드 이름을 실행 시점에 바꾸는 remapping이다. __ns:=/robot1은 네임스페이스를 지정한다. 네임스페이스는 노드를 묶어 주는 주소 공간이다. 같은 이름의 노드나 토픽이라도 네임스페이스가 다르면 서로 구분할 수 있다.
자율주행 로봇을 한 대만 다룰 때는 네임스페이스가 꼭 필요 없어 보일 수 있다. 하지만 로봇이 두 대가 되면 이야기가 달라진다. /robot1/scan과 /robot2/scan을 구분하지 못하면 어느 로봇의 LiDAR 데이터인지 알 수 없다. 같은 로봇 안에서도 /front_camera/image와 /rear_camera/image처럼 위치나 역할에 따라 이름을 나누는 일이 흔하다.
로그 출력에서도 노드 이름은 중요하다. 두 터미널에서 같은 코드가 실행되고 있지만 하나는 front_heartbeat, 다른 하나는 rear_heartbeat라는 이름으로 말한다. 나중에 센서 노드, 제어 노드, 계획 노드가 동시에 떠 있을 때 이 이름이 시스템을 이해하는 지도 역할을 한다.
C++ 노드는 어떻게 다를까
ROS2는 Python만 지원하지 않는다. C++에서는 rclcpp를 사용해 노드를 만든다. Python의 rclpy와 C++의 rclcpp는 언어는 다르지만 기본 흐름은 놀랄 만큼 비슷하다. C++ 노드도 초기화하고, 노드를 만들고, spin으로 실행을 유지하고, 마지막에 shutdown한다. C++ 예제에서도 rclcpp::init(), std::make_shared로 노드 생성, rclcpp::spin(), rclcpp::shutdown()의 흐름이 일반적으로 사용된다.4
가장 단순한 C++ 노드는 다음과 같은 모습이다.
#include "rclcpp/rclcpp.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("cpp_hello_node");
RCLCPP_INFO(node->get_logger(), "Hello from rclcpp!");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}Python 코드와 비교해 보자.
rclpy.init(args=args)
node = Node('hello_robot_node')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()표현은 다르지만 뼈대는 같다. ROS2를 초기화하고, 노드를 만들고, 실행 상태를 유지하고, 종료한다. 이 공통 구조를 이해하면 Python으로 먼저 배우더라도 C++ 노드가 완전히 낯설지는 않다.
그렇다면 언제 Python을 쓰고 언제 C++를 써야 할까? 빠르게 실험하고, 로직을 바꾸고, 도구 노드를 만들고, 데이터 흐름을 확인하는 데는 Python이 편하다. 코드가 짧고 읽기 쉬우며 개발 속도가 빠르다. 반면 높은 주기에서 제어해야 하거나, 성능이 중요하거나, 타입 안정성이 중요한 하위 제어 모듈은 C++가 더 적합할 수 있다.
실제 로봇 프로젝트에서는 두 언어를 함께 쓰는 경우가 많다. 예를 들어 고속 모터 제어 노드는 C++로 작성하고, 실험용 상태 모니터링 노드나 데이터 기록 보조 노드는 Python으로 작성할 수 있다. ROS2에서는 서로 다른 언어로 만든 노드도 같은 토픽, 서비스, 액션을 통해 통신할 수 있다. 이것이 ROS2가 로봇 소프트웨어 프레임워크로 강력한 이유 중 하나다.
자주 만나는 오류와 점검 순서
첫 노드를 만들 때는 코드보다 환경 문제에서 더 자주 막힌다. 오류 메시지가 길게 나오더라도 당황하지 말고 순서대로 좁혀 가면 된다.
ros2 run에서 패키지를 찾지 못한다면 워크스페이스를 빌드했는지, 그리고source ~/ros2_ws/install/setup.bash를 실행했는지 확인한다.실행 파일을 찾지 못한다면
setup.py의console_scripts이름과 Python 모듈 경로가 맞는지 확인하고 다시 빌드한다.ModuleNotFoundError: No module named 'rclpy'가 나온다면 ROS2 환경이 적용되지 않았을 가능성이 크다. 새 터미널에서source /opt/ros/humble/setup.bash를 먼저 실행한다.ros2 node list에 노드가 보이지 않는다면 프로그램이 이미 종료되었는지 확인한다. 노드가 계속 보이려면rclpy.spin(node)으로 실행 상태가 유지되어야 한다.로그가 너무 적게 보인다면 실행할 때
--ros-args --log-level debug를 붙여 디버그 로그까지 표시해 본다.
로봇 개발에서 디버깅은 "큰 원인을 한 번에 맞히는 일"이 아니다. 노드가 실행 중인지 보고, 이름이 맞는지 보고, 로그가 찍히는지 보고, 그다음 통신을 확인한다. 작은 확인을 차례로 쌓으면 복잡한 문제도 결국 좁혀진다.
첫 번째 노드에서 얻은 것
이번 장에서 만든 노드는 아직 로봇을 움직이지 않는다. 지도도 만들지 않고, 장애물도 피하지 않는다. 하지만 가장 중요한 문을 열었다. 이제 ROS2의 노드는 더 이상 추상적인 단어가 아니다. 직접 작성한 Python 파일이고, setup.py에 등록된 실행 파일이며, ros2 run으로 실행되고, ros2 node list에 나타나는 실제 프로세스다.
특히 다음 흐름을 확실히 기억하자.
초기화한다 → 노드를 만든다 → spin으로 콜백을 처리한다 → 노드를 정리한다 → 종료한다앞으로 토픽을 발행해 속도 명령을 보내고, LiDAR 메시지를 구독하고, 서비스로 상태를 묻고, 액션으로 목표 지점 이동을 요청해도 이 기본 흐름은 계속 남아 있다. 복잡한 로봇 시스템은 결국 작은 노드들의 조합이다. 그리고 그 첫 번째 노드는 방금 우리가 만든 hello_robot_node와 robot_heartbeat처럼 단순한 코드에서 시작한다.
다음 장에서는 노드를 단순히 "만드는 방법"을 넘어, 로봇 기능을 어떤 기준으로 노드에 나누어 담아야 하는지 살펴본다. 센서 노드, 제어 노드, 계획 노드가 각자 어떤 책임을 가져야 하는지 이해하면 ROS2 시스템의 전체 그림이 훨씬 선명해진다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.