ROS 2 — 거북이로 직접 해보는 토픽·서비스·액션 (turtlesim)
화면 속 거북이 한 마리로 ROS의 노드·토픽·메시지·서비스·액션을 처음부터 끝까지 직접 따라 해본다. 용어를 하나하나 풀어주고, 명령마다 무엇을 하고 결과가 무슨 뜻인지까지 설명한다.
ROS를 글로만 읽으면 노드가 어떻고 토픽이 어떻고 하는 말이 좀처럼 손에 안 잡힌다. 그래서 ROS에는 turtlesim(터틀심) 이라는 연습용 장난감이 딸려 온다. 화면에 거북이 한 마리가 떠 있고, 그 거북이를 움직이거나 말 거는 방식이 진짜 로봇을 다루는 방식과 똑같다.
이 글은 그 거북이로 ROS의 통신 수단을 전부 한 번씩 직접 써보는 실습이다. 끝까지 따라 하면 이런 걸 하게 된다.
- 거북이를 화면에 띄운다
- 거북이를 움직인다 (앞으로 가고 빙글빙글 돌게)
- 거북이의 현재 위치를 읽어온다
- 거북이를 한 마리 더 만든다
- 거북이를 정해진 방향으로 정확히 돌린다
이 다섯 가지를 하면서 자연스럽게 노드 · 토픽 · 메시지 · 서비스 · 액션 이라는 ROS의 핵심 다섯 단어를 다 만난다. 처음 보는 단어가 나와도 그때그때 풀어서 설명하니 걱정하지 않아도 된다.
0. 시작 전에 — 딱 네 가지만 약속하고 가자
본격적으로 들어가기 전에, 처음이면 헷갈리는 네 가지를 미리 짚는다. 이것만 알면 막혀도 당황하지 않는다.
① 명령은 “터미널”에 친다. 터미널은 글자로 컴퓨터에게 명령하는 검은 창이다. 마우스로 누르는 대신 명령어를 한 줄 적고 엔터를 친다. 아래 코드 칸에서 $ 뒤에 있는 게 내가 치는 명령이고, 그 아래 줄들이 컴퓨터가 보여주는 결과다. ($ 기호 자체는 치지 않는다.)
② 새 터미널을 열 때마다 ROS를 “소개”해줘야 한다. 터미널은 처음 열리면 ROS가 뭔지 모른다. 그래서 매번 이 한 줄을 먼저 친다.
$ source /opt/ros/jazzy/setup.bash
source(소스)는 “여기 ROS 있으니까 알아둬” 라고 터미널에게 알려주는 절차다. 이걸 빼먹으면 나중에 ros2: command not found(ros2 라는 명령을 못 찾겠다) 라는 에러가 난다. 그러면 십중팔구 이 줄을 안 친 것이다.
③ 터미널을 여러 개 열게 된다. 프로그램 하나를 켜면 그 터미널은 그 프로그램에 붙잡혀 계속 돌아간다. 그래서 거북이를 띄우는 터미널 하나, 명령을 내리는 터미널 하나… 이렇게 여러 개를 동시에 열어 둔다. 새 터미널을 열면 ②번부터 다시 한다.
④ 멈출 때는 Ctrl + C. 계속 돌아가는 프로그램을 멈추고 싶으면 그 터미널에서 키보드의 Ctrl 과 C 를 같이 누른다.
이제 시작하자.
1. 거북이를 화면에 띄우기
첫 번째 터미널을 열고, ROS를 소개해준 다음(약속 ②), 거북이를 띄운다.
$ source /opt/ros/jazzy/setup.bash
$ ros2 run turtlesim turtlesim_node
[INFO] [turtlesim]: Starting turtlesim with node name /turtlesim
[INFO] [turtlesim]: Spawning turtle [turtle1] at x=[5.5] y=[5.5] theta=[0.0]
→ 명령을 친 순간 파란 창이 하나 뜨고, 한가운데에 거북이 한 마리가 나타난다. 터미널에는 위처럼 “거북이를 (5.5, 5.5) 위치에 만들었다” 는 안내가 찍힌다.
✋ 잠깐 — 방금 친 명령 뜯어보기
ros2 run turtlesim turtlesim_node는 “turtlesim이라는 꾸러미 안의turtlesim_node라는 프로그램을 실행해줘” 라는 뜻이다.ros2 run [꾸러미 이름] [프로그램 이름]— ROS에서 프로그램 하나를 켜는 가장 기본 명령이다.
이 터미널은 닫지 말고 그대로 둔다. 여기가 거북이를 살아 있게 해주는 터미널이다. (Ctrl+C 를 누르면 거북이가 사라진다.)
2. 이 거북이의 정체 — “노드” 들여다보기
거북이가 떠 있는 동안, 새 터미널을 연다(약속 ②·③를 잊지 말고). 그리고 지금 컴퓨터에 어떤 ROS 프로그램이 돌고 있는지 물어본다.
$ source /opt/ros/jazzy/setup.bash
$ ros2 node list
/turtlesim
→ /turtlesim 하나가 보인다. 이렇게 돌고 있는 ROS 프로그램 하나를 ROS에서는 노드(node) 라고 부른다. 지금은 거북이 노드 하나만 켜져 있는 것이다.
✋ 잠깐 — 노드(node)란? 노드는 한 가지 일을 맡은 프로그램 한 개다. 진짜 로봇이라면 “카메라를 담당하는 노드”, “바퀴를 굴리는 노드”처럼 일감별로 노드를 나눈다. 지금 우리의 거북이 노드는 거북이를 그리고 움직이는 일을 맡고 있다.
이제 이 거북이 노드가 무엇을 할 수 있는지 통째로 들여다보자. 이게 오늘 실습의 지도가 된다.
$ ros2 node info /turtlesim
/turtlesim
Subscribers:
/turtle1/cmd_vel: geometry_msgs/msg/Twist
Publishers:
/turtle1/pose: turtlesim/msg/Pose
Service Servers:
/clear: std_srvs/srv/Empty
/spawn: turtlesim/srv/Spawn
Action Servers:
/turtle1/rotate_absolute: turtlesim/action/RotateAbsolute
(실제 출력은 더 길지만, 오늘 쓸 것만 추렸다.) 어려운 영어 단어가 보여도 괜찮다. 칸이 네 개로 나뉘어 있다는 것만 보자.
- Subscribers (구독) — 거북이가 받아서 듣는 것.
/turtle1/cmd_vel= “이 속도로 움직여” 라는 명령을 받는다 - Publishers (발행) — 거북이가 스스로 내보내는 것.
/turtle1/pose= “나 지금 여기 있어” 라는 위치 정보를 계속 알린다 - Service Servers (서비스) — 한 번 부탁하면 한 번 해주는 것.
/spawn= “거북이 한 마리 더 만들어줘” - Action Servers (액션) — 시간이 좀 걸리는 일을 시키는 것.
/turtle1/rotate_absolute= “이 방향 보도록 돌아”
오늘은 이 네 칸을 위에서부터 하나씩 직접 써본다. 거북이 노드 하나가 이 모든 통신 수단을 다 갖고 있다는 게 핵심이다.
3. 토픽으로 거북이 움직이기 (보내기)
가장 먼저 토픽부터. 토픽은 계속 흘러가는 데이터가 지나는 통로다. 비유하면 라디오 방송과 같다.
✋ 잠깐 — 토픽 / 발행 / 구독
- 토픽(topic) = 데이터가 흐르는 통로의 이름. 라디오의 주파수(예: 89.1 MHz)와 같다
- 발행(publish) = 그 통로에 데이터를 내보내는 것. 라디오 방송국이 하는 일
- 구독(subscribe) = 그 통로에서 데이터를 받는 것. 라디오 수신기가 하는 일
방송국과 라디오는 서로를 몰라도, 같은 주파수만 맞추면 소리가 전달된다. ROS도 똑같이, 같은 토픽 이름만 쓰면 데이터가 전달된다.
거북이는 /turtle1/cmd_vel 이라는 토픽을 구독하고 있었다(§2에서 봤다). cmd_vel 은 command velocity, 즉 “움직임 속도 명령” 이라는 뜻이다. 그러니 우리가 이 토픽에 속도를 발행하면 거북이가 움직인다.
다음 명령을 친다. (조금 길지만 그대로 따라 친다.)
$ ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.0}}"
publisher: beginning loop
publishing #1: geometry_msgs.msg.Twist(linear=...(x=2.0, y=0.0, z=0.0), angular=...(x=0.0, y=0.0, z=1.0))
publishing #2: ...
publishing #3: ...
→ 거북이가 빙글빙글 원을 그리며 움직이기 시작한다! 터미널에는 “내가 이 메시지를 보내고 있어” 하는 줄이 1초에 한 번씩 계속 찍힌다. 다 봤으면 Ctrl+C 로 멈춘다.
명령이 길어 보이지만, 사실 세 토막이다.
ros2 topic pub— “토픽에 발행(pub = publish)할게”/turtle1/cmd_vel— 어느 토픽에 보낼지 (거북이의 속도 명령 통로)geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.0}}"— 무슨 데이터를 보낼지
✋ 잠깐 — 메시지(message)와 Twist 통로(토픽)에 흘려보내는 데이터 한 덩어리를 메시지(message) 라고 한다. 그런데 아무 모양이나 보내면 거북이가 못 알아듣는다. 그래서 데이터의 정해진 양식이 있는데, 그게 메시지 타입이다. 여기서 쓴 타입은
Twist(트위스트) — 움직임을 표현하는 표준 양식이다. 두 칸으로 되어 있다.
linear(직선) →x= 앞뒤로 가는 속도angular(회전) →z= 제자리에서 도는 속도그래서
linear.x = 2.0(앞으로 가라) +angular.z = 1.0(동시에 돌아라) = 앞으로 가면서 돈다 = 원이 된다.
✏️ 직접 바꿔보기 위 명령에서 숫자를 바꿔 다시 쳐보자. 거북이가 어떻게 달라질까?
angular: {z: 0.0}으로 하면? → 안 돌고 똑바로 앞으로만 간다linear: {x: 1.0}, angular: {z: 2.0}으로 하면? → 더 작은 원을 그린다 (도는 게 빨라서)
4. 토픽으로 거북이 위치 읽기 (받기)
방금은 거북이에게 보냈다. 이번엔 거북이가 내보내는 걸 받아 본다. 거북이는 자기 위치를 /turtle1/pose 라는 토픽으로 끊임없이 발행하고 있다. (pose(포즈) = 위치와 방향.)
또 새 터미널을 열고(약속 ②), 그 통로를 엿듣는다.
$ source /opt/ros/jazzy/setup.bash
$ ros2 topic echo /turtle1/pose
x: 5.544444
y: 5.544444
theta: 0.0
linear_velocity: 0.0
angular_velocity: 0.0
---
x: 5.544444
y: 5.544444
theta: 0.0
...
→ 거북이의 현재 상태가 계속 찍힌다. 각 줄의 뜻은 이렇다.
x,y— 거북이의 현재 좌표 (창의 왼쪽 아래가 0,0 / 한가운데가 약 5.5, 5.5)theta(세타) — 거북이가 바라보는 방향. 단위는 라디안이다 (아래 설명)linear_velocity,angular_velocity— 지금 얼마나 빠르게 직진/회전 중인지
✋ 잠깐 — 라디안(radian)? 우리는 보통 각도를 도(°) 로 잰다 (직각 = 90°). ROS는 대신 라디안이라는 단위를 쓴다. 둘은 그냥 같은 걸 다르게 재는 것뿐이다.
- 한 바퀴(360°) ≈ 6.28 라디안
- 반 바퀴(180°) ≈ 3.14 라디안 (원주율 π!)
- 직각(90°) ≈ 1.57 라디안
echo(에코) 는 메아리처럼 흐르는 내용을 그대로 보여줘 라는 뜻이다. 여기서 멋진 걸 해볼 수 있다 — §3의 움직이는 명령과 이 echo 를 동시에 켜두면, 거북이가 도는 동안 x, y, theta 숫자가 실시간으로 바뀌는 게 보인다. 보내는 쪽과 받는 쪽이 동시에 도는 것이다.
이 위치 메시지의 양식은 turtlesim/msg/Pose 다. 아까 Twist 는 ROS 표준 양식이었는데, 이 Pose 는 turtlesim이 자기만 쓰려고 직접 만든 양식이다. 표준이든 직접 만든 것이든, 토픽 위를 흐른다는 점은 똑같다.
5. 서비스로 거북이 한 마리 더 만들기
토픽은 계속 흐르는 데이터에 어울린다. 그런데 “거북이 한 마리 더 만들어줘” 같은 일은 한 번만 하면 되는, 딱 떨어지는 부탁이다. 이런 건 서비스(service) 로 한다.
✋ 잠깐 — 서비스(service)? 서비스는 자판기와 같다. 버튼을 누르면(→ 요청, Request) 음료가 나온다(→ 응답, Response). 한 번 주고받으면 끝. 토픽처럼 계속 흐르지 않는다. “지금 상태 알려줘”, “설정 바꿔줘”, “하나 만들어줘” 같은 한 번짜리 부탁에 쓴다.
거북이 노드는 /spawn(스폰 = 생성) 이라는 서비스를 갖고 있었다. 새 터미널에서 이걸 호출한다.
$ source /opt/ros/jazzy/setup.bash
$ ros2 service call /spawn turtlesim/srv/Spawn "{x: 2.0, y: 2.0, theta: 0.0, name: 'turtle2'}"
requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.0, name='turtle2')
response:
turtlesim.srv.Spawn_Response(name='turtle2')
→ 파란 창의 (2, 2) 위치에 거북이가 한 마리 더 생긴다! 그리고 터미널을 보면 오간 대화가 그대로 찍혀 있다.
making request: ...(x=2.0, y=2.0, ... name='turtle2')— 내가 보낸 요청: “(2,2)에 ‘turtle2’라는 거북이 만들어줘”response: ...(name='turtle2')— 거북이 노드의 응답: “만들었어, 이름은 turtle2야”
ros2 service call [서비스 이름] [양식] "{내용}" — 한 번 부르고, 답을 받고, 끝난다. 토픽처럼 계속 흐르지 않는 게 핵심이다.
이번 양식 turtlesim/srv/Spawn 은 요청 양식과 응답 양식을 둘 다 담고 있다 (.srv 파일). 그래서 토픽 메시지(.msg)와 구분해서 srv 라고 적는다.
✏️ 직접 해보기 — 흔적 지우기 거북이들이 지나간 선이 지저분해졌으면, 가장 단순한 서비스로 싹 지울 수 있다.
$ ros2 service call /clear std_srvs/srv/Empty
Empty(엠프티 = 비어 있음) 는 요청에도 응답에도 내용이 없는 가장 단순한 양식이다. 그냥 “지워줘” 만 부탁하고 끝.
6. 액션으로 거북이를 정확한 방향으로 돌리기
마지막은 액션(action) 이다. “정확히 90도 방향을 보도록 천천히 돌아라” 같은 일은 시간이 좀 걸린다. 도는 동안 얼마나 남았는지 알고 싶고, 마음 바뀌면 취소도 하고 싶다. 이런 일에 액션을 쓴다.
✋ 잠깐 — 액션(action)? 액션은 택배 주문과 같다.
- 목표(Goal) — “이 주소로 보내줘” (주문)
- 진행(Feedback) — “지금 배송 중, 절반 왔어요” (중간 알림이 여러 번 온다)
- 결과(Result) — “도착했습니다” (마지막에 한 번)
- 게다가 도중에 취소도 할 수 있다.
서비스(자판기)는 즉시 끝나지만, 액션은 오래 걸리는 일을 맡기고 진행 상황을 받아보는 것이다.
거북이 노드의 /turtle1/rotate_absolute(회전 — 절대 방향) 액션을 불러서, 90도(= 1.57 라디안) 방향을 보게 한다. 새 터미널에서:
$ source /opt/ros/jazzy/setup.bash
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}" --feedback
Sending goal:
theta: 1.57
Goal accepted with ID: f8d8c2...
Feedback:
remaining: 1.21
Feedback:
remaining: 0.63
Feedback:
remaining: 0.05
Result:
delta: -1.57
Goal finished with status: SUCCEEDED
→ 거북이가 천천히 돌아서 위쪽(90도)을 바라보고 멈춘다. 터미널 출력에 택배 주문의 세 박자가 그대로 보인다.
Sending goal: theta: 1.57— 목표를 보냈다: “1.57 라디안 방향을 봐”Goal accepted— 거북이가 “접수했어요”Feedback: remaining: 1.21 → 0.63 → 0.05— 진행 알림. 남은 회전량이 점점 0에 가까워진다 (배송 추적!)Result: delta: -1.57— 결과: 실제로 돈 각도Goal finished with status: SUCCEEDED— 성공적으로 끝났다
맨 끝의 --feedback 은 “진행 상황도 보여줘” 라는 표시다. 이게 없으면 중간 Feedback 줄들이 안 나오고 결과만 나온다.
이 양식 turtlesim/action/RotateAbsolute 는 목표·진행·결과 세 가지를 담고 있다 (.action 파일). 그래서 msg(1개) · srv(요청+응답 2개) 와 구분해 action 이라고 적는다.
✏️ 직접 바꿔보기
theta숫자를 바꿔보자.3.14(반 바퀴 = 왼쪽),0.0(오른쪽, 처음 방향). 거북이가 매번 그 방향으로 정확히 돌아서 멈추는 걸 확인할 수 있다.
7. 방금 한 걸 한 장으로 정리
| 한 일 | 친 명령 | 쓴 수단 | 데이터 양식 |
|---|---|---|---|
| 거북이가 가진 것 보기 | ros2 node info /turtlesim | 노드 | — |
| 거북이 움직이기 | ros2 topic pub .../cmd_vel | 토픽(보내기) | geometry_msgs/msg/Twist |
| 위치 읽기 | ros2 topic echo .../pose | 토픽(받기) | turtlesim/msg/Pose |
| 거북이 추가 | ros2 service call /spawn | 서비스 | turtlesim/srv/Spawn |
| 정확히 회전 | ros2 action send_goal .../rotate_absolute | 액션 | turtlesim/action/RotateAbsolute |
다섯 줄 모두 거북이 노드 하나를 상대로 한 것이다. 하나의 노드가 토픽·서비스·액션을 동시에 가질 수 있다. 그럼 셋 중 뭘 쓸지는 어떻게 정할까? 일의 성격으로 고른다.
- 데이터가 계속 흐르면 → 토픽 (속도 명령, 위치, 카메라 영상…)
- 한 번 묻고 한 번 답이면 → 서비스 (상태 물어보기, 설정 바꾸기, 하나 만들기…)
- 시간이 걸리고 진행 상황·취소가 필요하면 → 액션 (정해진 곳까지 이동, 회전, 물건 집기…)
8. 막히면 — 이 증상이면 이 원인
| 이런 증상이면 | 십중팔구 원인 |
|---|---|
ros2: command not found | 그 터미널에서 source 를 안 함 → source /opt/ros/jazzy/setup.bash 먼저 (약속 ②) |
| 파란 창이 안 뜸 | 화면(디스플레이)이 없는 환경. turtlesim은 GUI가 필요하다 |
topic pub 했는데 거북이가 가만히 | 토픽 이름을 잘못 침 → ros2 topic list 로 정확한 이름 확인 |
service call 이 한참 멈춰 있음 | 서비스 이름이 틀림 → ros2 service list 로 확인 |
action send_goal 이 “server not available” | §1의 거북이 터미널이 꺼졌다 → 거북이를 다시 띄우기 |
| 명령이 너무 길어 오타가 남 | 천천히, 따옴표(")와 중괄호({ }) 짝이 맞는지 확인 |
가장 흔한 두 가지는 source 를 빼먹은 것과 거북이를 안 띄운 채 명령만 친 것이다. 막히면 이 둘부터 의심하자.
한 줄로 박아둘 것
- 노드 = 한 가지 일을 맡은 프로그램 한 개. 거북이도 노드 하나다 —
ros2 node info로 그 노드가 가진 통신 수단을 통째로 본다 - 토픽 = 계속 흐르는 데이터 통로.
topic pub(보내기)으로 거북이를 움직이고,topic echo(받기)로 위치를 읽는다 — 라디오 방송 모형 - 서비스 = 한 번 부탁 → 한 번 응답.
service call /spawn으로 거북이를 더 만든다 — 자판기 모형 - 액션 = 시간 걸리는 일 + 진행 알림 + 취소.
action send_goal으로 정확히 회전시킨다 — 택배 주문 모형 - 메시지 = 그 통로로 흐르는 데이터의 양식. 토픽은
.msg, 서비스는.srv, 액션은.action으로 그 양식을 정한다 - 셋 중 뭘 쓸지는 일의 성격으로 고른다 — 계속 흐르면 토픽, 묻고 답이면 서비스, 오래 걸리면 액션