# ROS2 tf2 예제 패키지 빌드 및 실행

#### 예제 패키지 준비

ROS2에서 tf2를 학습할 때 가장 먼저 확인해야 할 것은 실제 예제 패키지가 어떻게 구성되어 있는지이다. 예제 패키지에서는 다음과 같은 주요 파일 및 폴더가 존재한다고 가정하자.

* C++ 또는 Python 노드 소스 코드 (예: `src/tf2_example.cpp`)
* 패키지 설정 파일 (`package.xml`, `CMakeLists.txt` 등)
* 실행 또는 테스트를 위한 Launch 파일 (예: `launch/tf2_example.launch.py`)

일반적으로 ROS2 예제 패키지를 실행하려면 다음 순서를 따른다.

1. 예제 패키지를 ROS2 워크스페이스의 `src` 폴더에 복사(혹은 클론)한다.
2. 워크스페이스 전체를 빌드한다.
3. 빌드 후 실행 가능한 노드를 확인한다.
4. 필요한 노드를 실행하거나 Launch 파일을 통해 tf2 관련 노드들을 한꺼번에 구동한다.

예제 패키지를 로컬 환경에 복제 혹은 복사했으면, 아래와 같이 워크스페이스를 구성했다고 가정해 보자.

```
ros2_ws/
├── src/
│   ├── tf2_example_pkg/
│   ├── ...
└── ...
```

위와 같은 구조에서 `tf2_example_pkg`가 tf2 실습에 사용할 예제 패키지라고 할 때, 다음 단계에 따라 빌드를 진행한다.

#### 워크스페이스 빌드

ROS2 Humble 버전에서 colcon 툴을 사용해 워크스페이스를 빌드한다. 아래와 같은 명령을 입력한다.

```
cd ~/ros2_ws
colcon build --packages-select tf2_example_pkg
```

`--packages-select` 옵션을 통해 특정 패키지만 빌드할 수도 있고, 생략하면 워크스페이스 내 모든 패키지를 빌드한다. 빌드가 정상적으로 완료되면 `~/ros2_ws/install/tf2_example_pkg/` 경로 아래에 실행 파일과 라이브러리, 설정 파일 등이 설치된다.

빌드가 문제 없이 끝났다면, `~/ros2_ws/install/tf2_example_pkg/lib/tf2_example_pkg/` 경로에서 실제 실행 파일을 찾을 수 있다. 예컨대 `tf2_broadcaster`나 `tf2_listener` 등의 실행 파일을 확인할 수 있다.

#### tf2 노드 실행

예제 패키지 내에서 프레임 간의 변환을 브로드캐스팅하는 노드(예: `tf2_broadcaster`)와 브로드캐스트된 프레임을 받아서 확인하는 노드(예: `tf2_listener`)가 따로 존재할 수 있다. 혹은 둘 다의 역할을 하나의 노드가 수행하도록 구현될 수도 있다.

일반적인 시나리오는 다음과 같다.

1. **프레임 브로드캐스트 노드(tf2\_broadcaster)** 실행
2. **tf2 프레임 에코**를 통해 브로드캐스트 결과 확인
3. **tf2\_listener** 등을 통해 실제로 Transform 데이터를 수신하여 활용하거나, RViz2에서 시각화

이 과정을 한 번에 실행하기 위해 Launch 파일이 제공될 수도 있다. 예를 들어 `tf2_example.launch.py` 라는 파일이 있다면, 다음 명령으로 실행할 수 있다.

```
source ~/ros2_ws/install/setup.bash
ros2 launch tf2_example_pkg tf2_example.launch.py
```

이렇게 Launch 파일을 호출하면 내부적으로 `tf2_broadcaster`와 기타 필요한 노드들이 동시에 실행되고, 노드들 간의 토픽 연결이 자동으로 이루어진다.

#### tf2 동작 확인

tf2 브로드캐스트가 제대로 이루어지고 있는지 확인하려면 `tf2_echo`라는 명령을 사용할 수 있다. 이는 `tf2_ros` 패키지에서 제공되는 툴로, 특정 두 프레임 간의 상대 변환(Transform)을 콘솔 창에 출력한다.

예를 들어, 브로드캐스트된 프레임 중 `world`와 `robot_base` 라는 프레임이 있다고 하자. 이를 확인하려면 다음과 같이 실행한다.

```
ros2 run tf2_ros tf2_echo world robot_base
```

만약 브로드캐스트가 정상적으로 진행되고 있다면, 콘솔에 아래와 같이 각 Transform 정보를 계속 표시한다.

```
At time 12.3456
- Translation: x=0.0 y=0.0 z=0.0
- Rotation: x=0.0 y=0.0 z=0.0 w=1.0
...
```

이외에도 `rviz2`를 이용해 TF 플러그인을 활성화하여 실제 프레임 구조를 3D 공간에서 시각화할 수 있다. RViz2 상에서 `tf` 또는 `TF` 디스플레이를 추가하고, 브로드캐스트 중인 프레임들이 잘 연결되어 있는지 확인한다.

#### 브로드캐스트 노드 구성

tf2를 사용하려면 먼저 특정 프레임(좌표계)을 브로드캐스팅하는 노드를 구성해야 한다. 노드는 대체로 다음의 구조로 되어 있다.

1. **rclcpp::Node**를 상속하거나 생성하여 ROS2 노드 객체 생성
2. **tf2\_ros::TransformBroadcaster** 또는 **tf2\_ros::StaticTransformBroadcaster**를 사용하여 Transform 정보 전송
3. 주기적으로(예: 타이머 콜백) 또는 한 번만(Static Transform) Transform 메시지를 생성하여 브로드캐스트

아래는 C++로 구성된 동적 브로드캐스터(`tf2_broadcaster.cpp`) 예시 구조다.

```cpp
#include <memory>
#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

using namespace std::chrono_literals;

class Tf2BroadcasterNode : public rclcpp::Node
{
public:
  Tf2BroadcasterNode()
  : Node("tf2_broadcaster_node")
  {
    // TransformBroadcaster 생성
    tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(this);

    // 타이머 설정 (0.1초 주기로 콜백)
    timer_ = this->create_wall_timer(
      100ms, std::bind(&Tf2BroadcasterNode::broadcastTransform, this));
  }

private:
  void broadcastTransform()
  {
    geometry_msgs::msg::TransformStamped transform_stamped;

    // 현재 시각 설정
    transform_stamped.header.stamp = this->now();
    // 브로드캐스트할 부모 프레임명 (예: "world")
    transform_stamped.header.frame_id = "world";
    // 브로드캐스트할 자식 프레임명 (예: "robot_base")
    transform_stamped.child_frame_id = "robot_base";

    // 변환(Translation, Rotation) 설정
    transform_stamped.transform.translation.x = 1.0;
    transform_stamped.transform.translation.y = 2.0;
    transform_stamped.transform.translation.z = 0.0;
    transform_stamped.transform.rotation.x = 0.0;
    transform_stamped.transform.rotation.y = 0.0;
    transform_stamped.transform.rotation.z = 0.0;
    transform_stamped.transform.rotation.w = 1.0;

    // 브로드캐스트
    tf_broadcaster_->sendTransform(transform_stamped);
  }

  rclcpp::TimerBase::SharedPtr timer_;
  std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
};

int main(int argc, char* argv[])
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<Tf2BroadcasterNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
```

동적 브로드캐스터는 지속적으로, 혹은 주기적으로 변환 정보를 갱신하여 퍼블리시한다. 위 예제처럼 `tf2_ros::TransformBroadcaster`를 활용하면 된다.

#### 정적 브로드캐스트

만약 한 번 설정한 변환이 거의 변하지 않는다면, **정적 브로드캐스트**를 사용하는 편이 효율적이다. ROS2에서는 `tf2_ros::StaticTransformBroadcaster`를 제공한다. 정적 브로드캐스트는 노드를 시작하자마자 단 한 번만 Transform 정보를 퍼블리시하고, 노드가 종료되거나 파괴될 때까지 해당 Transform을 계속해서 tf 트리 내에서 볼 수 있다. 간단한 예시는 다음과 같다.

```cpp
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/static_transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

class StaticTfBroadcasterNode : public rclcpp::Node
{
public:
  StaticTfBroadcasterNode()
  : Node("static_tf_broadcaster_node")
  {
    static_broadcaster_ = std::make_unique<tf2_ros::StaticTransformBroadcaster>(this);

    geometry_msgs::msg::TransformStamped transform_stamped;
    transform_stamped.header.stamp = this->now();
    transform_stamped.header.frame_id = "world";
    transform_stamped.child_frame_id = "camera_link";
    transform_stamped.transform.translation.x = 0.5;
    transform_stamped.transform.translation.y = 0.0;
    transform_stamped.transform.translation.z = 1.0;
    transform_stamped.transform.rotation.x = 0.0;
    transform_stamped.transform.rotation.y = 0.0;
    transform_stamped.transform.rotation.z = 0.0;
    transform_stamped.transform.rotation.w = 1.0;

    static_broadcaster_->sendTransform(transform_stamped);
  }

private:
  std::unique_ptr<tf2_ros::StaticTransformBroadcaster> static_broadcaster_;
};

int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<StaticTfBroadcasterNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
```

이렇게 구현하면, 따로 타이머를 두지 않아도 정적으로 “world→camera\_link” 라는 변환이 퍼블리시된다.

#### 수식으로 본 Transform 구조

tf2에서 사용되는 변환은 기본적으로 회전 변환과 병진 변환으로 구성되며, 이를 4×4 동차변환행렬로 표현할 수 있다. 예를 들어, 어떤 좌표계 A에서 좌표계 B로의 변환행렬이 $^{A}\mathbf{T}\_{B}$ 라고 할 때,

$$
^{A}\mathbf{T}\_{B} =  \begin{bmatrix} \mathbf{R} & \mathbf{p} \ \mathbf{0}^\mathsf{T} & 1 \end{bmatrix}
$$

여기서 $\mathbf{R}$은 3×3 회전행렬, $\mathbf{p}$는 3×1 병진벡터이다. ROS2의 `geometry_msgs/msg/Transform` 메시지로 변환될 때는 아래처럼 나뉘어 전달된다.

* $\mathbf{p}$ : `Transform.translation.x`, `Transform.translation.y`, `Transform.translation.z`
* $\mathbf{R}$ : 쿼터니언 형태(`Transform.rotation.x, y, z, w`)로 전달

따라서 브로드캐스터 노드에서는 이 변환행렬에 해당하는 병진과 회전(쿼터니언)을 적절히 설정하여 `sendTransform()` 함수에 넘겨주면 된다.

#### 실행 스크립트 및 명령어

앞서 작성한 예제 코드가 `tf2_broadcaster.cpp`로 존재하고, 패키지명이 `tf2_example_pkg`라고 하면, CMakeLists.txt 내에 대략 아래와 같은 실행 타겟을 추가해야 한다.

```cmake
add_executable(tf2_broadcaster src/tf2_broadcaster.cpp)
ament_target_dependencies(tf2_broadcaster 
  rclcpp
  tf2_ros
  geometry_msgs
)
install(TARGETS
  tf2_broadcaster
  DESTINATION lib/${PROJECT_NAME}
)
```

그 후 아래 명령으로 빌드를 진행한다.

```
cd ~/ros2_ws
colcon build --packages-select tf2_example_pkg
```

정상 빌드가 끝난 뒤, 아래와 같이 노드를 실행한다.

```
source ~/ros2_ws/install/setup.bash
ros2 run tf2_example_pkg tf2_broadcaster
```

위 노드는 0.1초 간격으로 “world→robot\_base” 변환 정보를 브로드캐스트한다. 다른 터미널에서 `tf2_echo`를 통해 실제 브로드캐스팅 정보를 모니터링해볼 수 있다.

```
ros2 run tf2_ros tf2_echo world robot_base
```

확인 결과 변환 정보가 주기적으로 출력된다면, 브로드캐스터 노드가 정상 동작하고 있는 것이다.

#### tf2 리스너 노드

브로드캐스터 노드가 다른 프레임들 간의 변환을 퍼블리시하고 있다면, 이를 받아서 실제로 변환 정보를 활용하는 리스너(Listener) 노드가 필요하다. 일반적으로 리스너 노드는 다음과 같은 절차로 구현한다.

1. **tf2\_ros::Buffer** 객체를 생성한다.
2. **tf2\_ros::TransformListener** 객체에 위 Buffer를 인자로 주어, Buffer가 자동으로 TF 정보를 수신하고 내부에 저장하도록 한다.
3. 변환이 필요한 시점에 `lookupTransform` 메서드를 통해 원하는 부모 프레임에서 자식 프레임으로의 변환을 가져온다.
4. 가져온 변환(transform)을 활용하여 좌표 변환, 혹은 로봇 동작에 필요한 계산 등을 수행한다.

아래는 C++로 작성된 리스너 예제(`tf2_listener.cpp`)의 간단한 구조다.

```cpp
#include <memory>
#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

using namespace std::chrono_literals;

class Tf2ListenerNode : public rclcpp::Node
{
public:
  Tf2ListenerNode()
  : Node("tf2_listener_node")
  {
    // tf2_ros::Buffer 및 TransformListener 생성
    tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
    tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);

    // 타이머를 이용해 주기적으로 transform 확인
    timer_ = this->create_wall_timer(
      500ms, std::bind(&Tf2ListenerNode::timerCallback, this));
  }

private:
  void timerCallback()
  {
    try {
      // world -> robot_base 변환 요청
      geometry_msgs::msg::TransformStamped transform_stamped;
      transform_stamped = tf_buffer_->lookupTransform(
        "world",      // 부모 프레임
        "robot_base", // 자식 프레임
        tf2::TimePointZero
      );

      RCLCPP_INFO(this->get_logger(), "Transform: (x=%.2f, y=%.2f, z=%.2f)",
                  transform_stamped.transform.translation.x,
                  transform_stamped.transform.translation.y,
                  transform_stamped.transform.translation.z
      );
    } catch (const tf2::TransformException &ex) {
      RCLCPP_WARN(this->get_logger(), "%s", ex.what());
    }
  }

  rclcpp::TimerBase::SharedPtr timer_;
  std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
  std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
};

int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<Tf2ListenerNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
```

이 노드는 일정 주기로 “world→robot\_base” 변환을 요청하여 가져오고, 그 위치 정보를 로그로 출력한다. 브로드캐스터 노드를 병행 실행한다면 실제 변환값이 계속 갱신되는 것을 확인할 수 있다.

#### tf2 리스너 동작 방식

리스너 노드는 내부적으로 **tf2\_ros::Buffer**에 누적된 Transform 정보를 요청 시점에 조회한다. 브로드캐스터 노드가 퍼블리시하는 `tf` 토픽(`/tf` 또는 `/tf_static`)을 **TransformListener**가 구독하여 **Buffer**에 기록해두는 방식이다.

수식으로 표현하면, 리스너가 “world→robot\_base” 변환행렬 $^{\text{world}}\mathbf{T}\_{\text{robot\_base}}$ 를 요청한다고 할 때, 이 값은 내부적으로 브로드캐스터가 퍼블리시한 Transform을 그대로 가져와 4×4 행렬 형태(혹은 쿼터니언+병진 벡터)로 관리한다.

$$
^{\text{world}}\mathbf{T}\_{\text{robot\_base}} =  \begin{bmatrix} \mathbf{R} & \mathbf{p} \ \mathbf{0}^\mathsf{T} & 1 \end{bmatrix}
$$

리스너가 특정 시점에 `lookupTransform("world", "robot\_base", t)` 를 호출하면, **Buffer**가 유지하고 있던 해당 시점($t$) 혹은 최근 시점의 변환정보를 반환한다.

#### 간단한 리스너 빌드 및 실행

앞서 작성한 리스너 코드를 `tf2_listener.cpp`라는 이름으로 저장한 뒤, `CMakeLists.txt`에 실행 타겟을 추가한다.

```cmake
add_executable(tf2_listener src/tf2_listener.cpp)
ament_target_dependencies(tf2_listener 
  rclcpp
  tf2_ros
  geometry_msgs
)
install(TARGETS
  tf2_listener
  DESTINATION lib/${PROJECT_NAME}
)
```

이후 빌드를 진행한다.

```
cd ~/ros2_ws
colcon build --packages-select tf2_example_pkg
```

빌드가 정상적으로 완료되면, 아래와 같이 리스너 노드를 실행할 수 있다.

```
source ~/ros2_ws/install/setup.bash
ros2 run tf2_example_pkg tf2_listener
```

만약 이미 브로드캐스터 노드가 “world→robot\_base”를 퍼블리시 중이라면, 리스너 노드 콘솔에 주기적으로 위치 정보가 표시될 것이다.

#### Launch 파일 활용

tf2 브로드캐스터 노드와 리스너 노드를 각각 별도의 터미널에서 실행할 수도 있지만, ROS2에서는 Launch 파일을 사용하여 한 번에 여러 노드를 띄울 수 있다. 예를 들어 `tf2_example_pkg`에 `launch/tf2_example.launch.py`라는 Launch 파일을 만들고, 다음과 같이 작성한다고 가정해 보자.

```python
import os

from launch import LaunchDescription
from launch.actions import ExecuteProcess
from launch_ros.actions import Node

def generate_launch_description():

    broadcaster_node = Node(
        package='tf2_example_pkg',
        executable='tf2_broadcaster',
        name='broadcaster_node'
    )

    listener_node = Node(
        package='tf2_example_pkg',
        executable='tf2_listener',
        name='listener_node'
    )

    return LaunchDescription([
        broadcaster_node,
        listener_node
    ])
```

위 예시에서는 `tf2_broadcaster`와 `tf2_listener` 실행 파일을 각각 `Node` 액션으로 생성하여 Launch 파일에 등록했다. 이제 다음과 같은 명령어를 통해 두 노드를 동시에 구동할 수 있다.

```
source ~/ros2_ws/install/setup.bash
ros2 launch tf2_example_pkg tf2_example.launch.py
```

이후, 콘솔 출력을 확인하면 브로드캐스터 노드에서 브로드캐스트한 변환 정보를 리스너 노드가 받아서 로깅하고 있음을 확인할 수 있다.

#### RViz2에서 프레임 시각화

tf2는 텍스트 콘솔로나 코드 레벨에서만 확인하기보다, 실제 로봇 혹은 3D 환경에서 프레임들이 어떻게 배치되어 있는지를 시각적으로 확인하는 것이 더 직관적이다. 이를 위해 RViz2를 활용한다. 다음과 같은 절차로 시각화를 진행할 수 있다.

RViz2 실행:

```
source ~/ros2_ws/install/setup.bash
ros2 run rviz2 rviz2
```

**Displays 패널에서 `TF` 추가**: RViz2 왼쪽 하단의 `Displays` 탭에서 `Add` 버튼을 누른 뒤, `TF` 디스플레이를 추가한다.

**프레임 트리 확인**: `TF` 디스플레이를 활성화하면, 브로드캐스터 노드가 퍼블리시 중인 모든 프레임이 화살표나 축(Axis) 형태로 3D 공간에 표시된다. 예시로 `world`를 기준으로 `robot_base`, `camera_link` 등이 서로 어떻게 연결되어 있는지 시각화해볼 수 있다.

만약 정적 브로드캐스터 노드에서 “world→camera\_link” 변환을 퍼블리시 중이라면, RViz2에서도 해당 축이 정적으로 위치를 유지하고 있는 것을 확인할 수 있다. 동적 브로드캐스터 노드를 통해 시간에 따라 변환이 달라진다면, RViz2에서 그 움직임을 실시간으로 확인할 수 있다.

#### Transform 트리 확장

실제 로봇 시스템에서는 하나의 로봇에 여러 센서(예: 카메라, LiDAR 등)가 달려 있고, 센서마다 고유한 좌표축을 갖는다. 또한 로봇 자체에도 조인트(링크)이 존재할 경우, 여러 조인트 좌표계가 있을 수 있다. 이런 경우 tf2 프레임 트리는 자연스럽게 확장된다. 예를 들어,

* `world` → `robot_base`
* `robot_base` → `camera_link`
* `robot_base` → `lidar_link`
* `camera_link` → `camera_optical_frame`
* … (필요한 만큼 계속)

각 브로드캐스터 노드(또는 정적/동적 TransformBroadcaster)에서 적절한 부모-자식 관계를 지정해주면, tf2는 각 좌표계 간 변환을 하나의 트리로 엮어서 관리한다. RViz2를 통해 이를 한눈에 파악할 수 있다.

#### TF 트리 검사 툴 사용

tf2를 이용해 여러 프레임을 브로드캐스팅하고 리스닝하다 보면, 때로는 “특정 프레임이 잘 연결되어 있지 않다”거나 “타임스탬프 문제로 변환을 찾지 못한다” 같은 문제가 발생할 수 있다. 이러한 문제를 파악하기 위해 ROS2에서는 여러 가지 유틸리티를 제공한다.

**1) view\_frames (tf2\_tools)**

ROS1에서 `tf` 패키지에 포함되어 있던 `view_frames` 기능은 ROS2에서도 `tf2_tools` 패키지를 통해 제공된다. 이 기능은 현재의 TF 트리를 PDF 형태로 시각화한 결과물을 만들어준다. 아래와 같이 실행할 수 있다.

```
source ~/ros2_ws/install/setup.bash
ros2 run tf2_tools view_frames
```

명령어가 정상 동작했다면, 현재 활성화된 TF 트리의 구조를 `.pdf` 파일로 저장한다(기본적으로 `frames.pdf`라는 파일이 생성된다). 해당 PDF 파일을 열어보면, “world→robot\_base” 등으로 이어지는 프레임 관계도를 한눈에 파악할 수 있다.

만약 특정 프레임이 빠져 있다거나, 부모 프레임이 잘못 설정되어 있으면, PDF 상에서 그 구조가 잘못 표시되므로 문제 파악이 용이하다.

**2) rqt\_tf\_tree**

`rqt_tf_tree` 플러그인은 ROS2에서 제공하는 rqt 기반 TF 시각화 도구다. 아래 명령으로 실행할 수 있다.

```
source ~/ros2_ws/install/setup.bash
ros2 run rqt_tf_tree rqt_tf_tree
```

실행 후 GUI 창이 나타나면, 현재 TF 트리가 트리 구조로 표현된다. view\_frames와 마찬가지로, 프레임이 어떻게 연결되어 있는지 즉시 확인 가능하다. 만약 특정 노드(브로드캐스터)가 제대로 실행되지 않거나, 프레임 이름이 겹치는 등의 문제가 있으면 트리 형태에서 오류를 찾을 수 있다.

#### 시간 동기화 및 Transform 예외 처리

tf2는 내부적으로 “시간” 개념을 중요하게 다룬다. 브로드캐스트된 Transform에는 타임스탬프가 포함되며, 리스너가 특정 시점의 Transform을 가져오고자 할 때 타임스탬프 기준으로 검색한다. 이때 고려해야 할 사항들이 있다.

1. **타임스탬프 불일치** 브로드캐스터가 퍼블리시하는 Transform의 타임스탬프가 현재 시각보다 과도하게 미래이거나, 과거라면 리스너가 변환을 찾지 못할 수 있다. 예: “Lookup would require extrapolation” 에러.
2. **lookupTransform() 호출시 시간** `lookupTransform("frame1", "frame2", t)` 형태에서 $t$에 특정 ROS 시간을 넣을 수도 있지만, 보통은 최근 데이터를 사용하기 위해 `tf2::TimePointZero(현재 시점)` 혹은 "0"을 사용한다.
3. **TransformException** 브로드캐스트 노드가 아직 실행되지 않았거나, 프레임 이름이 잘못되었을 경우, `lookupTransform`가 실패한다. 이때 발생하는 예외를 반드시 `try-catch` 구문으로 처리해야 노드가 비정상 종료되지 않는다.

#### canTransform()을 통한 TF 가능 여부 확인

리스너 입장에서 변환을 시도하기 전, 해당 변환이 현재 tf Buffer에 존재하는지 확인하고 싶다면 $canTransform$를 사용하면 된다.

```cpp
bool can_transform = tf_buffer_->canTransform("world", "robot_base", tf2::TimePointZero);
if (can_transform) {
  auto transform_stamped = tf_buffer_->lookupTransform("world", "robot_base", tf2::TimePointZero);
  // 변환 로직
} else {
  RCLCPP_WARN(get_logger(), "Transform not found!");
}
```

이처럼 변환 가능 여부를 먼저 확인하면, 불필요한 예외 처리를 줄이고 노드 흐름을 제어하기 용이하다.

#### Python 예제

지금까지는 C++ 예제를 중심으로 살펴보았지만, ROS2 tf2는 Python API도 제공한다. Python으로 TF 브로드캐스터와 리스너를 구현하려면 다음과 같은 라이브러리를 활용한다.

* 브로드캐스트: `tf2_ros.StaticTransformBroadcaster()`, `tf2_ros.TransformBroadcaster()`
* 리스너: `tf2_ros.Buffer()`, `tf2_ros.TransformListener()`

아래는 브로드캐스터 Python 예제다(`tf2_broadcaster_py.py` 가정).

```python
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
from rclpy.duration import Duration
import math
import time

class Tf2BroadcasterPy(Node):
    def __init__(self):
        super().__init__('tf2_broadcaster_py')
        self.br = TransformBroadcaster(self)
        self.timer = self.create_timer(0.1, self.timer_callback)

    def timer_callback(self):
        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = 'world'
        t.child_frame_id = 'robot_base'
        t.transform.translation.x = 1.0
        t.transform.translation.y = 2.0
        t.transform.translation.z = 0.0
        # 회전은 z축 회전 예시
        theta = time.time() % (2*math.pi)
        qz = math.sin(theta/2)
        qw = math.cos(theta/2)
        t.transform.rotation.x = 0.0
        t.transform.rotation.y = 0.0
        t.transform.rotation.z = qz
        t.transform.rotation.w = qw
        
        self.br.sendTransform(t)

def main(args=None):
    rclpy.init(args=args)
    node = Tf2BroadcasterPy()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    rclpy.shutdown()

if __name__ == '__main__':
    main()
```

위 예제는 $0.1$초 주기로 `world→robot_base` 변환을 퍼블리시하며, $z$ 축을 기준으로 회전이 시간이 지날수록 변하는 형태로 구현했다. 이 코드를 `CMakeLists.txt` 또는 `setup.py` 등에서 ROS2 실행 가능 스크립트로 등록한 뒤, 다음과 같이 실행한다.

```
source ~/ros2_ws/install/setup.bash
ros2 run tf2_example_pkg tf2_broadcaster_py
```

해당 브로드캐스터가 올바르게 동작 중이라면, `tf2_ros tf2_echo world robot_base`를 통해 시간이 지남에 따라 `rotation.z` 값이 변하는 것을 확인할 수 있다.

#### Python 리스너 예제

이번에는 Python 기반 리스너 예제를 살펴보자. C++과 마찬가지로 tf2\_ros.Buffer와 tf2\_ros.TransformListener를 이용해 특정 프레임간 변환을 조회한다. 아래 코드는 “world→robot\_base” 변환을 주기적으로 조회해 로그를 남기는 간단한 예시이다(`tf2_listener_py.py` 가정).

```python
import rclpy
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException
import time

class Tf2ListenerPy(Node):
    def __init__(self):
        super().__init__('tf2_listener_py')
        self.buffer = Buffer()
        self.listener = TransformListener(self.buffer, self)
        self.timer = self.create_timer(0.5, self.timer_callback)

    def timer_callback(self):
        try:
            transform_stamped = self.buffer.lookup_transform(
                'world',
                'robot_base',
                rclpy.time.Time()
            )
            x = transform_stamped.transform.translation.x
            y = transform_stamped.transform.translation.y
            z = transform_stamped.transform.translation.z

            self.get_logger().info(f"world->robot_base: x={x:.2f}, y={y:.2f}, z={z:.2f}")
        except (LookupException, ConnectivityException, ExtrapolationException) as e:
            self.get_logger().warn(f"Could not transform: {str(e)}")

def main(args=None):
    rclpy.init(args=args)
    node = Tf2ListenerPy()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    rclpy.shutdown()

if __name__ == '__main__':
    main()
```

* `tf2_ros.Buffer()`를 생성하고 이를 `TransformListener`에 넘겨주면, 노드가 실행되는 동안 `/tf`, `/tf_static` 토픽을 구독해 TF 정보를 쌓는다.
* 주기적 타이머 콜백에서 `lookup_transform('world', 'robot_base', rclpy.time.Time())`를 통해 최신 변환을 가져온다.
* 예외 처리는 C++ 예제와 마찬가지로 `try-except`로 해야 한다. 만약 브로드캐스터 노드가 실행되지 않았다면 변환 정보를 찾을 수 없고, 예외가 발생한다.

#### Geometry 변환 유틸

tf2에서 메시지를 변환하거나, 행렬 연산을 쉽게 처리할 수 있도록 몇 가지 유틸이 제공된다. 예를 들어, C++에서는 `tf2::fromMsg`, `tf2::toMsg` 등을 사용해 `geometry_msgs::msg::Transform`, `geometry_msgs::msg::Pose` 등의 메시지를 4×4 행렬이나 다른 수학 라이브러리(eigen, kdl 등) 객체로 변환할 수 있다.

아래는 `geometry_msgs::msg::TransformStamped`를 `tf2::Transform` (KDL 기반) 객체로 변환하는 예시다.

```cpp
#include <tf2/transform_datatypes.h>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>

// ...

geometry_msgs::msg::TransformStamped ts = /* tf2_ros::Buffer에서 가져온 변환 */;
tf2::Transform tf2_transform;
tf2::fromMsg(ts.transform, tf2_transform);

// tf2_transform 객체를 이용해 KDL 형식으로 변환 연산 수행 가능
tf2::Vector3 origin = tf2_transform.getOrigin();  // 병진 벡터
tf2::Quaternion rot = tf2_transform.getRotation(); // 회전 (쿼터니언)
```

Python에서도 `tf_transformations` 혹은 `geometry_transforms` 라이브러리를 활용해 메시지를 행렬 형태로 바꾸는 방법이 있다. 예를 들어 아래와 같은 식으로 사용할 수 있다.

```python
import tf_transformations
from geometry_msgs.msg import TransformStamped

def transform_to_matrix(transform_stamped: TransformStamped):
    trans = transform_stamped.transform.translation
    rot = transform_stamped.transform.rotation
    # 변환행렬 4x4 생성
    mat = tf_transformations.quaternion_matrix([rot.x, rot.y, rot.z, rot.w])
    mat[0, 3] = trans.x
    mat[1, 3] = trans.y
    mat[2, 3] = trans.z
    return mat
```

이렇게 얻은 $4\times4$ 행렬(동차변환행렬)을 직접 곱셈하여 위치나 회전 좌표를 변환하거나, 더 복잡한 로봇 모델 계산에 활용할 수 있다.

#### tf2\_eigen 예시

ROS2의 `tf2_eigen` 패키지를 사용하면, Eigen 라이브러리를 활용하여 tf 변환을 다룰 수 있다. 예컨대 `Eigen::Isometry3d` 형식으로 변환 정보를 다루고 싶다면 다음과 같은 함수를 사용할 수 있다.

```cpp
#include <tf2_eigen/tf2_eigen.h>
#include <Eigen/Geometry>

// ...

geometry_msgs::msg::TransformStamped ts = /* tf2_ros::Buffer에서 가져온 변환 */;
Eigen::Isometry3d eigen_tf = tf2::transformToEigen(ts);
```

이렇게 변환하면 Eigen의 여러 연산(행렬 곱, 벡터 변환 등)을 바로 적용할 수 있다.

$$
\mathbf{p}' = \mathbf{T},\mathbf{p} =  \begin{bmatrix} \mathbf{R} & \mathbf{t} \ \mathbf{0}^\mathsf{T} & 1 \end{bmatrix} \begin{bmatrix} x \ y \ z \ 1 \end{bmatrix}
$$

과 같은 형태로, 3D 포인트 $\mathbf{p}$를 Eigen 라이브러리 상에서 곱셈 연산만으로 쉽게 변환 가능하다.

#### mermaid를 활용한 TF 트리 예시

tf2 트리를 시각화하기 위해 RViz2나 `view_frames` 등을 사용할 수 있지만, 간단히 문서에서 보여주고 싶다면 다이어그램을 활용할 수도 있다. 예를 들어 다음과 같이 `world`→`robot_base`→`camera_link`, `lidar_link` 구조를 나타낼 수 있다.

{% @mermaid/diagram content="graph LR
W\[world] --> R\[robot\_base]
R --> C\[camera\_link]
R --> L\[lidar\_link]" %}

위와 같은 다이어그램은 실제 ROS2 TF 트리와 동일하게 부모-자식(Parent-Child) 관계를 나타낸다. `W` 노드가 `world` 프레임에 해당하며, `R` 노드(`robot_base`)와 `C` 노드(`camera_link`), `L` 노드(`lidar_link`)가 자식 프레임이다. 실제 로봇에는 이보다 더 많은 링크(프레임)이 존재할 수 있으며, 각각을 tf2의 브로드캐스터 노드에서 브로드캐스트하면 전부 하나의 큰 트리가 된다.

#### 흔히 발생하는 문제와 해결 방안

1. **프레임 이름 충돌**
   * 서로 다른 브로드캐스터 노드에서 부모-자식 관계가 중복되거나, 동일 이름을 가진 프레임을 복수로 퍼블리시하면 TF 트리에 충돌이 생긴다.
   * 해결: 각 센서, 링크 등의 프레임 이름을 고유하게 설정하고, 중복되는 노드(브로드캐스터)가 있는지 확인한다.
2. **Transform 시점 오류**
   * 브로드캐스터가 현재 시각보다 과도하게 과거 시점의 Transform을 퍼블리시하거나, 미래 시각으로 된 Transform을 퍼블리시하는 경우 리스너에서 “Extrapolation” 에러가 발생한다.
   * 해결: 타임스탬프를 노드 실행 시각과 동기화하거나, ROS Clock을 적절히 활용한다. 예: `transform_stamped.header.stamp = this->now();`
3. **네임스페이스/토픽 이름 불일치**
   * 브로드캐스터와 리스너가 서로 다른 네임스페이스를 사용해 `/tf` 토픽을 제대로 주고받지 못하는 상황이 발생할 수 있다.
   * 해결: `ros2 topic list` 등을 통해 실제 토픽 이름을 확인하고, 동일한 네임스페이스에서 실행되도록 하거나 remap 설정을 맞춘다.
4. **Static Transform 누락**
   * 정적 변환이 필요한 링크가 있는데, 동적 브로드캐스터만 실행되어 정적 링크가 뜨지 않는 경우 트리 구조가 끊긴다.
   * 해결: `tf2_ros::StaticTransformBroadcaster`(C++) 또는 `tf2_ros.StaticTransformBroadcaster()`(Python)를 이용해 필요한 정적 링크도 누락 없이 퍼블리시한다.
5. **RViz2에서 프레임 미표시**
   * TF 디스플레이를 켰는데도 일부 프레임이 보이지 않는다면, 실제 변환이 퍼블리시되지 않거나, RViz2의 Fixed Frame 설정이 잘못되었을 수 있다.
   * 해결: Fixed Frame을 `world`(혹은 의도하는 최상위 프레임)로 설정하고, 브로드캐스터 노드가 정상 실행 중인지, `tf2_echo` 결과가 존재하는지 확인한다.
6. **시간 동기화 문제**
   * 시뮬레이션 환경(Gazebo 등)에서 시계(clock) 설정이 실제 시각과 크게 차이나는 경우, `lookupTransform`가 빈번히 실패할 수 있다.
   * 해결: 시뮬레이션 시간과 ROS2 시간(`use_sim_time`)을 일치시키거나, Transform 타임아웃을 증가시키는 등의 설정을 조정한다.

#### 성능 이슈와 최적화

ROS2 tf2는 보통 수백 Hz 정도의 Transform 퍼블리시도 무난히 처리할 수 있으나, 아주 많은 링크(수십\~수백)와 높은 빈도(수백 Hz 이상)가 동시에 쏟아지면 시스템 부하가 커질 수 있다.

* **퍼블리시 빈도 조절**: 동적으로 움직이는 프레임이 아니라면 `StaticTransformBroadcaster`를 쓰고, 동적이라도 꼭 필요한 빈도(예: 50Hz \~ 100Hz)로 줄인다.
* **Message Filter**: tf2 자체가 `tf2::Buffer`에 데이터를 쌓아두지만, 응용 측면에서도 필요하지 않은 변환 호출 횟수를 줄일 수 있다.
* **멀티 스레드 사용**: 고빈도 TF와 여러 노드가 동시에 동작할 때는 ROS2 Executor(멀티 스레드 스피너 등)를 활용해 노드 실행 루프가 병렬로 돌도록 설정한다.

#### tf2 예제 패키지 구조 정리

마무리로, 이 장에서 다룬 tf2 예제 패키지를 하나의 구조 예시로 제시하면 다음과 같다.

```
tf2_example_pkg
├── CMakeLists.txt
├── package.xml
├── launch
│   └── tf2_example.launch.py
├── src
│   ├── tf2_broadcaster.cpp
│   ├── tf2_listener.cpp
│   └── ...
├── include
│   └── tf2_example_pkg
│       └── ...
├── scripts
│   ├── tf2_broadcaster_py.py
│   └── tf2_listener_py.py
└── ...
```

* `src` 디렉터리: C++ 노드 소스
* `scripts` 디렉터리: Python 노드 스크립트
* `launch` 디렉터리: ROS2 Launch 파일
* `include` 디렉터리: 필요한 헤더 파일 (사용자 정의 라이브러리 등)

이처럼 tf2 브로드캐스터와 리스너, 그리고 이를 한 번에 실행할 수 있는 Launch 파일을 구성해두면, ROS2 Humble 기반의 tf2 실습 및 개발 과정에서 반복해서 활용하기가 편리하다.
