본문으로 건너뛰기

ROS 2 — 코드로 첫 노드 짜기: 거북이를 스스로 움직이게 (rclpy)

키보드도 명령어도 아닌, 직접 짠 파이썬 노드로 거북이를 움직인다. 발행 노드로 원을 그리고, 구독 노드로 위치를 읽으며 노드 코드의 뼈대를 한 줄씩 친절하게 푼다.

명령어를 한 줄씩 손으로 쳐서 거북이를 움직일 수도 있다. 하지만 진짜 로봇이라면 사람이 매번 명령을 칠 수는 없다. 프로그램이 알아서 판단하고 움직여야 한다. 그 “프로그램”이 바로 노드이고, 이번 글에서 처음으로 그걸 직접 코드로 짠다.

목표는 두 가지다.

  1. 발행 노드 — 거북이가 스스로 원을 그리게 하는 노드
  2. 구독 노드 — 거북이의 현재 위치를 읽어서 출력하는 노드

파이썬으로 짜고, 패키지를 만들 것도 없이 파일 하나로 바로 돌린다. 코드 한 줄 한 줄이 무슨 뜻인지 다 풀어줄 테니, 프로그래밍이 익숙하지 않아도 따라올 수 있다.

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() 의 다섯 줄을 일반화하면, 어떤 노드를 짜든 골격이 똑같다.

rclpy.init() — ROS 켜기

노드 만들기 (Node 상속)

발행자 / 구독자 / 타이머 등록

rclpy.spin(node) — 계속 돌기

rclpy.shutdown() — 정리

발행 노드든 구독 노드든, 나중에 더 복잡해지든, 이 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)파이썬은 들여쓰기가 문법이다. 칸 수를 코드와 똑같이

한 줄로 박아둘 것

  1. 노드 = 통로로 메시지를 주고받는 프로그램 한 개. 그걸 파이썬으로 짜는 도구가 rclpy
  2. 노드 뼈대는 늘 5단계init → 노드 만들기 → 발행/구독 등록 → spin → shutdown
  3. 발행 노드 = create_publisher(양식, 통로, 우편함) + 보통 타이머로 주기적으로 publish
  4. 구독 노드 = create_subscription(양식, 통로, 콜백, 우편함) + 메시지가 올 때마다 콜백 실행
  5. spin 은 “사건이 생기면 콜백을 불러주는” 무한 기다림 — 그래서 while 없이 콜백만 짜면 된다
  6. 패키지 없이 python3 파일.py 로 바로 돌아간다 (단, source 된 터미널에서)