ROS2Lab Chapter 04
Ch.04

첫 Python 노드 만들기

rclpy로 Publisher / Subscriber

⏱ 40분 #Python#코딩

🎯 이 챕터의 목표

Ch.03까지는 남이 만든 노드(turtlesim, demo_nodes)를 명령어로 굴려봤어요. 이번엔 진짜 시작 — 내가 직접 Python으로 노드를 코딩해봐요. Hello World 수준 두 개(Publisher/Subscriber)를 만들고, 마지막에 거북이를 자동 운전하는 노드까지.

약어
rclpy = ROS Client Library for Python · Python에서 ROS2 노드를 만들 때 쓰는 공식 라이브러리. C++ 버전은 rclcpp예요.

🏗 ROS2 코드의 3층 구조

내 코드를 ROS2에 올리려면 아래 3단계 구조를 만들어야 해요. 처음엔 좀 번거롭지만, 한 번 만들면 계속 재사용해요.

🌳
Workspace
작업공간. 여러 패키지를 모아두는 폴더 (보통 ~/ros2_ws/).
📦
Package
패키지. 하나의 기능 단위. 예: my_first_pkg.
🟢
Node
노드. 실제 실행되는 Python 스크립트. 패키지 하나에 여러 개 가능.
🏘 부동산 비유:
· Workspace = 단지(아파트 단지 전체)
· Package = 동 (101동, 102동, ...)
· Node = 호수 (101호, 102호, ...)

📂 STEP 1 — 워크스페이스 만들기

홈 디렉토리 아래에 ros2_ws/src 폴더를 만들어요. src(source) 안에 패키지들이 들어가요.

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src

이름은 ros2_ws가 관례지만 다른 이름이어도 OK. src는 반드시 있어야 해요(빌드 도구가 이 이름을 찾아요).

📦 STEP 2 — 패키지 만들기

ros2 pkg create 명령으로 패키지 골격을 만들어요. 이름은 my_first_pkg로.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0 my_first_pkg

옵션 풀이:

  • --build-type ament_python — Python 패키지로 만든다는 뜻. C++이면 ament_cmake.
  • --license Apache-2.0 — 라이센스 표기 (안 적으면 경고 뜸).
  • my_first_pkg — 패키지 이름. 소문자 + 밑줄(snake_case)만 가능. 하이픈 X.
약어
ament = ROS2의 빌드 시스템 이름. ROS1의 catkin을 대체. 어원은 별 의미 없음(개발자 작명).

생성된 폴더 구조

my_first_pkg/
├── package.xml         ← 패키지 메타데이터 (이름, 버전, 의존성)
├── setup.py            ← Python 빌드 설정 (entry_points 여기 박음)
├── setup.cfg           ← (자동 생성, 안 건드림)
├── resource/
│   └── my_first_pkg    ← (빈 파일, 마커용)
├── test/               ← (테스트 코드 자리)
└── my_first_pkg/       ← 🌟 여기에 노드 .py 파일들을 만든다
    └── __init__.py

실제 노드 코드는 안쪽 my_first_pkg/my_first_pkg/ 폴더에 만들어요. 이중 폴더 구조가 좀 헷갈리지만 관례예요.

🟢 STEP 3 — Publisher 노드 작성

"Hello ROS2 N"이라는 메시지를 1초마다 발행하는 노드를 만들어요.

cd ~/ros2_ws/src/my_first_pkg/my_first_pkg
nano chatter_pub.py

아래 코드를 그대로 붙여넣어요.

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class ChatterPub(Node):
    def __init__(self):
        # 노드 이름은 'chatter_pub'
        super().__init__('chatter_pub')

        # /chatter 토픽에 String 메시지 발행, 큐 사이즈 10
        self.pub = self.create_publisher(String, '/chatter', 10)

        # 1초마다 self.tick() 호출
        self.timer = self.create_timer(1.0, self.tick)
        self.count = 0
        self.get_logger().info('chatter_pub 시작!')

    def tick(self):
        msg = String()
        msg.data = f'Hello ROS2 {self.count}'
        self.pub.publish(msg)
        self.get_logger().info(f'발행: "{msg.data}"')
        self.count += 1


def main(args=None):
    rclpy.init(args=args)
    node = ChatterPub()
    try:
        rclpy.spin(node)   # 무한 루프 (이벤트 대기)
    except KeyboardInterrupt:
        pass
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

핵심 부분 짚어보기

  • rclpy.init() — ROS2 시스템 시작 (필수)
  • class ChatterPub(Node)Node 클래스를 상속받음. 이게 ROS2 노드가 되는 핵심.
  • create_publisher(타입, 토픽, 큐사이즈) — 발행자 만들기
  • create_timer(주기초, 콜백) — 일정 주기로 함수 호출
  • rclpy.spin(node) — 노드를 "돌린다". 이벤트(타이머/구독)가 올 때까지 대기.
  • get_logger().info() — ROS2 로그 출력. 그냥 print()보다 이걸 써요.
💡 spin은 "회전한다"가 아니라 "이벤트 루프를 돈다"는 뜻. Node.js의 event loop 같은 거예요.

🟢 STEP 4 — Subscriber 노드 작성

같은 폴더에 듣는 쪽 노드를 만들어요.

nano chatter_sub.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class ChatterSub(Node):
    def __init__(self):
        super().__init__('chatter_sub')

        # /chatter 토픽 구독, 콜백은 self.on_msg
        self.sub = self.create_subscription(
            String, '/chatter', self.on_msg, 10
        )
        self.get_logger().info('chatter_sub 시작! 메시지 기다리는 중...')

    def on_msg(self, msg):
        self.get_logger().info(f'수신: "{msg.data}"')


def main(args=None):
    rclpy.init(args=args)
    node = ChatterSub()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Publisher와 거의 같지만, create_publisher 대신 create_subscription을 쓰고, 콜백 함수 on_msg가 메시지를 받을 때마다 호출돼요.

⚙ STEP 5 — setup.py 수정 (entry_points)

Python 파일을 만들었다고 끝이 아니에요. setup.py"이 파일을 명령어로 만들어달라"고 등록해야 해요.

cd ~/ros2_ws/src/my_first_pkg
nano setup.py

entry_points 부분을 찾아서 다음처럼 수정해요.

entry_points={
    'console_scripts': [
        'chatter_pub = my_first_pkg.chatter_pub:main',
        'chatter_sub = my_first_pkg.chatter_sub:main',
    ],
},

형식: '명령어이름 = 패키지명.파일명:함수명'

  • 왼쪽 chatter_pub = 터미널에서 칠 명령어
  • 오른쪽 my_first_pkg.chatter_pub:main = my_first_pkg/chatter_pub.pymain() 함수
💡 등록 안 하면 ros2 run my_first_pkg chatter_pub 칠 때 "executable not found"가 떠요.

🔨 STEP 6 — 빌드

워크스페이스 루트로 가서 colcon build.

cd ~/ros2_ws
colcon build --packages-select my_first_pkg

--packages-select는 "이 패키지만 빌드" 옵션. 처음 빌드는 좀 오래 걸려요(첫 30초쯤).

Starting >>> my_first_pkg
Finished <<< my_first_pkg [3.2s]

Summary: 1 package finished [3.5s]

이제 빌드 결과물을 환경에 source 해줘요.

source ~/ros2_ws/install/setup.bash
💡 .bashrc에 추가해두면 매번 안 쳐도 돼요:
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc

🚀 STEP 7 — 실행!

터미널 2개를 띄워서 각각:

# 터미널 1
source ~/ros2_ws/install/setup.bash
ros2 run my_first_pkg chatter_pub
[INFO] [chatter_pub]: chatter_pub 시작!
[INFO] [chatter_pub]: 발행: "Hello ROS2 0"
[INFO] [chatter_pub]: 발행: "Hello ROS2 1"
[INFO] [chatter_pub]: 발행: "Hello ROS2 2"
...
# 터미널 2
source ~/ros2_ws/install/setup.bash
ros2 run my_first_pkg chatter_sub
[INFO] [chatter_sub]: chatter_sub 시작! 메시지 기다리는 중...
[INFO] [chatter_sub]: 수신: "Hello ROS2 3"
[INFO] [chatter_sub]: 수신: "Hello ROS2 4"
...

🎉 축하해요! 여러분이 직접 만든 두 Python 노드가 /chatter 토픽을 통해 대화하고 있어요. 다른 터미널에서 ros2 topic echo /chatter 쳐보면 똑같이 보여요.

🐢 보너스 — 거북이 자동 운전 노드

Ch.03에서 키보드로 조종했던 거북이를 이번엔 코드가 자동으로 운전하게 만들어요. turtle_driver.py 파일을 같은 폴더에 만들어요.

nano ~/ros2_ws/src/my_first_pkg/my_first_pkg/turtle_driver.py
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class TurtleDriver(Node):
    def __init__(self):
        super().__init__('turtle_driver')
        self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
        # 10Hz로 명령 발행
        self.timer = self.create_timer(0.1, self.tick)
        self.get_logger().info('거북이 자동 운전 시작! 원을 그립니다...')

    def tick(self):
        msg = Twist()
        msg.linear.x = 2.0   # 앞으로
        msg.angular.z = 1.0  # 좌회전
        self.pub.publish(msg)


def main(args=None):
    rclpy.init(args=args)
    node = TurtleDriver()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

setup.py의 entry_points에 한 줄 추가:

'turtle_driver = my_first_pkg.turtle_driver:main',

다시 빌드.

cd ~/ros2_ws
colcon build --packages-select my_first_pkg
source ~/ros2_ws/install/setup.bash

실행 (터미널 2개):

# 터미널 1 — 거북이 시뮬레이터
ros2 run turtlesim turtlesim_node

# 터미널 2 — 내가 만든 자동 운전 노드
ros2 run my_first_pkg turtle_driver

거북이가 원을 그리며 빙글빙글 돌면 성공. 내가 만든 노드가 표준 /turtle1/cmd_vel 토픽으로 명령을 쏘는 거예요. 이게 진짜 ROS2 프로그래밍의 시작.

💡 msg.linear.x, msg.angular.z 값을 바꿔보세요. 음수로 하면 거꾸로, 0이면 직진/제자리회전.

📜 코딩 패턴 — 외울 것은 6줄

모든 rclpy 노드는 이 뼈대를 따라요. 이거만 외우면 90% 끝.

import rclpy                          # 1. ROS2 라이브러리
from rclpy.node import Node           # 2. Node 클래스

class MyNode(Node):                   # 3. Node 상속
    def __init__(self):
        super().__init__('my_node')   # 4. 노드 이름
        # ↓ 여기에 pub/sub/service/timer 등 세팅

def main():
    rclpy.init()                      # 5. ROS2 시작
    rclpy.spin(MyNode())              # 6. 이벤트 루프
    rclpy.shutdown()
✏️ 퀴즈 1

rclpy는 무엇의 약자인가요?

✏️ 퀴즈 2

rclpy.spin(node)는 무슨 역할인가요?

✏️ 퀴즈 3

Python 노드를 만들고 빌드까지 했는데 ros2 run my_pkg my_node에서 "executable not found"가 떴어요. 가장 먼저 의심해야 할 것은?

✏️ 퀴즈 4

거북이를 코드로 자동 운전하려면 어떤 토픽에 어떤 타입의 메시지를 발행해야 하나요?

🎁 정리

  • ROS2 코드 구조: Workspace → Package → Node
  • 패키지 만들기: ros2 pkg create --build-type ament_python <이름>
  • 모든 rclpy 노드는 Node 클래스를 상속해서 작성
  • setup.pyentry_points에 등록해야 ros2 run으로 실행 가능
  • 빌드: colcon build --packages-select <패키지>
  • 빌드 후 항상 source install/setup.bash
  • rclpy.spin() = 이벤트 루프 시작

여기까지 왔으면 ROS2 입문 산을 절반 넘은 거예요. 다음은 메시지를 직접 정의하는 법이에요. 🛠