ROS 2 — 코드로 첫 노드 짜기: 거북이를 스스로 움직이게 (rclpy)
키보드도 명령어도 아닌, 직접 짠 파이썬 노드로 거북이를 움직인다. 발행 노드로 원을 그리고, 구독 노드로 위치를 읽으며 노드 코드의 뼈대를 한 줄씩 친절하게 푼다.
명령어를 한 줄씩 손으로 쳐서 거북이를 움직일 수도 있다. 하지만 진짜 로봇이라면 사람이 매번 명령을 칠 수는 없다. 프로그램이 알아서 판단하고 움직여야 한다. 그 “프로그램”이 바로 노드이고, 이번 글에서 처음으로 그걸 직접 코드로 짠다.
목표는 두 가지다.
- 발행 노드 — 거북이가 스스로 원을 그리게 하는 노드
- 구독 노드 — 거북이의 현재 위치를 읽어서 출력하는 노드
파이썬으로 짜고, 패키지를 만들 것도 없이 파일 하나로 바로 돌린다. 코드 한 줄 한 줄이 무슨 뜻인지 다 풀어줄 테니, 프로그래밍이 익숙하지 않아도 따라올 수 있다.
1. 거북이부터 띄우기
코드가 움직일 대상인 거북이를 먼저 띄운다. 터미널을 열고:
$ source /opt/ros/jazzy/setup.bash
$ ros2 run turtlesim turtlesim_node
→ 파란 창에 거북이 한 마리가 뜬다. 이 터미널은 그대로 둔다. (source 는 그 터미널에게 ROS가 어디 있는지 알려주는 한 줄이다. 새 터미널을 열 때마다 한 번씩 쳐준다.)
2. rclpy — 파이썬으로 노드를 짜는 도구
노드를 코드로 짜려면 도구가 필요한데, 그게 rclpy 다.
✋ 잠깐 — rclpy가 뭐죠? rclpy(아르씨엘파이)는 ROS를 파이썬에서 다루게 해주는 라이브러리다. ROS Client Library for Python 의 줄임말. 거북이에게 명령을 보내거나, 거북이가 보내는 정보를 받는 일을 파이썬 코드로 할 수 있게 해준다. (C++을 쓰고 싶으면
rclcpp라는 짝꿍이 있다. 하는 일은 같다.)
3. 발행 노드 짜기 — 거북이를 원으로 돌리기
거북이는 /turtle1/cmd_vel(속도 명령) 통로로 움직임 속도를 받는다. 그 통로에 “앞으로 가면서 동시에 돌아라” 를 0.5초마다 보내면 거북이는 원을 그린다. 이걸 하는 노드를 짠다.
아무 폴더에나 draw_circle.py 라는 파일을 만들고, 아래를 그대로 적는다.
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class DrawCircle(Node):
def __init__(self):
super().__init__('draw_circle') # ① 노드 이름 짓기
self.publisher = self.create_publisher(Twist, '/turtle1/cmd_vel', 10) # ② 발행자 만들기
self.timer = self.create_timer(0.5, self.tick) # ③ 0.5초마다 tick 부르기
self.get_logger().info('원 그리기 시작 — /turtle1/cmd_vel 발행')
def tick(self): # ④ 0.5초마다 실행되는 함수
msg = Twist()
msg.linear.x = 2.0 # 앞으로 2.0 (직선 속도)
msg.angular.z = 1.0 # 동시에 회전 1.0 (도는 속도) → 둘이 합쳐져 원
self.publisher.publish(msg) # ⑤ 통로에 메시지 내보내기
def main():
rclpy.init() # ⓐ ROS 켜기
node = DrawCircle() # ⓑ 노드 만들기
rclpy.spin(node) # ⓒ 계속 돌면서 사건을 처리 (핵심)
node.destroy_node() # ⓓ 정리
rclpy.shutdown()
if __name__ == '__main__':
main()
낯설어 보여도, 칸을 나눠 보면 이름표 붙이기에 가깝다. 하나씩 보자.
- ①
super().__init__('draw_circle')— 이 노드의 이름을draw_circle로 짓는다.ros2 node list에 뜨는 그 이름이다 - ②
create_publisher(Twist, '/turtle1/cmd_vel', 10)— 발행자(보내는 담당)를 만든다. 괄호 안 세 개가 핵심이다 — 무슨 양식(Twist)을 어느 통로(/turtle1/cmd_vel)로, 우편함 몇 칸(10)으로 보낼지 - ③
create_timer(0.5, self.tick)— 0.5초마다tick함수를 부르라고 예약한다. 사람이 키를 연타하는 걸 코드가 대신하는 셈 - ④⑤
tick안에서Twist메시지를 만들어 속도를 채우고(linear.x,angular.z),publish()로 통로에 내보낸다
✋ 잠깐 —
Twist가 뭐였죠? 거북이에게 “얼마나 빨리 직진하고, 얼마나 빨리 돌지” 를 한 묶음으로 적은 쪽지다.linear.x= 앞뒤 속도,angular.z= 도는 속도. 둘 다 주면 돌면서 앞으로 = 원이 된다.
main() 안의 ⓐ~ⓓ는 모든 노드가 똑같이 거치는 절차다. 다음 절에서 따로 본다.
4. 돌려보기 — 거북이가 스스로 돈다
거북이(turtlesim_node)는 §1에서 띄워둔 채로 있어야 한다. 새 터미널을 열어 파일을 실행한다.
$ source /opt/ros/jazzy/setup.bash
$ python3 draw_circle.py
[INFO] [draw_circle]: 원 그리기 시작 — /turtle1/cmd_vel 발행
→ 파란 창의 거북이가 스스로 원을 그리며 돈다! 키보드를 누르지도, 명령어를 치지도 않았는데. 우리가 짠 프로그램이 0.5초마다 알아서 속도를 보내고 있기 때문이다. 멈추려면 이 터미널에서 Ctrl+C.
방금 한 일이 바로 첫 노드다. 거창한 게 아니라 — 통로에 메시지를 보내는 프로그램 한 개.
5. 노드의 뼈대는 늘 똑같다 (5단계)
main() 의 다섯 줄을 일반화하면, 어떤 노드를 짜든 골격이 똑같다.
발행 노드든 구독 노드든, 나중에 더 복잡해지든, 이 5단계 위에 살이 붙을 뿐이다. 가운데 ③번(발행자/구독자/타이머 등록)만 그때그때 달라진다.
6. spin 이 뭐냐 — 계속 기다리는 일
rclpy.spin(node) 한 줄이 처음엔 낯설다. 왜 while True: 로 직접 안 돌리고 spin 한테 맡길까?
✋ 잠깐 — spin과 콜백 노드는 내가 일일이 시키는 게 아니라, 어떤 사건이 생기면 그에 반응하는 식으로 동작한다.
- 타이머 0.5초가 됐다 →
tick실행- 거북이가 위치를 보냈다 → (다음 절의)
on_pose실행이렇게 “사건이 생기면 불러달라” 고 미리 맡겨둔 함수를 콜백(callback) 이라 한다.
spin은 그 사건들을 무한히 기다렸다가 콜백을 불러주는 역할이다. 그래서 노드 코드엔 보통while이 없다 — 콜백들과, 그걸 돌려주는spin한 줄만 있다.
7. 구독 노드 짜기 — 거북이 위치 읽기
이번엔 반대로, 거북이가 내보내는 정보를 받는 노드를 짠다. 거북이는 자기 위치를 /turtle1/pose 통로로 계속 보내고 있다. 그걸 받아서 화면에 찍자.
read_pose.py 파일을 만든다.
import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose
class ReadPose(Node):
def __init__(self):
super().__init__('read_pose')
self.subscription = self.create_subscription( # 구독자 만들기
Pose, '/turtle1/pose', self.on_pose, 10) # 양식, 통로, 콜백, 우편함
self.get_logger().info('위치 읽기 시작 — /turtle1/pose 구독')
def on_pose(self, msg): # 위치가 올 때마다 실행
self.get_logger().info(
f'거북이 위치: x={msg.x:.2f}, y={msg.y:.2f}, 방향={msg.theta:.2f}')
def main():
rclpy.init()
node = ReadPose()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
발행 노드와 뼈대는 완전히 같고, 가운데만 바뀌었다.
create_subscription(Pose, '/turtle1/pose', self.on_pose, 10)— 구독자(받는 담당)를 만든다. 양식(Pose) + 통로(/turtle1/pose) + 콜백(self.on_pose) + 우편함 크기(10)- 발행 노드엔 없던 콜백이 들어간다.
/turtle1/pose에 위치가 도착할 때마다on_pose(msg)가 불린다.msg가 방금 도착한 위치 정보
발행은 내가 때맞춰 보내는 일이라 타이머를, 구독은 남이 보낼 때 반응하는 일이라 콜백을 쓴다. 같은 “사건 → 콜백” 의 두 얼굴이다.
8. 돌려보기 — 움직이는 거북이의 위치가 찍힌다
§4의 draw_circle.py 를 켜둔 채로, 또 새 터미널에서 위치 읽기 노드를 실행한다.
$ source /opt/ros/jazzy/setup.bash
$ python3 read_pose.py
[INFO] [read_pose]: 위치 읽기 시작 — /turtle1/pose 구독
[INFO] [read_pose]: 거북이 위치: x=5.96, y=5.85, 방향=1.34
[INFO] [read_pose]: 거북이 위치: x=6.21, y=6.19, 방향=1.71
[INFO] [read_pose]: 거북이 위치: x=6.30, y=6.58, 방향=2.08
→ 거북이가 원을 그리며 도는 동안, 그 위치 숫자가 실시간으로 바뀌며 찍힌다. 보내는 노드(draw_circle)와 받는 노드(read_pose)가 동시에 돌면서, 거북이를 사이에 두고 한쪽은 명령을 보내고 한쪽은 상태를 읽는 것이다.
✏️ 직접 바꿔보기
draw_circle.py의 숫자를 바꿔 다시 돌려보자.
msg.angular.z = 0.0→ 안 돌고 똑바로 앞으로만 (벽까지 직진)msg.linear.x = 1.0,msg.angular.z = 2.0→ 더 작은 원msg.linear.x = 0.0,msg.angular.z = 1.0→ 제자리에서 빙글빙글
9. 막히면 — 증상 → 원인
| 이런 증상이면 | 십중팔구 원인 |
|---|---|
ModuleNotFoundError: No module named 'rclpy' | 그 터미널에서 source 안 함 → source /opt/ros/jazzy/setup.bash 먼저 |
| 코드는 도는데 거북이가 가만히 | 거북이(turtlesim_node)를 안 띄웠거나, 통로 이름 /turtle1/cmd_vel 오타 |
| 위치 로그가 안 찍힘 | /turtle1/pose 오타, 또는 거북이가 안 떠 있음 |
python3: can't open file 'draw_circle.py' | 파일을 만든 폴더가 아닌 데서 실행함. 같은 폴더에서 실행 |
들여쓰기 에러(IndentationError) | 파이썬은 들여쓰기가 문법이다. 칸 수를 코드와 똑같이 |
한 줄로 박아둘 것
- 노드 = 통로로 메시지를 주고받는 프로그램 한 개. 그걸 파이썬으로 짜는 도구가 rclpy
- 노드 뼈대는 늘 5단계 —
init → 노드 만들기 → 발행/구독 등록 → spin → shutdown - 발행 노드 =
create_publisher(양식, 통로, 우편함)+ 보통 타이머로 주기적으로publish - 구독 노드 =
create_subscription(양식, 통로, 콜백, 우편함)+ 메시지가 올 때마다 콜백 실행 spin은 “사건이 생기면 콜백을 불러주는” 무한 기다림 — 그래서while없이 콜백만 짜면 된다- 패키지 없이
python3 파일.py로 바로 돌아간다 (단,source된 터미널에서)