🎯 이 챕터의 목표
Ch.03까지는 남이 만든 노드(turtlesim, demo_nodes)를 명령어로 굴려봤어요.
이번엔 진짜 시작 — 내가 직접 Python으로 노드를 코딩해봐요. Hello World 수준 두 개(Publisher/Subscriber)를 만들고, 마지막에 거북이를 자동 운전하는 노드까지.
rclcpp예요.
🏗 ROS2 코드의 3층 구조
내 코드를 ROS2에 올리려면 아래 3단계 구조를 만들어야 해요. 처음엔 좀 번거롭지만, 한 번 만들면 계속 재사용해요.
~/ros2_ws/).my_first_pkg.· 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.
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.py의main()함수
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()
rclpy는 무엇의 약자인가요?
rclpy.spin(node)는 무슨 역할인가요?
Python 노드를 만들고 빌드까지 했는데 ros2 run my_pkg my_node에서 "executable not found"가 떴어요. 가장 먼저 의심해야 할 것은?
거북이를 코드로 자동 운전하려면 어떤 토픽에 어떤 타입의 메시지를 발행해야 하나요?
🎁 정리
- ROS2 코드 구조: Workspace → Package → Node
- 패키지 만들기:
ros2 pkg create --build-type ament_python <이름> - 모든 rclpy 노드는
Node클래스를 상속해서 작성 setup.py의entry_points에 등록해야ros2 run으로 실행 가능- 빌드:
colcon build --packages-select <패키지> - 빌드 후 항상
source install/setup.bash rclpy.spin()= 이벤트 루프 시작
여기까지 왔으면 ROS2 입문 산을 절반 넘은 거예요. 다음은 메시지를 직접 정의하는 법이에요. 🛠