1. URDF 소개
1) URDF란?
URDF는 Unified Robot Description Format의 약자입니다.
ROS에서 로봇의 구조를 표현하기 위한 XML 기반 로봇 모델 설명 파일입니다.
ROS 2 공식 문서에서도 URDF를 로봇의 geometry와 organization, 즉 로봇의 형상과 구성 관계를 정의하는 파일 형식으로 설명합니다.
쉽게 말하면 URDF는 로봇에게 붙이는 설계도입니다.
예를 들어 로봇에 다음과 같은 부품이 있다고 가정합니다.
몸체
왼쪽 바퀴
오른쪽 바퀴
캐스터 휠
라이다 센서
카메라
로봇팔
그리퍼
URDF는 이 부품들을 각각 link로 정의하고, 부품 사이의 연결 관계를 joint로 정의합니다.
2) URDF의 사용 목적
URDF의 핵심 목적은 다음과 같습니다.
로봇의 물리적 구조를 ROS 시스템 안에서 공통된 방식으로 표현하는 것
즉, 사람이 눈으로 보는 로봇을 ROS가 이해할 수 있는 데이터 구조로 바꾸는 역할을 합니다.
URDF를 사용하면 다음 정보를 표현할 수 있습니다.
| 로봇 이름 | 로봇 모델의 이름 |
| link | 로봇을 구성하는 각 부품 |
| joint | link와 link 사이의 연결 관계 |
| visual | RViz에서 보이는 외형 |
| collision | 충돌 계산에 사용하는 형상 |
| inertial | 질량, 관성 등 물리 정보 |
| sensor 위치 | 카메라, 라이다, IMU 등의 장착 위치 |
| wheel 위치 | 바퀴 위치와 회전축 |
| TF 구조 | 로봇 좌표계 트리 구조 |
3) URDF가 필요한 이유
ROS 2에서 로봇을 제대로 움직이고 시각화하려면 각 부품의 위치 관계를 알아야 합니다.
예를 들어 자율주행 로봇에서 라이다가 로봇 중심보다 앞쪽에 달려 있다면, ROS는 다음 관계를 알아야 합니다.
base_link 기준으로
laser_link는 x 방향으로 0.15 m 앞에 있다.
카메라가 위쪽에 달려 있다면 다음 정보도 필요합니다.
base_link 기준으로
camera_link는 z 방향으로 0.25 m 위에 있다.
이런 정보를 모르면 ROS는 센서 데이터를 해석할 수 없습니다.
라이다 데이터가 들어와도 이 라이다가 로봇의 어디에 달려 있는지 모릅니다.
카메라 이미지가 들어와도 카메라 좌표계가 로봇 기준으로 어디에 있는지 모릅니다.
바퀴가 회전해도 바퀴가 로봇의 왼쪽에 있는지 오른쪽에 있는지 모릅니다.
그래서 URDF가 필요합니다.
4) URDF는 어디에 사용되는가?
URDF는 ROS 2에서 여러 핵심 기능과 연결됩니다.
a. RViz2 시각화
URDF를 이용하면 RViz2에서 로봇 모델을 볼 수 있습니다.
URDF 파일
↓
robot_state_publisher
↓
/robot_description
/tf
/tf_static
↓
RViz2 RobotModel 표시
robot_state_publisher는 URDF에 정의된 link와 joint 정보를 사용해 로봇의 TF 트리를 발행합니다.
ROS 2 공식 튜토리얼에서도 URDF와 robot_state_publisher를 함께 사용하는 방식을 소개하고 있습니다.
b. TF 좌표계 구성
ROS에서 로봇은 여러 좌표계를 가집니다.
예를 들면 다음과 같습니다.
map
odom
base_link
base_footprint
laser_link
camera_link
imu_link
left_wheel_link
right_wheel_link
URDF는 이 좌표계들의 부모-자식 관계를 정의합니다.
예:
base_link
├── left_wheel_link
├── right_wheel_link
├── caster_link
├── laser_link
└── camera_link
이 관계는 /tf 또는 /tf_static으로 발행됩니다.
3) Gazebo / Ignition 시뮬레이션
시뮬레이터에서 로봇을 사용하려면 외형뿐 아니라 물리 정보도 필요합니다.
예를 들어 다음 정보가 필요합니다.
질량
관성
충돌 형상
바퀴 회전축
마찰 계수
센서 위치
플러그인 설정
URDF 또는 Xacro 기반 모델은 Gazebo 시뮬레이션에서 로봇 모델을 생성하는 데 사용됩니다.
d. Navigation2
Navigation2를 사용할 때도 URDF가 중요합니다.
로봇이 자기 위치를 알고 이동하려면 다음 좌표계들이 필요합니다.
map → odom → base_link → laser_link
특히 라이다 기반 SLAM이나 Navigation에서는 laser_link가 base_link 기준으로 정확히 어디에 있는지가 중요합니다.
e. MoveIt2
로봇팔을 제어할 때는 URDF가 거의 필수입니다.
MoveIt2는 URDF를 통해 다음 정보를 파악합니다.
각 링크의 길이
각 조인트의 회전축
각 조인트의 제한 각도
충돌 모델
엔드이펙터 위치
즉, URDF는 로봇팔의 운동학 계산에도 사용됩니다.
2. URDF 기본 문법
URDF는 XML 구조입니다.
가장 기본 형태는 다음과 같습니다.
<?xml version="1.0"?>
<robot name="simple_robot">
</robot>
모든 로봇 설명은 <robot> 태그 안에 들어갑니다.
1) link
link는 로봇의 부품 하나를 의미합니다.
예를 들면 다음과 같습니다.
차체
바퀴
센서
카메라
라이다
로봇팔 링크
URDF에서는 다음처럼 작성합니다.
<link name="base_link">
</link>
base_link는 로봇의 중심 몸체를 의미하는 경우가 많습니다.
2) visual
visual은 RViz나 Gazebo에서 보이는 모양입니다.
<link name="base_link">
<visual>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
<material name="blue">
<color rgba="0.0 0.0 1.0 1.0"/>
</material>
</visual>
</link>
여기서 중요한 부분은 다음입니다.
<box size="0.3 0.2 0.1"/>
의미:
x 방향 길이: 0.3 m
y 방향 길이: 0.2 m
z 방향 길이: 0.1 m
URDF의 기본 단위는 보통 다음과 같이 사용합니다.
길이: meter
질량: kg
각도: radian
3) geometry
geometry는 물체의 모양을 정의합니다.
대표적으로 다음 세 가지가 자주 사용됩니다.
box
<box size="0.3 0.2 0.1"/>
직육면체입니다.
ylinder
<cylinder radius="0.05" length="0.02"/>
원통입니다. 바퀴 표현에 많이 사용합니다.
sphere
<sphere radius="0.05"/>
구 형태입니다. 캐스터 휠이나 간단한 센서 표현에 사용할 수 있습니다.
mesh
<mesh filename="package://my_robot_description/meshes/base.stl"/>
복잡한 3D 모델을 사용할 때 사용합니다.
보통 확장자는 다음을 많이 사용합니다.
.stl
.dae
.obj
4) origin
origin은 위치와 방향을 설정합니다.
<origin xyz="0.1 0.0 0.05" rpy="0.0 0.0 0.0"/>
의미:
xyz = x, y, z 위치
rpy = roll, pitch, yaw 회전
예를 들어 다음은 base_link 기준으로 x 방향 10cm, z 방향 5cm 위치에 배치한다는 뜻입니다.
<origin xyz="0.1 0.0 0.05" rpy="0.0 0.0 0.0"/>
5) material
material은 색상입니다.
<material name="red">
<color rgba="1.0 0.0 0.0 1.0"/>
</material>
rgba 의미:
r = red
g = green
b = blue
a = alpha, 투명도
값의 범위는 0.0 ~ 1.0입니다.
6) joint
joint는 두 개의 link를 연결합니다.
예를 들어 base_link와 left_wheel_link를 연결할 수 있습니다.
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<origin xyz="0.0 0.1 0.0" rpy="1.5708 0.0 0.0"/>
<axis xyz="0 0 1"/>
</joint>
7) joint type
URDF에서 자주 쓰는 joint type은 다음과 같습니다.
| fixed | 움직이지 않는 고정 조인트 |
| continuous | 무한 회전 조인트, 바퀴에 자주 사용 |
| revolute | 제한 각도를 가진 회전 조인트 |
| prismatic | 직선 이동 조인트 |
| floating | 6자유도 움직임 |
| planar | 평면상 움직임 |
모바일 로봇에서는 보통 다음을 많이 사용합니다.
fixed : 센서, 프레임 고정
continuous : 바퀴
8) parent와 child
URDF는 트리 구조입니다.
<parent link="base_link"/>
<child link="laser_link"/>
의미:
base_link가 부모
laser_link가 자식
즉, laser_link는 base_link를 기준으로 위치가 결정됩니다.
9) axis
axis는 조인트가 어느 축으로 움직이는지 정의합니다.
<axis xyz="0 0 1"/>
의미:
z축 기준 회전
바퀴의 경우 모델 방향에 따라 보통 다음 중 하나를 사용합니다.
<axis xyz="0 1 0"/>
또는
<axis xyz="0 0 1"/>
중요한 것은 바퀴 원통의 방향과 회전축이 맞아야 한다는 점입니다.
10) collision
collision은 충돌 계산용 모양입니다.
<collision>
<geometry>
<box size="0.3 0.2 0.1"/>
</geometry>
</collision>
RViz 시각화만 할 때는 없어도 됩니다.
하지만 Gazebo 같은 시뮬레이션에서는 필요합니다.
11) inertial
inertial은 질량과 관성 정보입니다.
<inertial>
<mass value="1.0"/>
<origin xyz="0 0 0"/>
<inertia ixx="0.01" ixy="0.0" ixz="0.0"
iyy="0.01" iyz="0.0"
izz="0.01"/>
</inertial>
Gazebo 시뮬레이션이나 동역학 계산에는 중요합니다.
단순 RViz 표시에서는 없어도 됩니다.
3. TurtleBot3 URDF 분석을 통한 URDF 문법 이해하기
TurtleBot3는 ROS 학습용으로 많이 쓰이는 대표적인 모바일 로봇입니다.
turtlebot3_description 패키지는 TurtleBot3의 3D 모델을 시각화와 시뮬레이션에 사용하기 위한 패키지입니다.
일반적으로 TurtleBot3 Burger 모델에는 다음과 같은 주요 링크가 있습니다.
base_footprint
base_link
base_scan
wheel_left_link
wheel_right_link
caster_back_link
imu_link
실제 TurtleBot3 Burger URDF는 보통 .urdf.xacro 형태로 관리됩니다.
burger.urdf.xacro
여기서 중요한 점은 다음입니다.
URDF : 순수 XML 로봇 설명 파일
Xacro : URDF를 더 편하게 작성하기 위한 매크로 문법
TurtleBot3 같은 로봇은 부품이 많고 반복 구조가 있기 때문에 보통 Xacro를 사용합니다.
TurtleBot3 모델은 보통 순수 .urdf보다 .urdf.xacro 형태로 관리됩니다.
turtlebot3_burger.urdf.xacro
turtlebot3_waffle.urdf.xacro
turtlebot3_waffle_pi.urdf.xacro
1) URDF와 Xacro의 차이
URDF는 XML입니다.
Xacro는 URDF를 더 편하게 작성하기 위한 매크로 도구입니다.
예를 들어 URDF로는 같은 바퀴를 왼쪽, 오른쪽 각각 반복해서 작성해야 합니다.
하지만 Xacro를 쓰면 매크로로 공통 구조를 만들고 재사용할 수 있습니다.
URDF → 결과물
Xacro → URDF를 생성하기 위한 템플릿
TurtleBot3처럼 구조가 복잡한 로봇은 보통 Xacro를 사용합니다.
2) TurtleBot3의 기본 링크 구조
TurtleBot3 Burger를 단순화하면 대략 다음 구조입니다.
base_footprint
└── base_link
├── wheel_left_link
├── wheel_right_link
├── caster_back_link
├── imu_link
└── base_scan
각 link의 의미는 다음과 같습니다.
| base_footprint | 바닥 기준 로봇 중심 좌표계 |
| base_link | 로봇 본체 좌표계 |
| wheel_left_link | 왼쪽 바퀴 |
| wheel_right_link | 오른쪽 바퀴 |
| caster_back_link | 뒤쪽 캐스터 |
| imu_link | IMU 센서 |
| base_scan | 라이다 센서 |
3) base_footprint와 base_link
모바일 로봇에서는 보통 base_footprint와 base_link를 구분합니다.
base_footprint : 바닥면에 붙어 있는 기준 좌표계
base_link : 로봇 몸체 중심 좌표계
예제 구조는 다음과 같습니다.
<link name="base_footprint"/>
<joint name="base_joint" type="fixed">
<parent link="base_footprint"/>
<child link="base_link"/>
<origin xyz="0.0 0.0 0.010" rpy="0 0 0"/>
</joint>
<link name="base_link">
<visual>
<geometry>
<box size="0.14 0.14 0.08"/>
</geometry>
</visual>
</link>
해석:
base_footprint는 바닥 기준 좌표계
base_link는 base_footprint보다 z 방향으로 약간 위에 있음
base_joint는 fixed이므로 두 링크는 고정 관계
4) 왼쪽 바퀴 링크
TurtleBot3 Burger의 바퀴는 좌우에 있습니다.
단순화하면 다음과 같이 표현할 수 있습니다.
<link name="wheel_left_link">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.033" length="0.018"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 1.0"/>
</material>
</visual>
</link>
<joint name="wheel_left_joint" type="continuous">
<parent link="base_link"/>
<child link="wheel_left_link"/>
<origin xyz="0.0 0.08 0.0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
핵심 문법:
<joint name="wheel_left_joint" type="continuous">
바퀴는 계속 회전해야 하므로 continuous를 사용합니다.
<parent link="base_link"/>
<child link="wheel_left_link"/>
wheel_left_link는 base_link에 연결됩니다.
<origin xyz="0.0 0.08 0.0"/>
왼쪽 바퀴는 base_link 기준 y 방향으로 +0.08m 위치에 있습니다.
<axis xyz="0 1 0"/>
y축 기준으로 회전합니다.
5) 오른쪽 바퀴 링크
오른쪽 바퀴는 y 방향 위치만 반대입니다.
<link name="wheel_right_link">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.033" length="0.018"/>
</geometry>
<material name="black">
<color rgba="0.0 0.0 0.0 1.0"/>
</material>
</visual>
</link>
<joint name="wheel_right_joint" type="continuous">
<parent link="base_link"/>
<child link="wheel_right_link"/>
<origin xyz="0.0 -0.08 0.0" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
차이점:
<origin xyz="0.0 -0.08 0.0"/>
오른쪽이므로 y 방향이 음수입니다.
6) 라이다 센서 링크
TurtleBot3 Burger에는 라이다가 위쪽에 장착됩니다.
단순화하면 다음과 같습니다.
<link name="base_scan">
<visual>
<geometry>
<cylinder radius="0.04" length="0.03"/>
</geometry>
<material name="green">
<color rgba="0.0 1.0 0.0 1.0"/>
</material>
</visual>
</link>
<joint name="scan_joint" type="fixed">
<parent link="base_link"/>
<child link="base_scan"/>
<origin xyz="0.0 0.0 0.13" rpy="0 0 0"/>
</joint>
해석:
base_scan은 라이다 프레임
base_link 기준 z 방향 13cm 위에 있음
라이다는 본체에 고정되어 있으므로 fixed joint 사용
7) TurtleBot3 Burger URDF에서 봐야 할 핵심
TurtleBot3 Burger URDF를 볼 때 중요한 것은 모델의 복잡한 수치보다 구조입니다.
봐야 할 순서:
1. robot name 확인
2. link 목록 확인
3. joint 목록 확인
4. parent-child 관계 확인
5. fixed joint와 continuous joint 구분
6. base_link, base_footprint, base_scan 위치 확인
7. wheel joint의 axis 확인
URDF를 처음 공부할 때는 mesh 모델보다 아래 요소를 먼저 봐야 합니다.
link
joint
origin
axis
parent
child
visual
collision
inertial
4. URDF 실습 예제
실습 목표
단순한 2륜 모바일 로봇 URDF를 만들어 봅니다.
완성 구조는 다음과 같습니다.
base_footprint
└── base_link
├── left_wheel_link
├── right_wheel_link
├── caster_link
└── laser_link
완성 후 RViz2에서 로봇 모델을 확인합니다.
1) 필요한 패키지 설치
sudo apt update
sudo apt install ros-humble-joint-state-publisher-gui
sudo apt install ros-humble-robot-state-publisher
sudo apt install ros-humble-xacro
sudo apt install ros-humble-rviz2


2) 워크스페이스 이동
cd ~/ros2_study
5. Python 패키지 my_first_package에서 URDF 실습 구성하기
이번 실습에서는 기존 Python 패키지인 my_first_package 안에 URDF와 launch 파일을 추가합니다.
패키지 구조는 다음처럼 만듭니다.
ros2_study/
└── src/
└── my_first_package/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│ └── my_first_package
├── my_first_package/
│ └── __init__.py
├── urdf/
│ └── simple_mobile_robot.urdf
├── launch/
│ └── display_simple_robot.launch.py
└── rviz/
└── simple_robot.rviz
1) 폴더 생성
launch와 rviz 폴더는 이미 생성하였습니다.
cd ~/ros2_study/src/my_first_package
mkdir -p urdf


2) URDF 파일 작성
touch urdf/simple_mobile_robot.urdf
내용은 그대로 사용하면 됩니다.
<?xml version="1.0"?>
<robot name="simple_mobile_robot">
<material name="blue">
<color rgba="0.0 0.2 0.8 1.0"/>
</material>
<material name="black">
<color rgba="0.0 0.0 0.0 1.0"/>
</material>
<material name="gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>
<material name="red">
<color rgba="0.8 0.0 0.0 1.0"/>
</material>
<link name="base_footprint"/>
<link name="base_link">
<visual>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry>
<box size="0.30 0.20 0.10"/>
</geometry>
<material name="blue"/>
</visual>
<collision>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<geometry>
<box size="0.30 0.20 0.10"/>
</geometry>
</collision>
<inertial>
<origin xyz="0 0 0.05" rpy="0 0 0"/>
<mass value="2.0"/>
<inertia
ixx="0.01" ixy="0.0" ixz="0.0"
iyy="0.02" iyz="0.0"
izz="0.03"/>
</inertial>
</link>
<joint name="base_footprint_to_base_link" type="fixed">
<parent link="base_footprint"/>
<child link="base_link"/>
<origin xyz="0 0 0.03" rpy="0 0 0"/>
</joint>
<link name="left_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.03"/>
</geometry>
<material name="black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.03"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia
ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0"
izz="0.001"/>
</inertial>
</link>
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel_link"/>
<origin xyz="0 0.115 0.04" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<link name="right_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.03"/>
</geometry>
<material name="black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="1.5708 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.03"/>
</geometry>
</collision>
<inertial>
<mass value="0.2"/>
<inertia
ixx="0.001" ixy="0.0" ixz="0.0"
iyy="0.001" iyz="0.0"
izz="0.001"/>
</inertial>
</link>
<joint name="right_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="right_wheel_link"/>
<origin xyz="0 -0.115 0.04" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
<link name="caster_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.025"/>
</geometry>
<material name="gray"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<sphere radius="0.025"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia
ixx="0.0001" ixy="0.0" ixz="0.0"
iyy="0.0001" iyz="0.0"
izz="0.0001"/>
</inertial>
</link>
<joint name="caster_joint" type="fixed">
<parent link="base_link"/>
<child link="caster_link"/>
<origin xyz="-0.12 0 0.02" rpy="0 0 0"/>
</joint>
<link name="laser_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.04"/>
</geometry>
<material name="red"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder radius="0.04" length="0.04"/>
</geometry>
</collision>
<inertial>
<mass value="0.1"/>
<inertia
ixx="0.0001" ixy="0.0" ixz="0.0"
iyy="0.0001" iyz="0.0"
izz="0.0001"/>
</inertial>
</link>
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser_link"/>
<origin xyz="0.10 0 0.13" rpy="0 0 0"/>
</joint>
</robot>

전체 구조
로봇 이름은 simple_mobile_robot입니다. 기본 기준 좌표계로 base_footprint를 사용하고, 실제 로봇 몸체는 base_link로 정의되어 있습니다.
base_link는 파란색 직육면체 형태의 본체입니다. 크기는 다음과 같습니다.
<box size="0.30 0.20 0.10"/>
즉, 길이 0.30m, 폭 0.20m, 높이 0.10m의 박스형 차체입니다.
기준 프레임
<link name="base_footprint"/>
base_footprint는 바닥 기준 프레임입니다. 로봇의 위치를 표현할 때 주로 사용됩니다.
<joint name="base_footprint_to_base_link" type="fixed">
이 조인트는 base_footprint와 base_link를 고정 연결합니다. base_link는 바닥 기준에서 z축으로 0.03m 위에 위치합니다.
좌우 바퀴
왼쪽 바퀴는 left_wheel_link, 오른쪽 바퀴는 right_wheel_link입니다. 두 바퀴 모두 원통형으로 정의되어 있습니다.
<cylinder radius="0.04" length="0.03"/>
바퀴 반지름은 0.04m, 두께는 0.03m입니다.
좌우 바퀴 조인트는 모두 continuous 타입입니다.
<joint name="left_wheel_joint" type="continuous">
<joint name="right_wheel_joint" type="continuous">
이는 바퀴가 계속 회전할 수 있다는 의미입니다. 회전축은 다음과 같이 y축 방향입니다.
<axis xyz="0 1 0"/>
따라서 이 로봇은 좌우 바퀴 속도 차이를 이용해 전진, 후진, 회전을 수행하는 차동구동 로봇 구조입니다.
캐스터 휠
<link name="caster_link">
캐스터는 회색 구 형태로 표현되어 있습니다.
<sphere radius="0.025"/>
반지름은 0.025m입니다. 위치는 본체 뒤쪽에 해당하는 x축 -0.12m 지점입니다.
<origin xyz="-0.12 0 0.02"/>
조인트 타입은 fixed이므로 실제 자유 회전 캐스터라기보다는, URDF상에서는 단순 보조 지지구로 표현되어 있습니다.
라이다 센서
<link name="laser_link">
라이다는 빨간색 원통형 링크로 정의되어 있습니다.
<cylinder radius="0.04" length="0.04"/>
본체 앞쪽 상단에 장착되어 있습니다.
<origin xyz="0.10 0 0.13"/>
즉, base_link 기준으로 x축 0.10m 앞, z축 0.13m 높이에 위치합니다.
물리 속성
각 링크에는 visual, collision, inertial 정보가 들어 있습니다.
visual은 RViz나 Gazebo에서 보이는 형상이고, collision은 충돌 계산에 사용되는 형상입니다. inertial은 질량과 관성 정보를 정의합니다.
본체 질량은 2.0kg이고, 각 바퀴는 0.2kg, 캐스터와 라이다는 각각 0.1kg으로 설정되어 있습니다.
6. URDF 문법 확인
cd ~/ros2_study/src/my_first_package
check_urdf urdf/simple_mobile_robot.urdf
check_urdf가 없다면 설치합니다.
sudo apt install liburdfdom-tools
정상 출력 예:
robot name is: simple_mobile_robot
---------- Successfully Parsed XML ---------------
root Link: base_footprint

7. launch 파일 작성
touch launch/display_simple_robot.launch.py

Python 패키지 기준으로 FindPackageShare('my_first_package')를 사용합니다.
import os
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
pkg_share = FindPackageShare(package='my_first_package').find('my_first_package')
default_model_path = os.path.join(
pkg_share,
'urdf',
'simple_mobile_robot.urdf'
)
default_rviz_config_path = os.path.join(
pkg_share,
'rviz',
'simple_robot.rviz'
)
model_arg = DeclareLaunchArgument(
name='model',
default_value=default_model_path,
description='Absolute path to robot urdf file'
)
robot_description = Command([
'cat ',
LaunchConfiguration('model')
])
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[
{
'robot_description': robot_description
}
],
output='screen'
)
joint_state_publisher_gui_node = Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
output='screen'
)
rviz_node = Node(
package='rviz2',
executable='rviz2',
arguments=[
'-d',
default_rviz_config_path
],
output='screen'
)
return LaunchDescription([
model_arg,
robot_state_publisher_node,
joint_state_publisher_gui_node,
rviz_node
])

이 launch 파일을 실행하면 다음 3개가 동시에 실행됩니다.
robot_state_publisher
joint_state_publisher_gui
rviz2
Command는 터미널 명령어 실행 결과를 launch 안에서 사용할 때 사용합니다.
from launch.substitutions import Command, LaunchConfiguration
여기서는 다음 명령을 실행하는 용도입니다.
cat simple_mobile_robot.urdf
LaunchConfiguration은 launch 인자 값을 가져올 때 사용합니다.
from launch_ros.substitutions import FindPackageShare
ROS 2 패키지의 share 폴더 경로를 찾기 위해 사용합니다.
여기서는 my_first_package 패키지의 설치 경로를 찾습니다.
pkg_share = FindPackageShare(package='my_first_package').find('my_first_package')
my_first_package 패키지의 share 디렉터리 경로를 찾습니다.
빌드 후에는 보통 다음과 같은 위치를 가리킵니다.
~/ros2_study/install/my_first_package/share/my_first_package
이 경로 안에 urdf, rviz, launch 폴더가 설치되어 있어야 합니다.
URDF 파일의 전체 경로를 만듭니다.
default_model_path = os.path.join(
pkg_share,
'urdf',
'simple_mobile_robot.urdf'
)
결과는 다음과 같은 형태입니다.
~/ros2_study/install/my_first_package/share/my_first_package/urdf/simple_mobile_robot.urdf
이 파일은 로봇의 구조를 정의한 파일입니다.
RViz 설정 파일의 전체 경로를 만듭니다.
default_rviz_config_path = os.path.join(
pkg_share,
'rviz',
'simple_robot.rviz'
)
이 파일에는 RViz에서 어떤 화면 구성을 사용할지 들어 있습니다.
model이라는 launch 인자를 선언합니다.
model_arg = DeclareLaunchArgument(
name='model',
default_value=default_model_path,
description='Absolute path to robot urdf file'
)
기본값은 위에서 만든 URDF 파일 경로입니다.
즉, 기본 실행은 다음 URDF를 사용합니다.
simple_mobile_robot.urdf
하지만 실행할 때 다른 URDF 파일을 지정할 수도 있습니다.
예:
ros2 launch my_first_package display_simple_robot.launch.py model:=/home/user/test.urdf
이 부분은 URDF 파일 내용을 읽어서 robot_description 파라미터로 넘기기 위한 코드입니다.
robot_description = Command([
'cat ',
LaunchConfiguration('model')
])
실제로는 다음 명령과 비슷합니다.
cat ~/ros2_study/install/my_first_package/share/my_first_package/urdf/simple_mobile_robot.urdf
ROS 2에서 로봇 모델은 보통 robot_description이라는 이름의 파라미터로 전달합니다.
robot_state_publisher 노드를 실행합니다.
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[
{
'robot_description': robot_description
}
],
output='screen'
)
이 노드의 역할은 다음과 같습니다.
URDF 파일을 읽는다.
link와 joint 관계를 해석한다.
TF 정보를 발행한다.
즉, URDF에 있는 다음 구조를 ROS 2 TF로 변환합니다.
base_footprint
└── base_link
├── left_wheel_link
├── right_wheel_link
├── caster_link
└── laser_link
joint_state_publisher_gui 노드를 실행합니다.
joint_state_publisher_gui_node = Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
output='screen'
)
이 노드는 조인트 값을 GUI로 조절할 수 있게 해줍니다.
예를 들어 바퀴 조인트가 continuous 타입이면 GUI에서 조인트 값을 움직여볼 수 있습니다.
이 노드는 /joint_states 토픽을 발행합니다.
/joint_states
rviz_node = Node(
package='rviz2',
executable='rviz2',
arguments=[
'-d',
default_rviz_config_path
],
output='screen'
)
-d 옵션은 RViz 설정 파일을 지정하는 옵션입니다.
즉, 다음 파일을 불러와서 RViz를 실행합니다.
rviz/simple_robot.rviz
이 설정 파일이 있으면 RViz를 켤 때마다 매번 RobotModel, TF, Grid를 직접 추가하지 않아도 됩니다.
실제로 launch에서 실행할 항목들을 등록합니다.
return LaunchDescription([
model_arg,
robot_state_publisher_node,
joint_state_publisher_gui_node,
rviz_node
])
실행 순서상 다음 항목들이 launch 시스템에 전달됩니다.
model 인자 등록
robot_state_publisher 실행
joint_state_publisher_gui 실행
rviz2 실행
8. RViz 설정 파일 작성
touch rviz/simple_robot.rviz

내용:
Panels:
- Class: rviz_common/Displays
Name: Displays
Visualization Manager:
Class: ""
Displays:
- Alpha: 0.5
Cell Size: 0.1
Class: rviz_default_plugins/Grid
Color: 160; 160; 164
Enabled: true
Name: Grid
Plane: XY
Plane Cell Count: 20
Reference Frame: base_footprint
Value: true
- Class: rviz_default_plugins/RobotModel
Description Topic:
Value: /robot_description
Enabled: true
Name: RobotModel
Value: true
- Class: rviz_default_plugins/TF
Enabled: true
Name: TF
Value: true
Enabled: true
Global Options:
Fixed Frame: base_footprint
Name: root
Tools:
- Class: rviz_default_plugins/Interact
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
Value: true
Window Geometry:
Height: 800
Width: 1200

9. setup.py 수정
Python 패키지는 CMakeLists.txt가 아니라 **setup.py**에서 설치 파일을 등록해야 합니다. 기존의 setup.py 파일을 이용합니다.
기존 setup.py에 data_files 항목에 urdf 파일 설치를 다음처럼 추가합니다.
import os
from glob import glob
from setuptools import setup
package_name = 'my_first_package'
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']
),
(
'share/' + package_name + '/launch',
glob.glob(os.path.join('launch', '*.launch.py'))
),
(
'share/' + package_name + '/config',
glob.glob(os.path.join('config', '*.yaml'))
),
(
'share/' + package_name + '/urdf',
glob.glob(os.path.join('urdf', '*.urdf'))
),
(
'share/' + package_name + '/rviz',
glob.glob(os.path.join('rviz', '*.rviz'))
),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='user',
maintainer_email='user@example.com',
description='Python package for ROS 2 URDF practice',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
],
},
)
중요한 부분은 이것입니다.
(
'share/' + package_name + '/launch',
glob.glob(os.path.join('launch', '*.launch.py'))
),
(
'share/' + package_name + '/urdf',
glob.glob(os.path.join('urdf', '*.urdf'))
),
(
'share/' + package_name + '/rviz',
glob.glob(os.path.join('rviz', '*.rviz'))
)

이 설정이 없으면 빌드 후에 launch, urdf, rviz 파일이 install 폴더로 복사되지 않습니다.
그러면 아래 명령이 실패합니다.
ros2 launch my_first_package display_simple_robot.launch.py
10. package.xml 수정
Python 패키지 기준으로 다음 의존성을 추가합니다.
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>joint_state_publisher</exec_depend>
<exec_depend>joint_state_publisher_gui</exec_depend>
<exec_depend>rviz2</exec_depend>
<exec_depend>xacro</exec_depend>
예시:
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>my_first_package</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="sjyong@todo.todo">sjyong</maintainer>
<license>TODO: License declaration</license>
<exec_depend>rclpy</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>my_first_package_msgs</exec_depend>
<exec_depend>turtlesim</exec_depend>
<exec_depend>robot_state_publisher</exec_depend>
<exec_depend>joint_state_publisher</exec_depend>
<exec_depend>joint_state_publisher_gui</exec_depend>
<exec_depend>rviz2</exec_depend>
<exec_depend>xacro</exec_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>

<export>
<build_type>ament_python</build_type>
</export>
my_first_package는 Python 패키지이므로 반드시 ament_python을 사용해야 합니다.
11. 빌드
cd ~/ros2_study
colcon build --packages-select my_first_package
환경 설정:
source install/setup.bash

12. 실행
ros2 launch my_first_package display_simple_robot.launch.py
정상 실행되면 다음 창이 뜹니다.
RViz2
joint_state_publisher_gui
RViz에서 로봇 모델이 보이면 성공입니다.


만약 에러가 발생하면 아래와 같이 joint-state-publisher-gui를 다시 설치하시기 바랍니다.
sudo apt update
sudo apt install ros-humble-joint-state-publisher-gui
13. 토픽 확인
다른 터미널을 열고 다음을 실행합니다.
source ~/ros2_study/install/setup.bash
ros2 topic list
확인할 토픽:
/robot_description
/joint_states
/tf
/tf_static

14. TF 트리 확인
ros2 run tf2_tools view_frames
frames.pdf가 생성됩니다.
예상 구조:
base_footprint
└── base_link
├── left_wheel_link
├── right_wheel_link
├── caster_link
└── laser_link


tf2_tools가 없다면 설치합니다.
sudo apt install ros-humble-tf2-tools
15. 실습 예제
1) 미션 1: 로봇 몸체 크기 변경
URDF에서 base_link의 박스 크기를 변경합니다.
기존: 기존 내용을 복사해서 사용하시고 기존 소스는 주석처리해서 보관합니다.
<box size="0.30 0.20 0.10"/>
변경:
<box size="0.40 0.25 0.12"/>
확인할 것:
RViz에서 몸체가 커지는가?
바퀴 위치가 어색해지지 않는가?
2) 미션 2: 라이다 위치 변경
기존:
<origin xyz="0.10 0 0.13" rpy="0 0 0"/>
변경:
<origin xyz="0.15 0 0.16" rpy="0 0 0"/>
확인할 것:
laser_link가 앞으로 이동하는가?
laser_link가 위로 올라가는가?
TF에서 위치가 바뀌는가?

3) 미션 3: 카메라 추가
다음 link를 추가합니다.
<link name="camera_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<box size="0.04 0.04 0.03"/>
</geometry>
<material name="gray"/>
</visual>
</link>
<joint name="camera_joint" type="fixed">
<parent link="base_link"/>
<child link="camera_link"/>
<origin xyz="0.16 0 0.12" rpy="0 0 0"/>
</joint>
넣을 위치는 </robot> 바로 위입니다.

확인:
check_urdf urdf/simple_mobile_robot.urdf

다시 빌드:
cd ~/ros2_study
colcon build --packages-select my_cpp_package
source install/setup.bash
ros2 launch my_cpp_package display_simple_robot.launch.py
RViz2에서 camera_link가 보이면 성공입니다.

4) 미션 4: TF 구조 확인
새 터미널에서 실행합니다.
source ~/ros2_study/install/setup.bash
ros2 run tf2_tools view_frames
결과로 frames.pdf가 생성됩니다.
확인할 구조:
base_footprint
└── base_link
├── left_wheel_link
├── right_wheel_link
├── caster_link
├── laser_link
└── camera_link

16. Python 패키지와 연결하는 추가 실습
기존 Python 패키지 my_first_package를 활용해 /robot_description 토픽이 실제로 발행되는지 확인하는 노드를 만들 수 있습니다.
1) Python 노드 작성
cd ~/ros2_study/src/my_first_package/my_first_package
touch robot_description_listener.py

내용:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from rclpy.qos import QoSProfile
from rclpy.qos import QoSDurabilityPolicy
from rclpy.qos import QoSReliabilityPolicy
class RobotDescriptionListener(Node):
def __init__(self):
super().__init__('robot_description_listener')
qos_profile = QoSProfile(depth=1)
qos_profile.durability = QoSDurabilityPolicy.TRANSIENT_LOCAL
qos_profile.reliability = QoSReliabilityPolicy.RELIABLE
self.subscription = self.create_subscription(
String,
'/robot_description',
self.listener_callback,
qos_profile
)
self.received_once = False
def listener_callback(self, msg):
if not self.received_once:
self.get_logger().info('Received /robot_description topic')
self.get_logger().info(f'URDF length: {len(msg.data)} characters')
self.received_once = True
def main(args=None):
rclpy.init(args=args)
node = RobotDescriptionListener()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

/robot_description은 일반 센서 토픽처럼 계속 반복 발행되는 토픽이 아닙니다.
예를 들어 /scan, /odom, /cmd_vel은 계속 발행됩니다.
/scan
/odom
/cmd_vel
하지만 /robot_description은 보통 로봇 모델 URDF 내용을 담고 있고, robot_state_publisher가 시작할 때 한 번 발행하는 성격이 강합니다.
그래서 listener가 나중에 실행되면 이미 지나간 메시지를 못 받을 수 있습니다.
이때 필요한 QoS가 이것입니다.
QoSDurabilityPolicy.TRANSIENT_LOCAL
의미는 간단합니다.
나중에 구독자가 붙어도 마지막 메시지를 받을 수 있게 해라.
2) setup.py 수정
~/ros2_study/src/my_first_package/setup.py에서 entry_points에 추가합니다.
entry_points={
'console_scripts': [
'robot_description_listener = my_first_package.robot_description_listener:main',
],
},
기존 노드가 있다면 쉼표로 이어서 추가합니다.
예:
entry_points={
'console_scripts': [
'my_node = my_first_package.my_node:main',
'robot_description_listener = my_first_package.robot_description_listener:main',
],
},

3) 빌드
cd ~/ros2_study
colcon build --packages-select my_first_package
source install/setup.bash

4) 실행
첫 번째 터미널:
source ~/ros2_study/install/setup.bash
ros2 launch my_first_package display_simple_robot.launch.py
두 번째 터미널:
source ~/ros2_study/install/setup.bash
ros2 run my_first_package robot_description_listener
예상 출력:
[INFO] [robot_description_listener]: Received /robot_description topic
[INFO] [robot_description_listener]: URDF length: 5000 characters

17. 정리
URDF에서 제일 중요한 것은 세 가지입니다.
link = 로봇의 부품
joint = 부품과 부품의 연결
origin = 부모 link 기준 자식 link의 위치와 자세
실무에서 URDF는 단순한 3D 모델 파일이 아닙니다.
RViz 시각화
TF 좌표계
센서 위치 정의
시뮬레이션
Navigation2
MoveIt2
로봇 제어
에 모두 연결됩니다.
따라서 URDF를 잘못 만들면 로봇이 RViz에서 이상하게 보이는 정도가 아니라, 센서 데이터 해석, 자율주행, 로봇팔 제어까지 전부 틀어질 수 있습니다.
URDF는 ROS 로봇 시스템의 뼈대입니다.
'강좌 > ROS2' 카테고리의 다른 글
| 서비스 서버 만들기 (0) | 2026.05.21 |
|---|---|
| 서비스 정의 만들기 (0) | 2026.05.21 |
| launch 파일에서 실행 인자 사용하기 #1 (0) | 2026.05.15 |
| ROS 2 C++ 실행 인자 사용하기 (0) | 2026.05.15 |
| ROS 2 C++ 파라미터 실습 (0) | 2026.05.15 |

