# Launch 파일의 기본 개념

#### Launch 시스템이란?

ROS2 Humble에서의 Launch 시스템은 여러 노드를 하나의 실행 단위로 묶어 일괄적으로 시작(또는 종료)할 수 있도록 해주는 도구다. 기존 ROS1 시대에는 `.launch`라는 XML 형식의 파일을 사용했으나, ROS2에서는 Python 스크립트를 기반으로 한 유연한 Launch 시스템이 도입되었다. 이를 통해 사용자는 런치 과정에서 다양한 로직과 조건 분기를 쉽게 적용할 수 있다.

#### Launch 파일의 역할

ROS2 Launch 파일은 다음과 같은 역할을 수행한다.

1. **노드 실행** 여러 노드를 동시에 또는 순차적으로 실행하기 위한 지침을 제공한다.
2. **파라미터 설정** 노드에서 사용하는 파라미터를 미리 정의하여 일괄 로딩한다.
3. **환경 설정** ROS2 통신 설정, 환경 변수, 작업 디렉토리 등을 필요한 형태로 조율한다.
4. **조건부 실행 및 제어** 디버깅이나 특정 상황에서만 노드를 실행하거나, Launch 시점에 조건을 걸어 분기 처리를 할 수 있게 해준다.

#### Python 기반 Launch 파일

ROS2 Humble에서 Launch 파일은 주로 Python 스크립트로 작성한다. Python 함수를 통해 노드를 생성하거나, 파라미터를 등록하는 등 아주 정교한 제어가 가능하다. 예를 들어, 아래와 같은 간단한 형태의 Launch 파일을 가정해보자.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='talker',
            name='my_talker'
        ),
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='my_listener'
        ),
    ])
```

* `launch.LaunchDescription` 객체에 여러 개의 `Node` 액션을 담아 반환하는 구조다.
* `Node`는 `package`, `executable`, `name` 등의 매개변수를 받아서 실행할 노드의 정보를 표현한다.

#### 노드와 액션(Action)

ROS2 Launch 시스템에서 핵심은 \*\*액션(Action)\*\*이다. 여기서 말하는 액션은 노드를 실행하거나, 로깅을 설정하거나, 특정 조건문을 수행하는 등의 작업 단위를 지칭한다. 대표적인 액션 유형은 다음과 같다.

* **Node**: 실제 ROS2 노드를 실행한다.
* **ExecuteProcess**: 일반 시스템 프로세스를 실행한다.
* **IncludeLaunchDescription**: 다른 Launch 파일을 재활용하여 중첩 실행한다.
* **LogInfo**: Launch 진행 상황 등을 로그로 출력한다.

#### Launch 파일의 최소 구성

Launch 파일을 작성할 때 최소 구성을 살펴보면 다음과 같은 과정을 거친다.

1. `import` 구문을 통해 Launch 시스템에 필요한 라이브러리를 불러온다.
2. `generate_launch_description()` 함수를 정의한다.
3. `LaunchDescription` 객체를 생성하고, 여기에 액션들을 순서대로 추가한다.
4. 마지막에 `LaunchDescription` 객체를 반환한다.

#### Launch 파일 실행 방법

ROS2 Launch 파일 실행은 `ros2 launch` 명령어를 사용한다. 예를 들어, 위 예시 Launch 파일이 `my_launch.py`라는 이름으로 저장되어 있다면, 다음과 같이 실행한다.

```bash
ros2 launch my_package my_launch.py
```

* `my_package`는 `setup.py` 또는 `package.xml`에서 정의된 ROS2 패키지 이름이다.
* `my_launch.py`는 `launch/` 디렉토리 내(혹은 별도로 설정한 위치)에 놓여 있는 Launch 파일이다.

#### Launch 파일과 조건부 실행

ROS2 Launch 시스템은 실행 시점에 특정 조건을 만족할 경우에만 액션을 실행하도록 제어할 수 있다. 이를 위해 `IfCondition`, `UnlessCondition` 등과 같은 구문을 사용할 수 있으며, Launch 파일 내에서 동적으로 로직을 분기할 수 있다. 예를 들어, 특정 환경 변수의 값에 따라 노드를 실행하거나 실행하지 않도록 설정할 수도 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    use_talker = LaunchConfiguration('use_talker', default='true')
    
    talker_node = Node(
        package='demo_nodes_py',
        executable='talker',
        name='conditional_talker',
        condition=IfCondition(use_talker)
    )

    return LaunchDescription([talker_node])
```

* `IfCondition(use_talker)`는 Launch 설정에서 `use_talker`가 `'true'`일 때만 실행을 허용한다.
* Launch 실행 시 다음 명령으로 파라미터를 넘길 수 있다:

```bash
ros2 launch my_package my_launch.py use_talker:=false
```

#### Substitution 활용

Launch 파일에서 여러 종류의 Substitution(대체 구문)을 이용해 환경 변수를 읽거나, 파라미터 값을 가져오거나, Launch Configuration(런치 설정)을 참조하여 로직을 구성할 수 있다. 자주 쓰이는 Substitution은 다음과 같다.

* **LaunchConfiguration**: Launch 실행 시 전달된 인자를 참조한다.
* **EnvironmentVariable**: OS 환경 변수를 읽어온다.
* **TextSubstitution**: 단순 문자열 치환을 처리한다.
* **Command**: 특정 쉘 명령을 실행하고 그 결과를 문자열로 가져온다.

사용 예시:

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import Command

def generate_launch_description():
    # Git commit hash를 노드 이름에 반영하고 싶다고 가정
    git_hash = Command(['git', 'rev-parse', '--short', 'HEAD'])
    
    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='listener',
            name=git_hash
        ),
    ])
```

* `Command(['git', 'rev-parse', '--short', 'HEAD'])`는 로컬 깃 저장소의 현재 커밋 해시를 문자열로 변환한다.
* Launch 파일 실행 시, 노드 이름으로 그 해시 문자열이 설정된다.

#### LaunchConfiguration과 Launch 선언

Launch 파일에서 `LaunchConfiguration` 객체를 생성하여, 이 값을 런치 인자로 받을 수 있게 설정할 수 있다. 이렇게 하면 여러 런치 옵션을 손쉽게 관리할 수 있다. 다음 예시를 보면:

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    namespace_arg = DeclareLaunchArgument(
        'robot_ns',
        default_value='robot1',
        description='Robot namespace'
    )
    robot_ns = LaunchConfiguration('robot_ns')

    return LaunchDescription([
        namespace_arg,
        Node(
            package='demo_nodes_py',
            executable='listener',
            namespace=robot_ns,
            name='my_listener'
        ),
    ])
```

* `DeclareLaunchArgument`를 통해 `'robot_ns'`라는 런치 인자를 선언하고, 기본값을 `'robot1'`로 설정한다.
* Launch 실행 시 명령어를 통해 다른 값을 넘길 수 있다:

```bash
ros2 launch my_package my_launch.py robot_ns:=robot2
```

* 그러면 생성되는 노드의 네임스페이스가 `robot2`가 된다.

#### 노드 그룹화 및 중첩 Launch

여러 노드를 묶어서 그룹 단위로 관리하거나, 다른 Launch 파일을 재사용(Include)하는 기능도 있다. 노드 그룹화는 주로 동일한 네임스페이스를 공유하거나 같은 목적으로 묶을 때 사용된다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch_ros.actions import PushRosNamespace

def generate_launch_description():
    return LaunchDescription([
        PushRosNamespace('robot1'),
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='listener1'
        ),
        Node(
            package='demo_nodes_py',
            executable='talker',
            name='talker1'
        ),
    ])
```

* `PushRosNamespace('robot1')` 액션 이후에 선언된 노드들은 모두 `robot1`라는 네임스페이스를 갖게 된다.

#### 이벤트 기반 제어와 이벤트 핸들러

ROS2 Launch 시스템은 단순히 노드를 실행하고 종료하는 것을 넘어, 런치 시점 혹은 노드 실행 중 발생하는 여러 이벤트를 처리할 수 있다. 예를 들어, 프로세스가 정상 종료했을 때 후속 액션을 취하거나, 런치가 종료될 때 특정 로직을 실행하는 식이다. 이를 위해 제공되는 대표적인 이벤트 핸들러 유형은 다음과 같다.

* **OnProcessExit**: 지정된 노드(혹은 프로세스)가 종료될 때 액션을 실행한다.
* **OnProcessStart**: 지정된 노드가 시작될 때 액션을 실행한다.
* **OnShutdown**: 런치 전체가 종료될 때 액션을 실행한다.

사용 예시는 다음과 같다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit, OnProcessStart
from launch.actions import LogInfo

def generate_launch_description():
    talker_node = Node(
        package='demo_nodes_py',
        executable='talker',
        name='talker'
    )
    listener_node = Node(
        package='demo_nodes_py',
        executable='listener',
        name='listener'
    )

    event_handler_on_exit = RegisterEventHandler(
        OnProcessExit(
            target_action=talker_node,
            on_exit=[LogInfo(msg="Talker node가 종료되었다.")]
        )
    )

    event_handler_on_start = RegisterEventHandler(
        OnProcessStart(
            target_action=listener_node,
            on_start=[LogInfo(msg="Listener node가 실행되었다.")]
        )
    )

    return LaunchDescription([
        talker_node,
        listener_node,
        event_handler_on_exit,
        event_handler_on_start
    ])
```

* `OnProcessExit`는 `talker_node`가 종료되는 이벤트를 감시하고, 종료 시점에 "Talker node가 종료되었다."라는 메시지를 출력한다.
* `OnProcessStart`는 `listener_node`가 시작되는 이벤트를 감시하고, 시작 시점에 "Listener node가 실행되었다."라는 메시지를 출력한다.

#### TimerAction

런치 파일에서 일정 시간이 흐른 뒤 액션을 실행해야 하는 경우가 있다. 이때 `TimerAction`을 활용하면 지정된 시간 후에 액션을 실행할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import TimerAction, LogInfo

def generate_launch_description():
    delayed_action = TimerAction(
        period=5.0,
        actions=[
            LogInfo(msg='5초 뒤에 실행되는 액션이다.')
        ]
    )

    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='listener'
        ),
        delayed_action
    ])
```

* `period=5.0`으로 설정하면, 런치가 시작된 지 5초 후에 `LogInfo` 액션을 수행한다.

#### OpaqueFunction

조금 더 복잡한 스크립트 로직이 필요한 경우, Launch 파일 내에서 Python 함수를 직접 호출하여 결과를 액션으로 연결하고 싶을 때가 있다. 이때 `OpaqueFunction` 액션을 사용한다.

```python
from launch import LaunchDescription
from launch.actions import OpaqueFunction, LogInfo

def complex_logic(context, *args, **kwargs):
    # 복잡한 계산 예시
    result = 1
    for i in range(1, 5):
        result *= i
    return [LogInfo(msg=f'계산 결과: {result}')]

def generate_launch_description():
    return LaunchDescription([
        OpaqueFunction(function=complex_logic),
    ])
```

* `OpaqueFunction`은 함수를 실행한 뒤, 해당 함수가 반환하는 액션(들)을 런치 시스템에 등록한다.
* 위 예시에서는 $1 \times 2 \times 3 \times 4 = 24$라는 결과를 메시지로 출력한다.

#### LaunchContext와 런치 흐름

ROS2 Launch 시스템은 내부적으로 **LaunchContext**라는 객체를 사용하여 런치 인자, 환경 변수, 이벤트, 액션 상태 등을 관리한다. 각 액션은 런치 컨텍스트를 통해 필요한 정보(예: LaunchConfiguration, OS 환경 변수, 앞선 액션의 결과 등)에 접근할 수 있다.

이를 수식으로 표현하자면, 런치 컨텍스트를 $\mathbf{LC}$라 하고, 액션들을 $\mathbf{A\_1}, \mathbf{A\_2}, \dots, \mathbf{A\_n}$이라 할 때,

$$
\mathbf{LC}: \mathbf{A\_i} \rightarrow \mathbf{S}
$$

위와 같이, 액션 $\mathbf{A\_i}$가 주어지면 컨텍스트 $\mathbf{LC}$를 통해 상태 $\mathbf{S}$(런치 인자, 환경 정보 등)을 받아오는 구조라고 볼 수 있다. 런치 시스템은 각 액션의 실행 시점에 $\mathbf{LC}$를 전달하며, 액션을 통해 변경된 정보 역시 재귀적으로 런치 컨텍스트에 갱신된다.

#### 디버깅과 로깅

Launch 시스템에서 디버깅을 위해 로깅 메시지를 출력하는 방식을 자주 사용한다. `LogInfo`, `LogWarn`, `LogError` 같은 액션을 통해 런치 스크립트 실행 과정이나 특정 이벤트 발생 시점에서 메시지를 남길 수 있다.

```python
from launch import LaunchDescription
from launch.actions import LogWarn

def generate_launch_description():
    return LaunchDescription([
        LogWarn(msg="테스트 실행을 시작한다."),
        # 다른 액션들...
    ])
```

* `LogWarn`은 경고 수준의 메시지를 노출하여 중요도 높은 알림을 전달할 수 있다.

#### 매개변수 파일(Parameter File)과 Launch

ROS2 노드가 필요로 하는 파라미터를 보다 체계적으로 관리하기 위해서는 파라미터 파일(일반적으로 YAML 형식)을 활용할 수 있다. Launch 파일에서 이 파라미터 파일을 로드하여 노드를 실행하면, 노드 내부에서 해당 파라미터 값을 자동으로 불러온다.

```yaml
# params.yaml
my_listener:
  ros__parameters:
    frequency: 10
    use_sim_time: true
```

위와 같은 YAML 파일이 있을 때, 이를 Launch 파일에서 노드 실행 시 참조하려면 다음과 같이 설정한다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='my_listener',
            parameters=['params.yaml']
        ),
    ])
```

* `parameters` 인자에 YAML 파일의 경로를 리스트로 주어 노드 생성 시점을 정의한다.
* 파일 경로는 상대 혹은 절대 경로 모두 지정 가능하다.
* 여러 개의 파라미터 파일을 동시에 로드하거나, Python dict 형태로 파라미터를 직접 전달할 수도 있다.

#### Remap 설정

ROS2에서 토픽, 서비스 등의 이름을 동적으로 바꿔야 할 때(예: 테스트 환경에서만 특정 토픽 이름을 다르게 사용하고자 할 때) Remap(리매핑) 기능을 활용한다. Launch 파일에서 Remap 설정은 `remappings` 인자를 통해 수행할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='my_listener',
            remappings=[
                ('/chatter', '/renamed_chatter')
            ]
        ),
    ])
```

* `remappings` 인자는 튜플의 리스트로 구성된다. 예: `('원본_이름', '대체_이름')`
* 실행 시 `/chatter` 토픽을 구독하려고 했던 노드는 `/renamed_chatter` 토픽을 구독하게 된다.

#### Composable Nodes와 Launch

ROS2의 특징 중 하나인 Composable Nodes를 Launch 파일로도 쉽게 구성할 수 있다. Composable Node는 여러 노드를 하나의 프로세스(프로세스 컨테이너) 내에서 실행하여, 노드 간 통신 오버헤드를 줄이고 효율을 높일 수 있다. Launch 파일 예시는 다음과 같다.

```python
from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    talker_composable = ComposableNode(
        package='demo_nodes_cpp',
        plugin='demo_nodes_cpp::Talker',
        name='talker_composable'
    )

    listener_composable = ComposableNode(
        package='demo_nodes_cpp',
        plugin='demo_nodes_cpp::Listener',
        name='listener_composable'
    )

    container = ComposableNodeContainer(
        name='my_container',
        namespace='',
        package='rclcpp_components',
        executable='component_container_mt',
        composable_node_descriptions=[
            talker_composable,
            listener_composable
        ],
        output='screen'
    )

    return LaunchDescription([container])
```

* `ComposableNodeContainer` 안에 여러 `ComposableNode`를 등록하여, 모든 노드가 하나의 프로세스 내에서 동작하게 구성한다.
* `component_container_mt`, `component_container` 등 여러 종류의 컨테이너 실행 방식을 고를 수 있으며, 멀티스레드 혹은 싱글스레드 실행 정책을 적용할 수 있다.

#### IncludeLaunchDescription로 다른 Launch 파일 재사용

대규모 프로젝트에서는 Launch 파일이 여러 개로 분리되어 있을 수 있다. 이때 특정 Launch 파일을 다른 Launch 파일에서 재활용하고자 할 때 `IncludeLaunchDescription` 액션을 사용한다.

```python
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_source import PythonLaunchDescriptionSource
import os

def generate_launch_description():
    current_dir = os.path.dirname(__file__)
    included_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource([os.path.join(current_dir, 'sub_launch.py')]),
        launch_arguments={'robot_ns': 'robot3'}.items()
    )

    return LaunchDescription([
        included_launch
    ])
```

* `PythonLaunchDescriptionSource`에 대상 Launch 파일(`sub_launch.py`) 경로를 전달한다.
* `launch_arguments`를 통해 하위 Launch 파일에 인자를 넘겨줄 수 있다.
* 이렇게 중첩 구조로 Launch 파일을 구성하면, 복잡한 프로젝트에서도 구조화된 런치 구성이 가능하다.

#### Launch 환경변수 설정

노드를 실행할 때 환경 변수를 바꿔야 하는 상황이 있다면, `set_env`, `unset_env`, `env` 등의 매개변수를 활용할 수 있다. 예시:

```python
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='my_listener',
            output='screen',
            emulate_tty=True,
            additional_env={'MY_ENV_VAR': 'HelloLaunch'}
        ),
    ])
```

* `additional_env`에 dict 형태로 환경 변수를 명시하면, 노드 실행 시점에 해당 환경 변수가 설정된다.
* `Node` 액션 외에 `ExecuteProcess` 액션에서도 유사한 형태로 환경 변수를 지정할 수 있다.

#### Launch 파일 내의 순서 보장

Launch 파일 안에서 노드 실행 순서를 엄격히 보장하고 싶을 때가 있다. 일반적으로 모든 노드가 거의 동시에 실행되지만, 특정 노드가 완전히 뜬 뒤에 다른 노드를 실행해야 하는 요구사항이 있을 수 있다. 이를 위해서는 이벤트 핸들러나 `TimerAction` 등을 조합해서 순서를 제어하는 방식을 사용한다.

예컨대 다음과 같은 의사 수식으로 표현해 보자:

$$
\mathbf{A\_1} \xrightarrow{\text{실행 완료}} \mathbf{A\_2}
$$

* $\mathbf{A\_1}$ 노드(액션)가 정상적으로 시작(혹은 종료)되었음을 이벤트로 감지한 후, $\mathbf{A\_2}$ 노드(액션)을 실행시키는 구조다.
* 이를 이벤트 핸들러와 `OnProcessStart`, `OnProcessExit` 등으로 구현할 수 있다. 때로는 `TimerAction`으로 의도적으로 대기 시간을 두는 방식도 사용된다.

#### Launch 전·후 처리(Action)

ROS2 Launch 시스템에서는 노드 실행뿐 아니라, 런치가 시작하기 직전 혹은 종료 직후에 특정 작업을 수행하고 싶을 때도 있다. 예컨대, ROS2 실행 전후로 데이터를 백업하거나 특정 프로세스를 종료하는 식이다. 이러한 전후 처리는 일반 액션이나 이벤트 핸들러를 적절히 활용하여 구현할 수 있다.

1. **런치 시작 전(Action 실행)** 런치가 시작되기 전에 작업을 수행하고 싶다면, `OpaqueFunction`을 활용하거나, 별도의 스크립트로 사전 준비 과정을 분리하는 방법을 쓸 수 있다. 실제로는 런치 시작 "직전"이 아니라 런치 시스템이 액션들을 평가하는 시점이므로, 완전한 사전 처리는 외부에서 하는 경우도 많다.
2. **런치 종료 후(Action 실행)** 런치가 종료되는 시점(`OnShutdown` 이벤트)에서 후속 처리를 할 수 있다. 예시:

   ```python
   from launch import LaunchDescription
   from launch.actions import RegisterEventHandler, LogInfo
   from launch.event_handlers import OnShutdown

   def generate_launch_description():
       shutdown_handler = RegisterEventHandler(
           OnShutdown(
               on_shutdown=[LogInfo(msg='런치가 종료되었다.')]
           )
       )
       return LaunchDescription([
           shutdown_handler
       ])
   ```

   * `OnShutdown` 이벤트가 발생하면 `LogInfo` 액션이 실행되어 "런치가 종료되었다."라는 메시지를 남긴다.
   * 필요에 따라 이 시점에서 파일 정리, 로그 저장 등 다양한 후속 작업을 수행할 수 있다.

#### Launch 조건문 구성(If, Unless)

ROS2 Launch는 Python의 조건문 로직을 직접 써도 되지만, Launch에서 제공하는 `IfCondition`, `UnlessCondition` 등을 활용해 더 명시적인 조건 분기를 설정할 수 있다. 이를 통해 특정 액션의 실행 여부를 직관적으로 표현할 수 있다.

```python
from launch import LaunchDescription
from launch.actions import LogInfo
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration
from launch.actions import DeclareLaunchArgument

def generate_launch_description():
    debug_arg = DeclareLaunchArgument('debug', default_value='false')
    debug_condition = LaunchConfiguration('debug')

    info_when_debug = LogInfo(
        msg="디버그 모드이다.",
        condition=IfCondition(debug_condition)
    )
    info_when_not_debug = LogInfo(
        msg="디버그 모드가 아니다.",
        condition=UnlessCondition(debug_condition)
    )

    return LaunchDescription([
        debug_arg,
        info_when_debug,
        info_when_not_debug
    ])
```

* 런치 파일 실행 시 `debug:=true`로 설정하면 `info_when_debug`만 실행되고, 그렇지 않으면 `info_when_not_debug`만 실행된다.
* 로직이 간단한 경우에는 Python의 `if` 문으로 분기할 수도 있지만, Launch 제공 조건을 쓰면 액션 단위로 로직을 명시적으로 분리할 수 있어 유지보수에 유리하다.

#### LaunchConfiguration 활용 심화

`LaunchConfiguration`은 런치 시점에 넘겨받은 인자값을 런치 파일 내에서 폭넓게 활용할 수 있는 강력한 기능이다. 노드 실행, 파라미터 파일 경로, 리매핑 정보 등 다양한 부분을 동적으로 바꿀 수 있다. 예를 들어, 파라미터 파일 경로를 런치 인자로 받고 싶다면 다음과 같이 구성할 수 있다.

```python
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    param_file_arg = DeclareLaunchArgument(
        'param_file',
        default_value='default_params.yaml',
        description='파라미터 파일 경로'
    )
    param_file_path = LaunchConfiguration('param_file')

    return LaunchDescription([
        param_file_arg,
        Node(
            package='demo_nodes_py',
            executable='listener',
            name='my_listener',
            parameters=[param_file_path]
        ),
    ])
```

* 위 예시에서는 `param_file`이라는 런치 인자를 선언하고, 기본값을 `default_params.yaml`로 둔다.
* 실제 실행 시 `ros2 launch my_pkg my_launch.py param_file:=custom_params.yaml`로 원하는 파라미터 파일을 넘길 수 있다.

#### Lifecycle 노드와 Launch

ROS2에는 **Lifecycle Node**라는 개념이 있어, 노드가 상태(State)를 가지고 활성화·비활성화·종료 등을 명시적으로 관리한다. Lifecycle 노드를 Launch 파일에서 실행하려면 일반 노드 실행과 비슷하지만, 노드 상태 변화(transition)를 트리거하기 위해서는 추가적인 트리거 액션이 필요하다.

* 예시(간단화):

```python
from launch import LaunchDescription
from launch_ros.actions import LifecycleNode

def generate_launch_description():
    lifecycle_node = LifecycleNode(
        package='my_lifecycle_package',
        executable='my_lifecycle_node',
        name='my_lifecycle_node',
        namespace=''
    )

    return LaunchDescription([
        lifecycle_node
    ])
```

* `LifecycleNode`는 ROS2의 Managed Node 인터페이스를 지원한다. 노드가 활성화(activate) 되면 각종 콜백을 시작하고, 비활성화(deactivate) 되면 콜백이 정지된다.
* 런치 파일에서 노드 상태 전이를 자동화하려면 별도의 액션(`ChangeState`) 등을 추가로 사용해야 한다.

#### LaunchTesting

ROS2 Launch에는 테스트를 자동화하기 위한 **LaunchTesting** 프레임워크가 존재한다. 이것은 GTest(혹은 pytest)와 결합하여, 런치 파일에서 실행하는 노드의 결과나 동작을 검증하는 용도로 쓰인다. 예:

```python
import os
import pytest
from launch import LaunchDescription
from launch_ros.actions import Node
from launch_testing.utilities import resolve_paths
import launch_testing
import unittest

@pytest.mark.launch_test
def generate_test_description():
    test_node = Node(
        package='demo_nodes_py',
        executable='talker',
        name='test_talker'
    )
    return LaunchDescription([
        test_node,
        launch_testing.actions.ReadyToTest()
    ])

class TestTalker(unittest.TestCase):
    def test_node_startup(self):
        self.assertTrue(True)
```

* `@pytest.mark.launch_test` 데코레이터가 붙은 함수에서 런치 파일 구성처럼 액션을 기술하고, `launch_testing.actions.ReadyToTest()`를 통해 실제 테스트 코드 실행 시점과 연동한다.
* `unittest.TestCase` 기반의 테스트 메서드에서 노드가 정상적으로 동작하는지 검사할 수 있다(토픽 통신 검사, 메시지 내용 검사 등 다양한 방법이 있음).

#### Frontend Launch 파일(XML, YAML)

ROS2 Launch는 Python 기반 외에도 XML이나 YAML 포맷으로 작성할 수 있는 **Frontend**를 제공한다. 예를 들어 XML 형식으로 노드를 실행하려면 다음과 같이 작성할 수 있다.

```xml
<launch>
  <node
    pkg="demo_nodes_py"
    exec="talker"
    name="xml_talker" />
</launch>
```

* 해당 파일을 `my_launch.launch.xml` 등으로 저장한 뒤, `ros2 launch my_pkg my_launch.launch.xml` 명령어로 실행 가능하다.
* YAML 포맷도 유사한 방식이지만, 여러 액션을 표현하기가 조금 까다로울 수 있으므로, 복잡한 로직이 필요한 경우 Python Launch 파일을 쓰는 것이 일반적이다.

#### 실전 팁: Python Launch 작성 시 주의사항

1. **순수 Python 코드와의 결합** Launch 파일은 어디까지나 런치를 위한 스크립트 역할에 집중해야 한다. 지나치게 복잡한 로직(예: 대규모 데이터 처리, 외부 DB 연동 등)은 별도의 모듈에서 처리하고, Launch 파일에서는 최소한의 호출만 수행하는 것이 바람직하다.
2. **런치 인자의 남용 주의** 너무 많은 런치 인자를 선언해버리면 운영과 유지보수가 복잡해진다. 가장 핵심적인 인자만 노출하고, 나머지는 파라미터 파일 또는 다른 형태로 관리하는 방법을 고민해야 한다.
3. **프로세스 충돌** 여러 노드가 동시에 실행될 때, 포트를 사용하는 노드가 충돌할 수도 있다. 이러한 문제를 방지하기 위해 파라미터나 네임스페이스를 적절히 설정해야 하며, Launch로 병렬 노드를 관리할 때는 로그를 면밀히 확인해봐야 한다.
4. **동적 설정 vs 정적 설정** 노드 파라미터나 Remap 설정 등을 런치 시점에 다 동적으로 하려는 욕심을 부리기보다, 자주 변하지 않는 설정은 YAML 파라미터 파일이나 코드 내부에 정적으로 두는 편이 안전하다.
5. **버전 호환** ROS2 버전별로 Launch API 변경 사항이 있을 수 있으므로, 사용 중인 ROS2 버전에 맞는 공식 문서를 확인해야 한다.
