ROS 2를 처음 배울 때 많은 분들이 바로 토픽, 서비스, 액션, 노드 코드부터 시작합니다.
그런데 실제로 현장에서 문제가 생기는 지점은 의외로 코드 문법이 아닙니다.
진짜 문제는 보통 이런 곳에서 터집니다.
속도 1.0이 m/s인지 cm/s인지 모른다.
90도를 넣었는데 로봇이 이상하게 돈다.
RViz에서는 위로 가는데 실제 드론은 아래로 간다.
ros2 run을 했는데 실행 파일이 안 보인다.
빌드는 됐는데 새 터미널에서 패키지를 못 찾는다.
TF 에러가 나는데 좌표 문제인지 시간 문제인지 헷갈린다.
ROS 2는 여러 노드, 여러 센서, 여러 패키지, 여러 개발자가 함께 사용하는 구조입니다.
그래서 각자 마음대로 단위, 좌표계, 시간 기준, 파일 구조를 쓰면 시스템이 바로 꼬입니다.
설명할 내용은 아래와 같습니다.
1. ROS 2 표준 단위
2. ROS 2 좌표계
3. Roll, Pitch, Yaw와 Quaternion
4. ROS 2 시간
5. ROS 2 파일 시스템
6. ROS 2 빌드 시스템
7. C++ 패키지 구조
8. Python 패키지 구조
단위 변환 노드 만들기
좌표와 시간 출력 노드 만들기
colcon 빌드와 패키지 실행
12. 자주 나는 에러와 해결법
1. ROS 2에서 단위가 중요한 이유
로봇은 숫자로 움직입니다.
예를 들어 어떤 노드가 다음 값을 보냈다고 합시다.
velocity = 1.0
이 값은 무엇일까요?
1.0 m/s
1.0 cm/s
1.0 km/h
1.0 rad/s
1.0 degree/s
숫자는 같지만 의미는 완전히 다릅니다.
그래서 ROS에서는 특별한 이유가 없다면 SI 단위계를 기준으로 사용합니다. REP 103은 ROS에서 표준 단위 사용을 권장하며, 길이, 질량, 시간, 각도 등 주요 물리량의 기준을 정의합니다.
대표적으로 다음 단위를 사용합니다.
| 거리 | meter, m | 1.2는 1.2m |
| 시간 | second, s | 0.01은 10ms |
| 선속도 | m/s | 0.5는 초당 0.5m |
| 각도 | radian, rad | 3.1416은 약 180도 |
| 각속도 | rad/s | IMU gyro 값 |
| 질량 | kg | 로봇 본체 질량 |
| 힘 | N | 접촉력, 추력 |
| 전압 | V | 배터리 전압 |
| 전류 | A | 모터 전류 |
2. 초보자가 가장 많이 틀리는 단위: degree와 radian
사람은 보통 각도를 도 단위로 생각합니다.
90도
180도
360도
하지만 ROS 2, 수학 라이브러리, TF, Quaternion 계산에서는 대부분 radian을 사용합니다.
90 deg = π / 2 rad ≒ 1.5708
180 deg = π rad ≒ 3.1416
360 deg = 2π rad ≒ 6.2832
Python에서는 다음처럼 변환합니다.
import math
yaw_deg = 90.0
yaw_rad = math.radians(yaw_deg)
print(yaw_rad)
출력:
1.5707963267948966
반대로 radian을 degree로 바꿀 수도 있습니다.
import math
yaw_rad = 1.5708
yaw_deg = math.degrees(yaw_rad)
print(yaw_deg)
사람에게 설명할 때: degree
ROS 메시지와 계산에 넣을 때: radian
드론, 모바일 로봇, 매니퓰레이터에서 이걸 틀리면 실제 동작이 바로 이상해집니다.
3. ROS 2 좌표계 기본: x forward, y left, z up
ROS의 기본 좌표계는 오른손 좌표계를 사용합니다. REP 103은 ROS 좌표계가 오른손 좌표계이며, 기본 축 방향으로 x forward, y left, z up 관례를 제시합니다.

REP 103은 ROS에서 표준 단위 사용을 권장하며, 길이, 질량, 시간, 각도 등 주요 물리량의 기준을 정의합니다.
모바일 로봇 기준으로 보면 다음과 같습니다.

즉 다음처럼 이해하면 됩니다.
x+ : 로봇 전방
y+ : 로봇 왼쪽
z+ : 로봇 위쪽
강의에서는 손으로 직접 오른손 좌표계를 보여주는 것이 가장 좋습니다.
엄지 → x축
검지 → y축
중지 → z축
이 약속은 단순해 보이지만 실무에서는 매우 중요합니다.
예를 들어 로봇에게 다음 명령을 준다고 합시다.
x = 1.0
y = 0.0
z = 0.0
모바일 로봇에서는 보통 “앞으로 1m”라는 의미가 됩니다.
반대로 다음 값은 왼쪽 이동입니다.
x = 0.0
y = 1.0
z = 0.0
4. ROS 2와 PX4 드론 좌표계 차이
드론 개발에서는 좌표계가 더 중요합니다.
ROS 계열에서는 ENU 성향의 좌표계를 많이 사용합니다.
ENU
x : East 또는 forward 문맥
y : North 또는 left 문맥
z : Up
반면 PX4, MAVLink, 항공 분야에서는 NED 좌표계를 많이 사용합니다.
NED
x : North
y : East
z : Down

여기서 가장 위험한 부분은 z축입니다.
ROS 계열: z+ 는 위
PX4/NED: z+ 는 아래
그래서 드론 개발에서 이런 증상이 자주 나옵니다.
고도 명령을 줬는데 반대로 움직인다.
yaw 방향이 이상하다.
RViz에서는 정상인데 실제 기체가 반대로 간다.
Offboard 제어에서 setpoint 방향이 이상하다.
이 문제는 코드 문법 문제가 아니라 좌표계 변환 문제일 가능성이 큽니다.
5. Roll, Pitch, Yaw와 Quaternion
로봇의 회전은 보통 세 가지 축 기준으로 설명합니다.
Roll : x축 기준 회전
Pitch : y축 기준 회전
Yaw : z축 기준 회전

사람이 이해하기에는 Roll, Pitch, Yaw가 쉽습니다.
하지만 ROS 2 메시지에서는 회전을 Quaternion으로 표현하는 경우가 많습니다.
예를 들어 geometry_msgs/msg/Pose는 개념적으로 다음 구조를 가집니다.
position:
x
y
z
orientation:
x
y
z
w
초보자가 많이 헷갈리는 지점은 이것입니다.
position.x : 위치의 x값
orientation.x : Quaternion의 x 성분
둘은 전혀 다른 값입니다.
Yaw 90도를 Quaternion으로 바꾸는 Python 예시는 다음과 같습니다.
import math
from tf_transformations import quaternion_from_euler
roll = 0.0
pitch = 0.0
yaw = math.radians(90.0)
qx, qy, qz, qw = quaternion_from_euler(roll, pitch, yaw)
print(qx, qy, qz, qw)
ROS 2에서 tf_transformations가 없다면 다음처럼 설치할 수 있습니다.
sudo apt update
sudo apt install ros-humble-tf-transformations
6. ROS 2 시간: 실제 시간과 ROS Time을 구분해야 한다
ROS 2에서 시간은 단순히 PC의 현재 시각만 의미하지 않습니다.
로봇 시스템에서는 여러 시간 기준이 존재합니다.
System Time : 운영체제 실제 시간
Steady Time : 단조 증가 시간, 주기 측정에 유리
ROS Time : ROS에서 관리하는 시간
시뮬레이션이나 rosbag 재생에서는 실제 PC 시간이 아니라 /clock 토픽으로 전달되는 시간을 기준으로 동작할 수 있습니다. ROS 2는 실제 시간과 시뮬레이션 시간 모두에서 동작할 수 있도록 시간 추상화를 제공합니다.
Python 노드에서 ROS 시간을 읽는 기본 형태는 다음과 같습니다.
now = self.get_clock().now()
self.get_logger().info(f"ROS time: {now.nanoseconds}")
C++에서는 다음처럼 사용합니다.
rclcpp::Time now = this->get_clock()->now();
RCLCPP_INFO(this->get_logger(), "ROS time: %ld", now.nanoseconds());
time.time()과 node.get_clock().now()는 같은 목적이 아닙니다.
ROS 2 노드에서는 메시지 timestamp, TF, 센서 동기화, rosbag 재생까지 고려해야 합니다.
그래서 ROS 노드 안에서는 가능하면 ROS Clock을 사용하는 습관이 좋습니다.
실습 1. ROS 2 작업 공간 만들기
이제 직접 실습을 해보겠습니다.
기준 환경은 Ubuntu + ROS 2 Humble로 가정합니다. 다른 배포판을 사용한다면 humble 부분을 본인 배포판 이름으로 바꾸면 됩니다.
먼저 ROS 2 환경을 불러옵니다.
source /opt/ros/humble/setup.bash
작업 공간을 만듭니다.
mkdir -p ~/ros2_basic_ws/src
cd ~/ros2_basic_ws
빌드합니다.
colcon build
빌드가 끝나면 다음 폴더들이 생깁니다.
ros2_basic_ws/
├── src/
├── build/
├── install/
└── log/
각 폴더의 의미는 다음과 같습니다.
| src/ | 사용자가 작성하거나 가져온 ROS 2 패키지 소스 |
| build/ | 빌드 중간 산출물 |
| install/ | 빌드 후 실행 가능한 결과물 |
| log/ | 빌드 로그 |
빌드 후에는 반드시 환경을 적용합니다.
source install/setup.bash
아래의 내용을 매번 새 터미널에서 자동 적용할 수 있도록 이미 .bashrc에 추가했습니다.
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
echo "source ~/ros2_basic_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc
단, 여러 워크스페이스를 쓸 때는 순서가 중요합니다.
source /opt/ros/humble/setup.bash
source ~/ros2_basic_ws/install/setup.bash
기본 ROS 2 환경을 먼저 불러오고, 그 위에 사용자 워크스페이스를 overlay하는 구조입니다.
실습 2. Python 패키지 만들기
이번에는 Python 패키지를 만들어보겠습니다.
cd ~/ros2_basic_ws/src
ros2 pkg create ros2_basic_practice \
--build-type ament_python \
--dependencies rclpy std_msgs geometry_msgs
생성된 구조는 대략 다음과 같습니다.
ros2_basic_practice/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│ └── ros2_basic_practice
├── ros2_basic_practice/
│ └── __init__.py
└── test/
ROS 2 패키지는 CMake 또는 Python 방식으로 만들 수 있고, 공식적으로 ament를 빌드 시스템으로, colcon을 빌드 도구로 사용합니다.
실습 3. degree를 radian으로 변환하는 노드 만들기
파일을 하나 만듭니다.
cd ~/ros2_basic_ws/src/ros2_basic_practice/ros2_basic_practice
touch angle_converter_node.py
code ~/ros2_basic_ws/src
code .
다음 코드를 입력합니다.
import math
import rclpy
from rclpy.node import Node
class AngleConverterNode(Node):
def __init__(self):
super().__init__('angle_converter_node')
self.declare_parameter('angle_deg', 90.0)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
angle_deg = self.get_parameter('angle_deg').value
angle_rad = math.radians(angle_deg)
self.get_logger().info(
f'angle_deg={angle_deg:.2f}, angle_rad={angle_rad:.4f}'
)
def main(args=None):
rclpy.init(args=args)
node = AngleConverterNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
이 노드는 angle_deg 파라미터를 받아서 radian으로 변환합니다.
이제 setup.py를 수정합니다.
entry_points={
'console_scripts': [
'angle_converter = ros2_basic_practice.angle_converter_node:main',
],
},
전체 setup.py 예시는 다음과 비슷합니다.
from setuptools import setup
package_name = 'ros2_basic_practice'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='robot',
maintainer_email='robot@example.com',
description='ROS 2 basic practice package',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'angle_converter = ros2_basic_practice.angle_converter_node:main',
],
},
)
빌드합니다.
cd ~/ros2_basic_ws
colcon build --packages-select ros2_basic_practice
source install/setup.bash
실행합니다.
ros2 run ros2_basic_practice angle_converter
출력 예시:
[INFO] [angle_converter_node]: angle_deg=90.00, angle_rad=1.5708
파라미터를 바꿔서 실행할 수도 있습니다.
ros2 run ros2_basic_practice angle_converter --ros-args -p angle_deg:=180.0
출력 예시:
angle_deg=180.00, angle_rad=3.1416
이 실습의 핵심은 다음입니다.
ROS 2에서 각도 계산은 degree가 아니라 radian 기준으로 처리하는 습관을 들인다.
실습 4. 좌표와 ROS 시간을 출력하는 노드 만들기
이번에는 좌표계와 ROS 시간을 같이 확인하는 노드를 만들겠습니다.
cd ~/ros2_basic_ws/src/ros2_basic_practice/ros2_basic_practice
touch coordinate_time_node.py
다음 코드를 입력합니다.
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PointStamped
class CoordinateTimeNode(Node):
def __init__(self):
super().__init__('coordinate_time_node')
self.publisher = self.create_publisher(
PointStamped,
'sample_point',
10
)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
msg = PointStamped()
msg.header.stamp = self.get_clock().now().to_msg()
msg.header.frame_id = 'base_link'
msg.point.x = 1.0
msg.point.y = 0.5
msg.point.z = 0.2
self.publisher.publish(msg)
self.get_logger().info(
f'frame={msg.header.frame_id}, '
f'x={msg.point.x}, y={msg.point.y}, z={msg.point.z}'
)
def main(args=None):
rclpy.init(args=args)
node = CoordinateTimeNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
setup.py의 entry_points에 실행 파일을 추가합니다.
entry_points={
'console_scripts': [
'angle_converter = ros2_basic_practice.angle_converter_node:main',
'coordinate_time = ros2_basic_practice.coordinate_time_node:main',
],
},
빌드합니다.
cd ~/ros2_basic_ws
colcon build --packages-select ros2_basic_practice
source install/setup.bash
실행합니다.
ros2 run ros2_basic_practice coordinate_time
다른 터미널에서 토픽을 확인합니다.
source ~/ros2_basic_ws/install/setup.bash
ros2 topic list
출력 예시:
/sample_point
토픽 내용을 확인합니다.
ros2 topic echo /sample_point
출력 예시:
header:
stamp:
sec: 12345
nanosec: 678900000
frame_id: base_link
point:
x: 1.0
y: 0.5
z: 0.2
여기서 핵심은 세 가지입니다.
header.stamp : 이 데이터가 생성된 ROS 시간
header.frame_id : 이 좌표가 어느 좌표계 기준인지
point.x/y/z : 해당 좌표계에서의 위치값
ROS 2 메시지에서 frame_id와 stamp는 대충 넣는 값이 아닙니다.
TF, 센서 동기화, RViz 시각화, rosbag 재생에서 매우 중요합니다.
'강좌 > ROS2' 카테고리의 다른 글
| ROS 2 디버깅과 관찰 도구: 로그, rqt_console, rqt_graph, rqt_plot 실습 정리 #2 (0) | 2026.05.24 |
|---|---|
| ROS2 Action Server에 Cancel 기능 추가하기 (0) | 2026.05.23 |
| 3일차 강의 (0) | 2026.05.23 |
| 2일차 강의 (0) | 2026.05.23 |
| 1일차 강의 (0) | 2026.05.22 |