지난 장에서 우리는 URDF로 로봇을 그렸다. link는 로봇의 부품이고, joint는 부품과 부품을 연결하는 관절이라고 했다. 차체를 만들고, 바퀴를 붙이고, 센서 위치를 잡으면 RViz에서 로봇이 모습을 드러낸다. 여기까지 오면 꽤 뿌듯하다.
그런데 곧 불편함이 찾아온다. 바퀴 하나를 만들 때는 괜찮았는데, 왼쪽 바퀴와 오른쪽 바퀴를 만들기 시작하면 비슷한 XML을 계속 복사하게 된다. 2WD 로봇이라면 바퀴가 두 개, 4WD 로봇이라면 네 개다. 여기에 캐스터, LiDAR, IMU, RGB-D 카메라, 배터리 박스, 센서 마운트를 붙이면 URDF 파일은 어느새 긴 두루마리처럼 늘어난다.
더 큰 문제는 수정할 때 생긴다. 바퀴 반지름을 0.05에서 0.06으로 바꾸고 싶다고 해 보자. 시각 형상에도 있고, 충돌 형상에도 있고, 관성 계산에도 있고, 조인트 위치에도 영향을 준다. 한 곳을 고치고 다른 곳을 놓치면 로봇은 RViz에서는 멀쩡해 보이는데 Gazebo에서는 바퀴가 바닥에 박히거나 공중에 뜰 수 있다. 로봇이 이상하게 움직일 때 원인이 제어 코드가 아니라 복사 붙여넣기 실수일 수도 있다는 뜻이다.
이 문제를 해결하기 위해 사용하는 도구가 xacro다. xacro는 XML Macro의 줄임말로, URDF를 더 짧고 재사용 가능하게 작성하기 위한 매크로 언어다. xacro 파일은 최종적으로 URDF로 변환되어 robot_state_publisher, RViz, Gazebo 같은 도구에서 사용된다. ROS 2 공식 문서에서도 xacro의 핵심 장점으로 상수, 수식, 매크로를 통해 URDF의 반복을 줄이고 유지보수성을 높이는 점을 설명한다.1

URDF가 길어지는 순간
처음 만든 이동 로봇을 떠올려 보자. 차체는 박스 하나, 바퀴는 원기둥 두 개, LiDAR는 작은 원기둥 하나일 수 있다. 이 정도면 순수 URDF도 읽을 만하다. 하지만 자율주행 로봇은 대개 여기서 멈추지 않는다.
처음에는 left_wheel_link와 right_wheel_link만 있던 모델에 앞뒤 캐스터가 붙는다. 나중에는 충돌 영역을 더 정확히 만들기 위해 collision 형상을 따로 조정한다. 시뮬레이션을 위해 각 링크의 inertial도 추가한다. 카메라 높이를 바꾸고, LiDAR 위치를 2cm 앞으로 옮기고, 배터리 무게를 반영하다 보면 같은 숫자가 여러 곳에 흩어진다.
이때 순수 URDF는 마치 계산기 없이 설계 도면을 손으로 다시 그리는 느낌이다. 한 줄을 바꾸면 관련된 다섯 줄을 기억해서 같이 바꿔야 한다. 사람은 이런 일을 잘 못한다. 컴퓨터가 잘한다. xacro는 바로 이 반복과 계산을 컴퓨터에게 맡기는 방법이다.
xacro를 사용하면 다음과 같은 값들을 한곳에 모아 둘 수 있다.
바퀴 반지름, 바퀴 폭, 차체 길이, 차체 폭, 차체 높이
휠베이스, 트랙 폭, 센서 높이, 센서 전후 위치
링크 색상, 재질 이름, 질량, 관성 계산에 필요한 치수
여기서 중요한 관점은 "URDF를 예쁘게 줄인다"가 아니다. 진짜 목표는 로봇 모델의 기준값을 한곳에서 관리한다는 것이다. 로봇 모델은 단순한 그림이 아니라 TF 트리, 충돌 형상, 시뮬레이션 물리, Nav2 설정의 기반이 된다. 기준값이 흩어지면 시스템 전체가 흔들린다.
xacro 파일의 기본 형태
xacro 파일은 XML 형식을 따른다. 다만 일반 URDF와 달리 <robot> 태그에 xacro 네임스페이스를 추가한다. 네임스페이스는 "이 파일 안에서 xacro:로 시작하는 태그를 xacro 문법으로 해석하라"는 표시다.
<?xml version="1.0"?>
<robot name="my_mobile_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<link name="base_link"/>
</robot>파일 이름은 보통 robot.urdf.xacro처럼 붙인다. 확장자가 길어 보이지만 의미가 분명하다. "이 파일은 URDF를 생성하기 위한 xacro 파일"이라는 뜻이다.
xacro 파일은 직접 URDF로 변환할 수 있다.
ros2 run xacro xacro my_robot.urdf.xacro -o my_robot.urdf또는 더 짧게 다음처럼 쓸 수도 있다.
xacro my_robot.urdf.xacro > my_robot.urdfROS 2에서는 launch 파일에서 xacro를 실행해 robot_description 파라미터에 바로 넣는 방식도 자주 쓴다. 이렇게 하면 xacro 파일을 수정했을 때 별도의 URDF 파일을 계속 생성해 두지 않아도 된다. 다만 launch 시작 시 xacro 변환 시간이 조금 추가될 수 있다.1
설치가 필요하다면 배포판 이름에 맞춰 다음처럼 설치한다.
sudo apt install ros-$ROS_DISTRO-xacro예를 들어 Jazzy를 사용 중이고 ROS_DISTRO 환경 변수가 jazzy로 설정되어 있다면 ros-jazzy-xacro 패키지가 설치된다.
property로 숫자에 이름 붙이기
xacro의 첫 번째 무기는 property다. 프로그래밍 언어의 상수나 변수처럼, 반복해서 쓰는 값에 이름을 붙인다.
아래는 차체와 바퀴 치수를 property로 분리한 예다.
<?xml version="1.0"?>
<robot name="xacro_mobile_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:property name="base_length" value="0.45"/>
<xacro:property name="base_width" value="0.32"/>
<xacro:property name="base_height" value="0.12"/>
<xacro:property name="wheel_radius" value="0.055"/>
<xacro:property name="wheel_width" value="0.035"/>
<link name="base_link">
<visual>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
</visual>
<collision>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
</collision>
</link>
</robot>${base_length}는 xacro가 URDF로 변환될 때 0.45로 바뀐다. 눈으로 보기에는 작은 변화지만, 유지보수 관점에서는 큰 차이다. 차체 길이를 바꾸고 싶으면 이제 base_length 한 줄만 수정하면 된다.
property의 장점은 숫자뿐 아니라 의도를 담는 데 있다. 0.32라는 숫자만 보면 이것이 차체 폭인지, 센서 간격인지, 바퀴 위치인지 알기 어렵다. 하지만 base_width라고 쓰면 모델을 읽는 사람이 바로 이해한다. 로봇 모델 파일은 미래의 나, 그리고 팀 동료가 읽는 문서이기도 하다.
property 이름에는 하이픈보다 언더스코어를 사용하는 편이 안전하다. 예를 들어 wheel-radius보다 wheel_radius가 낫다. 수식에서 다루기도 쉽고, 다른 ROS 2 설정 파일과 함께 볼 때도 일관성이 좋다.
xacro 안에서 계산하기
xacro는 단순히 값을 치환하는 데서 끝나지 않는다. ${...} 안에서 기본적인 수식도 계산할 수 있다. ROS 2 공식 URDF 튜토리얼에서도 바퀴 지름을 반으로 나누어 반지름을 만들거나, 좌우 대칭 위치를 계산하는 식의 예를 보여 준다.1
이 기능은 이동 로봇 모델에서 특히 유용하다. 바퀴 위치는 차체 폭, 바퀴 폭, 휠베이스와 연결되어 있기 때문이다.
<xacro:property name="base_length" value="0.45"/>
<xacro:property name="base_width" value="0.32"/>
<xacro:property name="base_height" value="0.12"/>
<xacro:property name="wheel_radius" value="0.055"/>
<xacro:property name="wheel_width" value="0.035"/>
<xacro:property name="wheel_x" value="0.12"/>
<xacro:property name="wheel_y" value="${base_width / 2.0 + wheel_width / 2.0}"/>
<xacro:property name="wheel_z" value="${-base_height / 2.0 + wheel_radius}"/>여기서 wheel_y는 바퀴가 차체 옆에 붙는 위치다. 차체 폭의 절반만큼 옆으로 가고, 거기에 바퀴 폭의 절반을 더한다. 이렇게 계산해 두면 차체 폭을 바꾸더라도 바퀴 위치가 함께 따라간다.
wheel_z도 흥미롭다. base_link를 차체 중심에 둔다면 차체 바닥은 -base_height / 2.0 위치에 있다. 바퀴 중심은 바닥보다 바퀴 반지름만큼 위에 있어야 하므로 -base_height / 2.0 + wheel_radius가 된다.
이런 계산은 사소해 보이지만, 실제로는 로봇 모델의 품질을 결정한다. 바퀴가 바닥에 정확히 닿는지, 충돌 형상이 시각 형상과 어긋나지 않는지, 센서 프레임이 의도한 높이에 있는지가 모두 이런 숫자에서 시작된다.
material도 한곳에서 관리하기
로봇 모델을 만들 때 색상과 재질도 반복된다. 차체는 파란색, 바퀴는 검은색, 센서 마운트는 회색처럼 정해 두면 모델을 보기 쉽다. xacro property와는 별개로 URDF의 material 정의를 한곳에 모아 둘 수 있다.
<material name="body_blue">
<color rgba="0.1 0.2 0.9 1.0"/>
</material>
<material name="wheel_black">
<color rgba="0.02 0.02 0.02 1.0"/>
</material>
<material name="sensor_gray">
<color rgba="0.5 0.5 0.5 1.0"/>
</material>그다음 링크 안에서는 이름만 참조한다.
<visual>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
<material name="body_blue"/>
</visual>작은 프로젝트에서는 색상이 별것 아닌 것처럼 느껴질 수 있다. 하지만 RViz에서 TF, LaserScan, RobotModel을 함께 볼 때 로봇 부품이 명확히 구분되면 디버깅 속도가 빨라진다. LiDAR가 어디에 붙었는지, 카메라가 어느 방향을 보는지, 바퀴가 좌우 바뀌지는 않았는지 한눈에 보인다.
macro로 바퀴 템플릿 만들기
property가 반복되는 숫자를 줄인다면, xacro:macro는 반복되는 구조를 줄인다. 매크로는 URDF 조각을 만드는 템플릿이다. 바퀴처럼 모양은 같고 이름과 위치만 다른 요소에 딱 맞다.
먼저 바퀴 하나를 만드는 매크로를 작성해 보자.
<xacro:macro name="wheel" params="prefix x y z">
<link name="${prefix}_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
<material name="wheel_black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
</collision>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel_link"/>
<origin xyz="${x} ${y} ${z}" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
</xacro:macro>이 매크로는 prefix, x, y, z를 매개변수로 받는다. prefix는 링크와 조인트 이름을 구분하기 위해 사용한다. left를 넣으면 left_wheel_link, left_wheel_joint가 만들어지고, right를 넣으면 right_wheel_link, right_wheel_joint가 만들어진다.
이제 바퀴 두 개를 만드는 코드는 아주 짧아진다.
<xacro:wheel prefix="left" x="${wheel_x}" y="${wheel_y}" z="${wheel_z}"/>
<xacro:wheel prefix="right" x="${wheel_x}" y="${-wheel_y}" z="${wheel_z}"/>여기서 오른쪽 바퀴의 y 값은 -${wheel_y}다. 좌우 대칭 구조를 숫자 하나로 표현한 것이다. 로봇 모델링에서 이런 대칭은 계속 등장한다. 좌우 바퀴, 좌우 캐스터, 좌우 모터 브래킷, 좌우 범퍼 센서까지 모두 같은 패턴을 갖는다.
매크로를 사용하면 복사 붙여넣기가 사라지는 것처럼 보이지만, 더 정확히 말하면 복사 붙여넣기를 선언으로 바꾸는 것이다. "바퀴 XML을 한 번 더 붙여넣자"가 아니라 "여기에 오른쪽 바퀴를 하나 생성하자"라고 작성하는 방식이다. 코드가 로봇 구조를 더 직접적으로 말하게 된다.
2WD 로봇을 xacro로 정리하기
이제 작은 2WD 이동 로봇 모델의 뼈대를 구성해 보자. 차체 중심을 base_link로 두고, 좌우 바퀴가 continuous 조인트로 연결되는 구조다.
<?xml version="1.0"?>
<robot name="two_wheel_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:property name="base_length" value="0.45"/>
<xacro:property name="base_width" value="0.32"/>
<xacro:property name="base_height" value="0.12"/>
<xacro:property name="wheel_radius" value="0.055"/>
<xacro:property name="wheel_width" value="0.035"/>
<xacro:property name="wheel_x" value="0.0"/>
<xacro:property name="wheel_y" value="${base_width / 2.0 + wheel_width / 2.0}"/>
<xacro:property name="wheel_z" value="${-base_height / 2.0 + wheel_radius}"/>
<material name="body_blue">
<color rgba="0.1 0.2 0.9 1.0"/>
</material>
<material name="wheel_black">
<color rgba="0.02 0.02 0.02 1.0"/>
</material>
<link name="base_link">
<visual>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
<material name="body_blue"/>
</visual>
<collision>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
</collision>
</link>
<xacro:macro name="wheel" params="prefix x y z">
<link name="${prefix}_wheel_link">
<visual>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
<material name="wheel_black"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
<geometry>
<cylinder radius="${wheel_radius}" length="${wheel_width}"/>
</geometry>
</collision>
</link>
<joint name="${prefix}_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="${prefix}_wheel_link"/>
<origin xyz="${x} ${y} ${z}" rpy="0 0 0"/>
<axis xyz="0 1 0"/>
</joint>
</xacro:macro>
<xacro:wheel prefix="left" x="${wheel_x}" y="${wheel_y}" z="${wheel_z}"/>
<xacro:wheel prefix="right" x="${wheel_x}" y="${-wheel_y}" z="${wheel_z}"/>
</robot>이 파일은 아직 완벽한 물리 모델은 아니다. 관성 값도 단순화되어 있고, 실제 모터나 제어 플러그인도 없다. 하지만 구조는 이미 좋아졌다. 바퀴 반지름을 바꾸면 두 바퀴가 함께 바뀐다. 바퀴 폭을 바꾸면 좌우 위치도 계산식에 따라 함께 조정된다. 2WD 로봇 모델의 기준이 한곳에 모인 것이다.
여기서 axis xyz="0 1 0"도 주의 깊게 봐야 한다. 바퀴 원기둥을 rpy="${pi/2} 0 0"로 눕혔기 때문에 회전축을 어느 방향으로 둘지 모델 좌표계와 함께 확인해야 한다. 바퀴가 RViz에서 보기에는 맞는데 시뮬레이션에서 이상하게 돈다면 조인트 축과 원기둥 방향을 함께 점검해야 한다.
캐스터와 센서 마운트도 매크로가 된다
바퀴만 매크로로 만들 수 있는 것은 아니다. 캐스터도 구조가 반복된다. 앞 캐스터와 뒤 캐스터는 이름과 위치만 다를 뿐, 작은 구 또는 바퀴 모양으로 표현되는 경우가 많다.
<xacro:property name="caster_radius" value="0.025"/>
<xacro:property name="caster_x" value="0.17"/>
<xacro:property name="caster_z" value="${-base_height / 2.0}"/>
<xacro:macro name="caster" params="prefix x z">
<link name="${prefix}_caster_link">
<visual>
<geometry>
<sphere radius="${caster_radius}"/>
</geometry>
<material name="wheel_black"/>
</visual>
<collision>
<geometry>
<sphere radius="${caster_radius}"/>
</geometry>
</collision>
</link>
<joint name="${prefix}_caster_joint" type="fixed">
<parent link="base_link"/>
<child link="${prefix}_caster_link"/>
<origin xyz="${x} 0 ${z}" rpy="0 0 0"/>
</joint>
</xacro:macro>
<xacro:caster prefix="front" x="${caster_x}" z="${caster_z}"/>
<xacro:caster prefix="rear" x="${-caster_x}" z="${caster_z}"/>센서 마운트도 마찬가지다. LiDAR를 차체 위에 올릴 때, 단순히 lidar_link만 만드는 것이 아니라 lidar_mount_link를 따로 두는 편이 좋다. 실제 로봇에서는 센서가 허공에 붙어 있지 않다. 브래킷이나 지지대가 있고, 그 높이와 위치가 센서 원점에 영향을 준다.
<xacro:property name="lidar_height" value="0.18"/>
<xacro:property name="lidar_radius" value="0.04"/>
<xacro:property name="lidar_length" value="0.035"/>
<link name="lidar_link">
<visual>
<geometry>
<cylinder radius="${lidar_radius}" length="${lidar_length}"/>
</geometry>
<material name="sensor_gray"/>
</visual>
<collision>
<geometry>
<cylinder radius="${lidar_radius}" length="${lidar_length}"/>
</geometry>
</collision>
</link>
<joint name="base_to_lidar" type="fixed">
<parent link="base_link"/>
<child link="lidar_link"/>
<origin xyz="0.08 0 ${lidar_height}" rpy="0 0 0"/>
</joint>여기서 프레임 이름은 매우 중요하다. 나중에 SLAM에서 /scan 메시지의 frame_id가 lidar_link인데 TF 트리에 lidar_link가 없으면 RViz는 레이저 데이터를 올바르게 표시하지 못한다. 반대로 URDF에는 laser_link라고 되어 있는데 센서 드라이버는 lidar_link라고 발행하면 둘은 서로 다른 프레임이 된다. 로봇 입장에서는 눈이 머리에 붙어 있는데, 소프트웨어는 눈이 책상 위에 있다고 믿는 셈이다.
파일을 나누면 모델이 프로젝트가 된다
작은 예제에서는 모든 내용을 한 파일에 넣어도 된다. 하지만 로봇 모델이 커지면 xacro 파일도 역할별로 나누는 편이 좋다. 실제 산업용 로봇 설명 패키지나 드라이버 문서에서도 상위 xacro 파일과 매크로 정의 파일을 분리하는 관례를 사용한다. 예를 들어 *.urdf.xacro는 최종 로봇 설명을 생성하는 상위 파일로 두고, *_urdf_macro.xacro는 URDF 조각을 생성하는 매크로 정의 파일로 두는 식이다.2
이동 로봇 패키지라면 다음처럼 구성할 수 있다.
my_robot_description/
├── urdf/
│ ├── my_robot.urdf.xacro
│ ├── common_properties.xacro
│ ├── materials.xacro
│ ├── mobile_base_urdf_macro.xacro
│ ├── wheel_urdf_macro.xacro
│ └── sensor_urdf_macro.xacro
├── meshes/
├── rviz/
└── launch/
└── display.launch.py상위 파일인 my_robot.urdf.xacro는 전체 조립 순서를 보여 주는 역할을 한다.
<?xml version="1.0"?>
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find my_robot_description)/urdf/common_properties.xacro"/>
<xacro:include filename="$(find my_robot_description)/urdf/materials.xacro"/>
<xacro:include filename="$(find my_robot_description)/urdf/mobile_base_urdf_macro.xacro"/>
<xacro:include filename="$(find my_robot_description)/urdf/sensor_urdf_macro.xacro"/>
<xacro:mobile_base parent="base_link"/>
<xacro:lidar_sensor parent="base_link" name="lidar" xyz="0.08 0 ${lidar_height}"/>
</robot>이 파일을 열었을 때 독자는 "아, 공통 속성을 읽고, 재질을 읽고, 모바일 베이스를 만들고, 센서를 붙이는구나"라고 전체 구조를 바로 파악할 수 있다. 세부 구현은 매크로 파일 안에 숨겨 둔다.
이 패턴은 프로그래밍에서 함수를 나누는 것과 비슷하다. 모든 코드를 main() 함수 하나에 넣을 수도 있지만, 시간이 지나면 유지보수가 어려워진다. 로봇 설명 파일도 마찬가지다. 로봇이 커질수록 "어디에 무엇이 있는지"가 중요해진다.
include는 생성이 아니라 가져오기다
xacro:include를 사용할 때 자주 하는 착각이 있다. 매크로 파일을 include하면 그 안의 로봇 부품이 자동으로 생성된다고 생각하는 것이다. 하지만 include는 매크로 정의를 가져올 뿐이다. 실제 링크와 조인트를 만들려면 매크로를 호출해야 한다.
예를 들어 wheel_urdf_macro.xacro에 다음 매크로가 있다고 하자.
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="wheel" params="prefix parent x y z">
<!-- wheel link and joint -->
</xacro:macro>
</robot>상위 파일에서 이 파일을 include하는 것만으로는 바퀴가 생기지 않는다.
<xacro:include filename="$(find my_robot_description)/urdf/wheel_urdf_macro.xacro"/>실제로 생성하려면 다음처럼 호출해야 한다.
<xacro:wheel prefix="left" parent="base_link" x="0" y="0.18" z="0"/>
<xacro:wheel prefix="right" parent="base_link" x="0" y="-0.18" z="0"/>산업용 로봇 workcell 예제에서도 이 패턴은 자주 등장한다. 별도 파일에서 로봇 매크로를 include한 뒤, 상위 xacro 파일에서 필요한 인자와 원점 정보를 넘겨 실제 로봇 인스턴스를 생성하는 방식이다.3
이 차이를 이해하면 xacro 파일 분리가 훨씬 명확해진다. include는 "부품 카탈로그를 가져오는 일"이고, 매크로 호출은 "카탈로그에서 부품을 꺼내 로봇에 장착하는 일"이다.
2WD, 4WD, 메카넘을 같은 차체에서 파생하기
xacro의 진짜 힘은 로봇 변형을 만들 때 드러난다. 같은 차체를 쓰지만 구동 방식이 달라질 수 있다.
처음에는 2WD 로봇으로 시작한다. 좌우 바퀴 두 개와 앞뒤 캐스터가 있다. 나중에 더 안정적인 주행을 위해 4WD로 바꾼다. 다시 실내 좁은 공간에서 옆으로 이동하는 기능이 필요해져 메카넘 바퀴를 고려한다. 순수 URDF라면 세 모델을 각각 복사해서 수정하기 쉽다. 그러면 세 파일은 조금씩 달라지고, 어느 순간 어떤 파일이 최신인지 알 수 없게 된다.
xacro에서는 공통 차체는 유지하고 구동부만 바꿀 수 있다.
<xacro:arg name="drive_type" default="2wd"/>
<xacro:mobile_base_body/>
<xacro:if value="$(arg drive_type) == '2wd'">
<xacro:drive_2wd/>
</xacro:if>
<xacro:if value="$(arg drive_type) == '4wd'">
<xacro:drive_4wd/>
</xacro:if>
<xacro:if value="$(arg drive_type) == 'mecanum'">
<xacro:mecanum_drive/>
</xacro:if>위 코드는 개념을 보여 주기 위한 예다. 실제 프로젝트에서는 xacro 버전과 표현식 처리 방식에 맞춰 조건식을 점검해야 한다. 중요한 것은 구조다. 차체, 센서, 재질, 기본 프레임은 공통으로 두고, 구동부만 매크로로 교체한다.
4WD 매크로는 바퀴 네 개를 만든다.
<xacro:macro name="drive_4wd" params="">
<xacro:wheel prefix="front_left" x="${wheelbase/2}" y="${track_width/2}" z="${wheel_z}"/>
<xacro:wheel prefix="front_right" x="${wheelbase/2}" y="${-track_width/2}" z="${wheel_z}"/>
<xacro:wheel prefix="rear_left" x="${-wheelbase/2}" y="${track_width/2}" z="${wheel_z}"/>
<xacro:wheel prefix="rear_right" x="${-wheelbase/2}" y="${-track_width/2}" z="${wheel_z}"/>
</xacro:macro>메카넘 구동도 같은 방식으로 표현할 수 있다. 바퀴의 시각 형상은 일반 원기둥으로 단순화할 수도 있고, 메쉬 파일을 사용해 롤러가 있는 모양을 표현할 수도 있다. 중요한 것은 조인트 이름, 링크 이름, 위치, 회전 방향을 일관되게 관리하는 것이다. 구동 제어기나 시뮬레이션 플러그인은 이 이름을 기준으로 바퀴를 찾기 때문이다.
이 방식의 장점은 실험 속도다. 같은 로봇 패키지에서 drive_type:=2wd, drive_type:=4wd처럼 인자를 바꿔 여러 모델을 생성할 수 있다. 자율주행 로봇 개발에서는 하드웨어 구성이 자주 바뀐다. xacro는 그 변화를 URDF 복사본의 늪이 아니라 매개변수의 변화로 다루게 해 준다.
센서를 선택적으로 붙이기
자율주행 로봇은 센서 구성에 따라 성격이 달라진다. 2D LiDAR만 있는 로봇은 실내 SLAM과 Nav2에 적합하다. RGB-D 카메라가 있으면 깊이 정보를 활용할 수 있다. IMU가 있으면 자세 변화와 회전 정보를 보완할 수 있다.
그런데 모든 실험에서 모든 센서를 쓰지는 않는다. 처음에는 LiDAR만 사용하고, 나중에 카메라를 추가할 수 있다. 실제 로봇에는 IMU가 있지만 시뮬레이션에서는 잠시 제외하고 싶을 수도 있다. 이때 센서 링크를 주석 처리했다가 다시 풀기 시작하면 실수가 생긴다.
xacro 인자를 사용하면 센서를 선택적으로 추가할 수 있다.
<xacro:arg name="use_lidar" default="true"/>
<xacro:arg name="use_imu" default="true"/>
<xacro:arg name="use_rgbd_camera" default="false"/>
<xacro:if value="$(arg use_lidar)">
<xacro:lidar_sensor parent="base_link" name="lidar" xyz="0.08 0 0.18"/>
</xacro:if>
<xacro:if value="$(arg use_imu)">
<xacro:imu_sensor parent="base_link" name="imu" xyz="0 0 0.08"/>
</xacro:if>
<xacro:if value="$(arg use_rgbd_camera)">
<xacro:rgbd_camera parent="base_link" name="camera" xyz="0.20 0 0.16"/>
</xacro:if>센서 매크로는 링크와 조인트를 함께 만들어야 한다. 예를 들어 LiDAR 매크로는 lidar_link를 만들고, base_link에서 lidar_link로 가는 fixed joint를 만든다.
<xacro:macro name="lidar_sensor" params="parent name xyz">
<link name="${name}_link">
<visual>
<geometry>
<cylinder radius="0.04" length="0.035"/>
</geometry>
<material name="sensor_gray"/>
</visual>
<collision>
<geometry>
<cylinder radius="0.04" length="0.035"/>
</geometry>
</collision>
</link>
<joint name="${parent}_to_${name}" type="fixed">
<parent link="${parent}"/>
<child link="${name}_link"/>
<origin xyz="${xyz}" rpy="0 0 0"/>
</joint>
</xacro:macro>프레임 이름은 단순히 보기 좋은 이름이 아니다. 센서 드라이버가 발행하는 메시지의 frame_id, RViz의 Fixed Frame, TF 트리, SLAM 노드의 파라미터가 모두 이 이름과 연결된다. 따라서 lidar_link, imu_link, camera_link, camera_color_optical_frame 같은 이름을 프로젝트 전체에서 일관되게 사용해야 한다.
나중에 SLAM을 실행할 때 /scan이 lidar_link 기준으로 발행된다면 URDF에도 lidar_link가 있어야 한다. RGB-D 카메라는 일반적으로 카메라 본체 프레임과 optical frame을 구분한다. optical frame은 이미지 좌표계와 관련된 프레임이므로 방향이 일반적인 로봇 좌표계와 다를 수 있다. 이 부분은 카메라 드라이버 문서와 함께 맞춰야 한다.
inertial도 매크로로 다룰 수 있다
RViz에서 보기만 할 모델이라면 visual과 collision만 있어도 어느 정도 확인할 수 있다. 하지만 시뮬레이션에서 로봇을 움직이려면 inertial이 중요하다. inertial은 질량과 관성 정보를 담는다. 관성은 물체가 회전 운동을 얼마나 버티는지를 나타내는 값이다.
처음에는 관성 값을 정교하게 계산하지 않더라도, 링크마다 최소한의 질량과 관성 정보를 넣어 두는 습관이 좋다. xacro 매크로를 사용하면 반복을 줄일 수 있다.
<xacro:macro name="simple_inertial" params="mass ixx iyy izz">
<inertial>
<origin xyz="0 0 0" rpy="0 0 0"/>
<mass value="${mass}"/>
<inertia ixx="${ixx}" ixy="0.0" ixz="0.0"
iyy="${iyy}" iyz="0.0"
izz="${izz}"/>
</inertial>
</xacro:macro>그리고 링크 안에서 호출한다.
<link name="base_link">
<visual>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
<material name="body_blue"/>
</visual>
<collision>
<geometry>
<box size="${base_length} ${base_width} ${base_height}"/>
</geometry>
</collision>
<xacro:simple_inertial mass="4.0" ixx="0.03" iyy="0.08" izz="0.09"/>
</link>나중에는 박스, 원기둥, 구에 대한 관성 공식을 xacro 수식으로 넣어 더 정확하게 만들 수도 있다. 다만 처음부터 지나치게 복잡한 관성 매크로에 매달릴 필요는 없다. 학습 단계에서는 "각 링크에 질량과 관성이 있으며, 시뮬레이션 물리에 영향을 준다"는 감각을 먼저 잡는 것이 좋다.
launch에서 xacro를 robot_state_publisher에 넘기기
로봇 모델은 결국 robot_description 파라미터로 robot_state_publisher에 전달된다. robot_state_publisher는 URDF의 링크와 조인트 구조를 읽고 TF를 발행한다. 고정 조인트는 정적인 TF로, 움직이는 조인트는 /joint_states 값을 반영해 동적인 TF로 이어진다.
xacro 파일을 launch에서 바로 처리하는 예는 다음과 같다.
from launch import LaunchDescription
from launch.substitutions import Command, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
xacro_file = PathJoinSubstitution([
FindPackageShare('my_robot_description'),
'urdf',
'my_robot.urdf.xacro'
])
robot_description = ParameterValue(
Command(['xacro ', xacro_file]),
value_type=str
)
return LaunchDescription([
Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_description}],
output='screen'
)
])이 방식은 모델 파일을 수정하면서 RViz로 확인할 때 편리하다. 특히 colcon build --symlink-install을 사용하면 설치 공간에 심볼릭 링크가 걸려, 파일 수정 후 재빌드 부담이 줄어든다. 다만 패키지의 setup.py 또는 CMakeLists.txt에서 urdf, meshes, launch, rviz 파일이 설치되도록 설정해야 launch가 해당 파일을 찾을 수 있다.
Python launch에서 xacro를 처리하는 방법은 여러 가지가 있다. Command(['xacro ', path])처럼 launch substitution을 사용할 수도 있고, Python 코드에서 xacro.process_file()을 호출해 XML 문자열을 만든 뒤 넘길 수도 있다. 두 방식 모두 핵심은 같다. 최종적으로 robot_description에는 xacro 자체가 아니라 xacro가 확장된 URDF 문자열이 들어간다.
xacro를 URDF로 펼쳐 보기
xacro를 쓰면 파일이 짧아진다. 하지만 디버깅할 때는 "최종 URDF가 실제로 어떻게 생성되었는지"를 봐야 한다. 매크로가 기대한 대로 확장되었는지 확인하지 않으면 엉뚱한 이름의 링크가 생기거나, 조인트가 빠진 채로 RViz를 열게 된다.
먼저 xacro를 수동으로 변환해 보자.
ros2 run xacro xacro \
src/my_robot_description/urdf/my_robot.urdf.xacro \
-o /tmp/my_robot.urdf또는 다음처럼 표준 출력으로 바로 확인할 수 있다.
ros2 run xacro xacro src/my_robot_description/urdf/my_robot.urdf.xacro출력된 URDF에서 확인할 것은 복잡하지 않다. 링크 이름이 중복되지 않는지, 조인트의 parent와 child가 올바른지, 센서 프레임 이름이 의도한 대로 나왔는지 보면 된다.
특히 매크로에서 prefix를 빼먹으면 링크 이름이 중복될 수 있다. URDF에서는 같은 이름의 링크를 두 개 만들 수 없다. 예를 들어 네 바퀴가 모두 wheel_link라는 이름으로 생성되면 로봇 모델은 제대로 파싱되지 않는다. 그래서 매크로에는 prefix나 name 매개변수를 두는 습관이 중요하다.
check_urdf로 구조 확인하기
URDF로 변환한 뒤에는 check_urdf로 기본 구조를 검사할 수 있다.
check_urdf /tmp/my_robot.urdf환경에 따라 urdfdom 관련 도구가 설치되어 있어야 한다. 이 명령은 URDF 문법 오류, 링크와 조인트 연결 구조, 루트 링크 등을 확인하는 데 도움이 된다. XML 태그를 잘못 닫았거나, 존재하지 않는 링크를 child로 지정했을 때 빠르게 발견할 수 있다.
검증은 단계적으로 하는 편이 좋다.
xacro가 URDF로 변환되는지 확인한다.
check_urdf로 URDF 구조를 확인한다.robot_state_publisher를 실행해/robot_description과 TF가 나오는지 확인한다.RViz에서 RobotModel과 TF를 표시해 링크 위치와 방향을 눈으로 확인한다.
움직이는 조인트가 있다면
/joint_states와 함께 조인트 움직임을 확인한다.
이 순서를 지키면 문제를 좁히기 쉽다. RViz에서 로봇이 보이지 않을 때 바로 "RViz 설정 문제인가?"라고 생각하기보다, xacro 변환부터 차근차근 확인하는 것이다.
RViz에서 TF와 모델을 함께 보기
로봇 모델을 확인할 때 RViz에서는 RobotModel과 TF 표시를 함께 켜는 것이 좋다. RobotModel은 로봇의 시각 형상을 보여 주고, TF는 각 링크 프레임의 위치와 방향을 보여 준다.
고정 프레임은 상황에 따라 다르다. 아직 map이나 odom이 없다면 base_link 또는 base_footprint를 Fixed Frame으로 두고 확인한다. 이동 로봇에서는 base_footprint를 바닥에 투영한 기준 프레임으로 쓰고, base_link를 차체 중심 프레임으로 쓰는 경우가 많다. 프로젝트에서 어떤 기준을 사용할지 정했다면 URDF, TF, Nav2 설정에서 같은 이름을 써야 한다.
RViz에서 확인할 때는 다음 장면을 상상하듯 보면 좋다. 로봇을 위에서 내려다봤을 때 x축은 앞을 향하는가? y축은 왼쪽을 향하는가? z축은 위를 향하는가? LiDAR는 실제로 차체 위에 있는가? 카메라가 뒤를 보고 있지는 않은가? 바퀴 조인트 축은 바퀴가 굴러가야 할 방향과 맞는가?
이 질문들은 단순한 시각 확인이 아니다. 나중에 /cmd_vel을 보냈는데 로봇이 옆으로 움직이거나, LiDAR 스캔이 지도에서 90도 돌아가 보이거나, Nav2가 장애물을 엉뚱한 위치에 표시하는 문제를 미리 막아 준다.
자주 만나는 오류들
xacro와 URDF를 다룰 때 자주 만나는 오류는 대체로 비슷하다. 좋은 소식은 한 번 원인을 이해하면 다음부터는 빨리 찾을 수 있다는 점이다.
가장 흔한 오류는 XML 문법 오류다. 태그를 열고 닫지 않았거나, 따옴표가 빠졌거나, <origin> 태그가 잘못된 위치에 들어간 경우다. xacro 변환 단계에서 바로 에러가 난다면 먼저 XML 문법을 의심한다.
두 번째는 링크 이름 중복이다. 매크로를 여러 번 호출했는데 내부 링크 이름에 ${prefix}를 넣지 않으면 같은 이름의 링크가 반복 생성된다. 바퀴 매크로, 센서 매크로, 카메라 매크로에서 특히 조심해야 한다.
세 번째는 끊어진 TF 트리다. 어떤 링크가 만들어졌지만 부모 조인트가 없거나, 조인트의 parent 이름이 실제 링크 이름과 다르면 트리가 이어지지 않는다. URDF는 기본적으로 하나의 연결된 트리 구조여야 한다. 로봇 부품 하나가 우주 미아처럼 따로 떠 있으면 RViz에서 제대로 표시되지 않는다.
네 번째는 프레임 이름 불일치다. URDF에는 lidar_link가 있는데 센서 메시지에는 laser_frame이 들어가는 식이다. 이 경우 모델 자체는 보이지만 LaserScan이 표시되지 않거나 TF 오류가 난다. 센서 드라이버 설정과 URDF 이름을 함께 맞춰야 한다.
다섯 번째는 축 방향 오류다. 바퀴가 굴러야 하는 축과 조인트 axis가 다르면 시뮬레이션에서 바퀴가 이상하게 돈다. 카메라 optical frame 방향이 잘못되면 이미지 기반 처리에서 좌표가 뒤틀린다. 눈으로 보기에는 작은 화살표 하나지만, 로봇에게는 세상을 해석하는 기준이다.
xacro를 잘 쓰는 기준
xacro는 강력하지만, 모든 것을 매크로로 만들 필요는 없다. 너무 과한 추상화는 오히려 모델을 읽기 어렵게 만든다. 처음 보는 사람이 파일을 열었을 때 로봇 구조를 따라갈 수 있어야 한다.
좋은 기준은 단순하다. 같은 구조가 두 번 이상 반복되고, 앞으로도 바뀔 가능성이 있다면 매크로를 고려한다. 한 번만 등장하는 특별한 부품이라면 그냥 URDF로 직접 써도 된다. 모든 숫자를 property로 빼야 하는 것도 아니다. 로봇의 핵심 치수, 여러 곳에서 쓰는 값, 실험 중 자주 바꿀 값부터 property로 분리하면 충분하다.
특히 이동 로봇에서는 다음 값들을 우선적으로 property로 관리하는 것이 좋다.
차체 길이, 폭, 높이, 질량
바퀴 반지름, 바퀴 폭, 좌우 바퀴 간 거리
휠베이스, 캐스터 위치, 센서 높이
LiDAR, IMU, 카메라의 프레임 이름과 장착 위치
매크로는 다음 요소에 잘 어울린다.
좌우 바퀴, 4WD 바퀴, 메카넘 바퀴
캐스터, 범퍼, 반복되는 센서 마운트
LiDAR, IMU, 카메라처럼 선택적으로 붙이는 센서 블록
파일 분리는 프로젝트가 커질 때 시작해도 된다. 처음부터 너무 많은 파일로 나누면 학습이 어려워질 수 있다. 하지만 로봇 모델이 한 화면을 훌쩍 넘어가고, 바퀴와 센서와 Gazebo 설정이 뒤섞이기 시작하면 그때가 분리할 시점이다.
이번 장의 로봇 모델이 다음 장으로 이어지는 방식
이번 장에서 xacro로 한 일은 단순한 정리 작업이 아니다. 우리는 로봇 모델을 변경 가능한 시스템으로 만들었다. 바퀴 반지름을 바꾸면 바퀴 위치가 따라 바뀌고, 센서 높이를 바꾸면 TF 트리의 센서 프레임이 함께 조정된다. 2WD에서 4WD로 바꾸거나, LiDAR만 쓰던 로봇에 RGB-D 카메라를 추가할 수도 있다.
이제 로봇 설명 파일은 "한 번 만들고 잊는 XML"이 아니다. 로봇의 하드웨어 구성, 센서 배치, 시뮬레이션 조건을 담는 중심 문서가 된다. 이 중심이 단단해야 RViz에서 보는 모델도 정확하고, Gazebo에서 움직이는 로봇도 안정적이며, SLAM과 Nav2가 사용하는 TF도 일관된다.
다음 장에서는 RViz를 본격적으로 사용해 로봇 모델, TF, 센서 데이터를 눈으로 확인한다. xacro로 만든 모델이 정말 우리가 의도한 모습인지, 프레임이 올바르게 이어졌는지, 센서 데이터가 로봇 위에 정확히 얹히는지 살펴볼 것이다. 이제 로봇은 XML 속에만 있지 않다. 화면 위에서, 좌표계 위에서, 데이터와 함께 살아 움직이기 시작한다.
참고
1Using Xacro to clean up your code
2Extending a Robot Model — FANUC ROS 2 Driver Documentation
3Assembling the URDF — Universal Robots ROS 2 Driver Documentation
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.