# Launch Arguments 활용 방법

#### Launch Arguments 소개

ROS2의 Launch 시스템에서 ‘Launch Arguments(이하 인자)’는 런치 파일을 실행할 때 외부에서 값을 전달받아, 이를 Launch 서브스크립트(Launch File) 내부에서 동적으로 활용할 수 있게 해준다. 일반적인 Python 코드에서 함수를 호출할 때 함수 인자(argument)를 전달하는 것과 유사하다. 이러한 인자를 사용하면 여러 상황에서 유연하게 값을 바꿔가며 노드를 구동하는 것이 가능해진다.

예를 들어, 같은 Launch 파일을 사용하더라도 로봇 모델 이름, 시뮬레이션 속도, 특정 네임스페이스(namespace) 등을 다르게 주어 실행하고 싶을 때, 인자를 적극 활용하면 별도의 중복 코드를 작성할 필요 없이 런치 파일 한 개로 다양한 환경 설정을 수행할 수 있다.

#### Launch Arguments 활용 시 장점

1. **코드 재사용성 증대** 런치 파일 내에서 하드코딩된 값을 최소화하고, 외부에서 손쉽게 바꿀 수 있도록 유연성을 높여 준다.
2. **동적 파라미터 설정** 실행 시점에 결정할 수 있는 값들을 Launch 인자로 전달받아, 노드를 구동하기 직전에 동적으로 파라미터를 설정할 수 있다.
3. **프로파일링 및 디버깅 편의** 동일한 런치 파일 구조를 유지하면서, 인자만 바꾸어 다양한 테스트 환경을 구성할 수 있으므로 디버깅 및 프로파일링이 간단해진다.

#### Launch 파일에서 인자 선언

ROS2의 Launch 파일(예: Python API 사용)에서 인자를 선언하려면 `launch.actions.DeclareLaunchArgument` 클래스를 활용한다. 다음은 기본적인 선언 예시이다.

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

def generate_launch_description():
    # 인자 선언
    robot_name_arg = DeclareLaunchArgument(
        'robot_name',
        default_value='my_robot',
        description='로봇의 이름'
    )
    
    # 인자 사용 시에는 LaunchConfiguration으로 받아온다.
    robot_name = LaunchConfiguration('robot_name')

    return LaunchDescription([
        robot_name_arg
        # 다른 액션을 추가할 수 있음
    ])
```

여기서 `DeclareLaunchArgument`는

* 첫 번째 매개변수 `'robot_name'`을 통해 인자의 이름을 지정하고,
* `default_value`로 기본값을 설정한다.
* `description`은 도큐멘테이션을 위해 인자가 어떤 용도인지 설명해준다.

이렇게 선언한 인자는 런치 파일 내 다른 액션이나 노드 실행 시에 참조할 수 있다. 참조 시에는 `LaunchConfiguration('robot_name')` 객체로 값을 불러온다.

#### 명령줄에서 인자 전달

런치 파일을 실행할 때 인자를 전달하려면 다음과 같이 터미널에서 `ros2 launch` 명령어 뒤에 `name:=value` 형태를 지정해주면 된다.

```bash
ros2 launch my_package my_launch_file.py robot_name:=test_robot
```

만약 인자를 전달하지 않으면, Launch 파일에 선언된 `default_value`가 사용된다.

#### Launch Arguments와 Substitutions

인자의 값이 단순 문자열이 아닌, 실행 시점에 다른 값을 참조하거나 특정 계산을 통해 동적으로 결정될 수도 있다. 이런 경우 ROS2에서 제공하는 여러 Substitutions(예: `PathJoinSubstitution`, `Command`, `FindPackageShare` 등)를 결합해 인자를 처리할 수 있다.

예를 들어, 특정 패키지 내 리소스 경로를 인자로 받은 뒤, `PathJoinSubstitution`을 활용해 실제 파일 경로를 만들 수 있다.

```python
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare

robot_description_path = PathJoinSubstitution([
    FindPackageShare('my_robot_description'),
    'urdf',
    LaunchConfiguration('robot_name') + '.urdf'
])
```

이와 같이 Substitutions를 결합하면, 런치 파일 내부에서 동적으로 파일 경로 등을 만들어낼 수 있어 매우 유용하다.

#### IfCondition, UnlessCondition과 Launch Arguments

런치 파일에서 특정 인자값이 참인지 거짓인지 등에 따라 액션(노드 실행, Include 등)을 조건부로 수행하려면 `IfCondition`, `UnlessCondition` 같은 조건 액션을 쓸 수 있다. 이때 Launch Arguments로부터 값을 받아와서 조건을 만족할 때만 액션을 실행하도록 설정할 수 있다.

예를 들어, ‘use\_namespace’ 인자가 `true`이면 특정 네임스페이스를 사용하고, 아니라면 루트 네임스페이스를 사용하도록 하고자 할 때 다음과 같이 구성할 수 있다.

```python
from launch.actions import DeclareLaunchArgument, GroupAction
from launch.conditions import IfCondition
from launch_ros.actions import PushRosNamespace, Node
from launch.substitutions import LaunchConfiguration

use_namespace_arg = DeclareLaunchArgument(
    'use_namespace',
    default_value='false',
    description='네임스페이스 사용 여부'
)

namespace_arg = DeclareLaunchArgument(
    'robot_namespace',
    default_value='my_robot',
    description='네임스페이스 이름'
)

group_in_namespace = GroupAction([
    PushRosNamespace(LaunchConfiguration('robot_namespace')),
    Node(
        package='demo_nodes_py',
        executable='talker',
        name='my_talker_namespaced'
    )
], condition=IfCondition(LaunchConfiguration('use_namespace')))

group_in_root = GroupAction([
    Node(
        package='demo_nodes_py',
        executable='talker',
        name='my_talker_root'
    )
], condition=IfCondition('$Not(LaunchConfiguration("use_namespace"))'))
```

위 예시에서,

* `use_namespace` 인자가 `true`일 경우 `group_in_namespace` 액션이 실행되어 `my_robot` 네임스페이스 아래에서 노드가 구동된다.
* `use_namespace` 인자가 `false`면 `group_in_root` 액션이 동작하여 루트 네임스페이스에 노드를 띄운다.

단, `IfCondition('$Not(...)')` 처럼 문자열을 통한 논리 부정 사용은 구버전에서 사용되던 방식이며, ROS2 Humble 버전 이후부터는 `UnlessCondition()`을 쓰는 편이 더 명료하다.

```python
group_in_root = GroupAction([
    Node(
        package='demo_nodes_py',
        executable='talker',
        name='my_talker_root'
    )
], condition=UnlessCondition(LaunchConfiguration('use_namespace')))
```

이처럼 Launch Arguments와 조건 액션을 결합하여 런치 파일 내부에서 유연하게 분기 로직을 구성할 수 있다.

#### GroupAction과 Launch Arguments

여러 노드를 하나의 그룹으로 묶어 공통 설정(네임스페이스, 파라미터 등)을 적용하려면 `GroupAction`을 사용할 수 있다. 이때 인자값을 통해 그룹이 적용할 네임스페이스나 다른 공통 옵션을 손쉽게 설정할 수 있다.

```python
from launch.actions import GroupAction
from launch_ros.actions import PushRosNamespace, Node

robot_ns_arg = DeclareLaunchArgument(
    'robot_ns',
    default_value='robot1',
    description='노드를 실행할 로봇 네임스페이스'
)

grouped_nodes = GroupAction([
    PushRosNamespace(LaunchConfiguration('robot_ns')),
    Node(
        package='my_robot_bringup',
        executable='sensor_driver',
        name='sensor_driver'
    ),
    Node(
        package='my_robot_bringup',
        executable='controller',
        name='controller'
    )
])
```

여기서 `robot_ns` 인자로 전달받은 네임스페이스를 `PushRosNamespace` 액션이 적용하고, 그 그룹 안에 정의된 노드들은 모두 해당 네임스페이스를 사용하게 된다. 따라서 여러 대의 로봇을 동시에 실험할 때, 동일한 런치 파일을 복수 번 실행하되 인자값만 바꾸어주면 된다.

```bash
# 첫 번째 로봇
ros2 launch my_package multi_robot_launch.py robot_ns:=robot1

# 두 번째 로봇
ros2 launch my_package multi_robot_launch.py robot_ns:=robot2
```

#### SetLaunchConfiguration으로 동적 인자 설정

런치 실행 중에 임의의 시점에서 인자(LaunchConfiguration 값)를 새롭게 변경하기 위해 `SetLaunchConfiguration` 액션을 활용할 수도 있다. 이는 런치 파일이 진행되는 동안 런치 설정값을 갱신하기 때문에, 이후에 정의되는 액션에서는 새로운 값이 반영된다.

```python
from launch.actions import SetLaunchConfiguration

set_arg_action = SetLaunchConfiguration(
    name='robot_name',
    value='temporary_robot'
)
```

이 액션을 실행하면 이후에 `LaunchConfiguration('robot_name')`를 참조하는 액션들은 `'temporary_robot'`라는 값을 사용하게 된다. 다만, 대부분의 경우에는 런치 파일이 처음부터 끝까지 일관된 인자값을 사용하는 설계가 많기 때문에, `SetLaunchConfiguration`은 특수한 경우(서로 다른 타임라인에서 동적 변경이 필요할 때)에만 사용된다.

#### Python Script나 Environment Variable과 연동

경우에 따라서는 Shell 환경 변수나 Python 스크립트의 결과를 인자로 활용하고 싶을 수 있다. 이를 위해서도 ROS2의 Substitution 기법을 이용하거나, `os.environ`(Python 표준 라이브러리) 등을 조합해 유연한 설정을 만들 수 있다.

예시) Shell 환경 변수를 런치 인자로 읽어들이는 코드:

```python
import os
from launch.substitutions import EnvironmentVariable

env_var_arg = DeclareLaunchArgument(
    'model_name_from_env',
    default_value=EnvironmentVariable('MODEL_NAME'),
    description='환경변수 MODEL_NAME에서 값 읽기'
)
```

터미널에서 `MODEL_NAME` 환경 변수를 정의하고 런치 파일을 실행하면, `model_name_from_env` 인자에 환경 변수의 값이 들어간다.

```bash
export MODEL_NAME=my_robot_model
ros2 launch my_package env_var_launch.py
```

또한 Python 함수를 통해 동적으로 값을 계산해서 인자로 할당하고 싶다면, 별도의 함수에서 계산을 수행한 뒤, `DeclareLaunchArgument`의 `default_value` 또는 `Node` 파라미터로 넘기면 된다. 마치 다음과 같다:

```python
import random

def get_random_model_name():
    # 임의의 로봇 모델 이름 생성 예시
    return f'model_{random.randint(0, 100)}'

random_model_arg = DeclareLaunchArgument(
    'robot_model_random',
    default_value=get_random_model_name(),
    description='임의로 생성된 로봇 모델 이름'
)
```

실행할 때마다 다른 로봇 모델 이름이 인자로 설정되어, 테스트나 디버깅 상황에서 유용할 수 있다.

#### Launch Arguments와 YAML 파라미터 파일 동적 결합

일반적으로 ROS2 노드는 YAML 형식의 파라미터 파일을 참조하여 설정값을 불러온다. 여기서 런치 인자와 YAML 파일을 연동해, YAML 파일 내부 값 중 일부를 런치 시점에 바꿔 주고 싶을 때는 다음과 같은 접근이 가능하다.

1. **정적인 YAML 파일 + Launch 인자** 런치 파일에서 `parameters=[LaunchConfiguration('param_file')]` 형태로 YAML 파일 경로만 동적으로 바꾼다. YAML 파일 내부 값 자체는 고정되어 있다.
2. **템플릿 YAML 파일 + 런치 파일에서 템플릿 변환** 런치 파일에서 Python 코드를 통해 YAML 템플릿(예: `$ROBOT_NAME`, `$TOPIC_NAME` 등으로 변수 부분을 표시해 둔 YAML) 내용을 문자열 처리로 바꿔치기 한 다음, 임시 파일로 저장하여 노드에 넘긴다.
3. **동적으로 생성한 파라미터 Dictionary** YAML 파일 없이 런치 파일 내부에서 Python Dictionary 형태로 파라미터를 구성하고, 인자값으로부터 그 Dictionary를 유연하게 세팅한다. 이때 `Node(parameters=[my_param_dict])`처럼 지정하면 된다.

예시) Python Dictionary에 인자를 반영:

```python
robot_ns = LaunchConfiguration('robot_ns')
sensor_frequency = LaunchConfiguration('sensor_freq')

param_dict = {
    'robot_namespace': robot_ns,
    'sensor': {
        'frequency': sensor_frequency
    }
}

node_with_dict_params = Node(
    package='my_robot_bringup',
    executable='sensor_driver',
    name='sensor_driver',
    parameters=[param_dict]
)
```

런치 실행 시:

```bash
ros2 launch my_package param_dict_launch.py robot_ns:=my_robot sensor_freq:=50
```

그러면 노드는 `robot_namespace`와 `sensor.frequency` 값을 런치 인자에서 받은 값으로 초기화한다.

#### 상호 의존 인자 처리

두 인자가 서로 의존성이 있을 경우, 이를 처리하는 패턴이 필요하다. 예를 들어, `enable_sensor` 인자가 `true`일 때만 `sensor_type` 인자를 해석하도록 만들고 싶을 수 있다. 이런 경우 다음과 같이 OpaqueFunction이나 조건 액션을 사용해 처리한다.

```python
from launch.actions import OpaqueFunction

def validate_args(context, *args, **kwargs):
    enable_sensor = context.launch_configurations['enable_sensor']
    sensor_type = context.launch_configurations['sensor_type']
    if enable_sensor == 'true' and not sensor_type:
        # 에러 처리 혹은 기본값 대입
        raise RuntimeError('센서를 사용하려면 sensor_type 인자가 필요하다.')
    return []

check_args_action = OpaqueFunction(function=validate_args)
```

런치 파일이 실행되면 `check_args_action`이 먼저 수행되어, 런치 인자의 유효성을 검사하게 된다. 이 로직을 통해 잘못된 인자 조합을 사전에 방지할 수 있다.

#### Launch Configuration 간 연산

인자값들을 단순히 문자열만이 아니라, 수치연산으로도 처리하고 싶을 수 있다. 예를 들어, $x$, $y$ 좌표를 인자로 받아 특정 수학적 변환을 한 뒤 노드에 넘기는 경우다. 이때는 ROS2 Substitution 중 `PythonExpression`을 사용할 수 있다.

```python
from launch.substitutions import LaunchConfiguration, PythonExpression

x_arg = DeclareLaunchArgument('x_coord', default_value='0.0')
y_arg = DeclareLaunchArgument('y_coord', default_value='0.0')

distance_expr = PythonExpression([
    '(', LaunchConfiguration('x_coord'), ')**2 + (', LaunchConfiguration('y_coord'), ')**2'
])
# distance_expr = x^2 + y^2 (제곱합)

node_with_distance = Node(
    package='math_package',
    executable='distance_calculator',
    name='distance_calculator',
    parameters=[{'distance': distance_expr}]
)
```

위 예시에서 `distance_expr`는 인자값 $x$, $y$로부터 제곱합을 계산한 문자열로 구성된다. 런치가 실행되면 내부적으로 `eval()`을 통해 $distance$ 파라미터 값이 계산된 후, 노드에 전달된다.

만약 수학식이 조금 복잡하다면, 일반 파이썬 함수를 만들어 OpaqueFunction으로 처리하는 편이 더 가독성이 좋다. 예:

```python
def compute_distance(context, *args, **kwargs):
    x_val = float(context.launch_configurations['x_coord'])
    y_val = float(context.launch_configurations['y_coord'])
    distance = (x_val**2 + y_val**2)**0.5  # 유클리디안 거리
    return [SetLaunchConfiguration(name='distance', value=str(distance))]
```

이렇게 계산 후 `SetLaunchConfiguration`를 통해 런치 설정값을 갱신하면, 이후 노드나 액션들이 `distance` 값을 참조할 수 있다.

#### Launch Arguments와 Node Remapping

ROS2에서 토픽이나 서비스 이름을 리매핑(remap)할 때도 런치 인자를 사용할 수 있다. `Node` 액션의 `remappings` 인수에 `LaunchConfiguration`를 조합하면 동적으로 리매핑 경로를 지정할 수 있다.

```python
camera_topic_arg = DeclareLaunchArgument(
    'camera_topic',
    default_value='/camera/image_raw',
    description='카메라 토픽 이름'
)

node_camera_viewer = Node(
    package='image_tools',
    executable='showimage',
    name='camera_viewer',
    remappings=[('image', LaunchConfiguration('camera_topic'))]
)
```

여기서 런치 실행 시점에 `camera_topic` 인자를 바꾸면, `node_camera_viewer`에서 구독할 토픽 이름도 변경된다.

```bash
ros2 launch my_package camera_launch.py camera_topic:=/usb_cam/image_raw
```

또한 다중 리매핑을 적용할 때는 `remappings=[('old_name', 'new_name'), (...)]` 형태로 배열에 여러 쌍을 추가해 주고, 각각에 대해 `LaunchConfiguration` 혹은 `PythonExpression`, `IfCondition` 등을 섞어 활용할 수 있다.

#### 실전 예시: 로봇 시뮬레이션 전체 런치

마지막으로, Launch Arguments를 다수 활용해 복잡한 시뮬레이션 환경을 구성하는 예시를 간략히 살펴보자. 가령 다음과 같은 인자를 받는다고 하자.

* `world_file`: 시뮬레이터(가령 Gazebo)에서 사용할 월드 파일 경로
* `robot_model`: 스폰할 로봇 URDF 모델
* `gui`: GUI 사용 여부 (true/false)
* `use_rviz`: RViz 실행 여부
* `controller_type`: 제어기의 종류 (position, velocity 등)

런치 파일 내부에서는:

1. `$world_file`를 기반으로 `gazebo` 노드를 실행.
2. `$robot_model`을 Spawn 노드에 전달.
3. `$gui`가 `true`일 때만 GUI 관련 노드 실행.
4. `$use_rviz`가 `true`이면 RViz 노드를 Include.
5. `$controller_type`에 따라 controller.launch.py를 다르게 Include.

이러한 방식으로, 하나의 런치 파일에서 다양한 조합을 인자를 통해 핸들링할 수 있게 된다. 이를 통해 코드 중복을 최소화하고, 유지보수성을 높일 수 있다.
