흩어진 스크립트에서 로봇 소프트웨어로
앞 장에서 우리는 ROS2 개발 환경을 준비했다. 터미널을 열고 source /opt/ros/humble/setup.bash 같은 명령을 입력했으며, ROS2 명령어가 제대로 동작하는지도 확인했다. 이제부터는 한 단계 더 들어간다. 단순히 명령어를 실행하는 사용자가 아니라, ROS2 프로젝트를 만드는 개발자가 되어야 한다.
처음 로봇 프로그램을 만들 때는 파일 하나로 시작하기 쉽다. 예를 들어 move_robot.py라는 파이썬 파일을 만들고, 그 안에 센서 읽기, 장애물 판단, 속도 명령 전송, 로그 출력까지 전부 넣을 수 있다. 작은 장난감 예제라면 괜찮다. 하지만 로봇이 조금만 복잡해져도 이 방식은 금방 무너진다.
로봇은 혼자 움직이는 프로그램 하나가 아니다. 바퀴를 제어하는 코드가 있고, LiDAR 데이터를 읽는 코드가 있고, 지도 위에서 현재 위치를 추정하는 코드가 있으며, 목표 지점까지 경로를 계획하는 코드가 있다. 여기에 설정 파일, 로봇 모델 파일, Launch 파일, RViz 설정, Gazebo 시뮬레이션 파일까지 붙기 시작한다. 이 모든 것을 한 폴더에 마구 넣으면, 며칠 뒤에는 자신이 만든 프로젝트조차 미로처럼 느껴진다.
ROS2는 이 문제를 해결하기 위해 워크스페이스(workspace)와 패키지(package)라는 구조를 사용한다. 워크스페이스는 여러 ROS2 패키지를 모아 개발하고 빌드하고 테스트하는 작업 공간이다. 패키지는 관련된 노드, 설정, 리소스, 의존성을 묶는 재사용 단위다. 즉, 워크스페이스가 로봇 개발을 위한 작업장이라면, 패키지는 그 안에 놓인 공구 상자다.
이 장의 목표는 명확하다. 앞으로 우리가 만들 자율주행 로봇 프로젝트를 단발성 스크립트 모음이 아니라, 재사용 가능하고 확장 가능한 ROS2 소프트웨어로 구성하는 방법을 익히는 것이다.

워크스페이스란 무엇인가
ROS2 워크스페이스는 ROS2 패키지를 모아 두는 독립적인 작업 디렉터리다. 이 디렉터리 안에서 패키지를 만들고, 외부 패키지를 가져오고, colcon으로 빌드하고, 빌드된 결과를 실행 환경에 연결한다. ROS2 공식 문서에서도 워크스페이스를 ROS2 패키지를 포함하는 디렉터리로 설명하며, 개발과 테스트를 위해 기존 ROS2 설치 환경 위에 새로운 작업 공간을 올려 사용하는 방식을 안내한다.1
가장 흔한 워크스페이스 이름은 ros2_ws다. 꼭 이 이름이어야 하는 것은 아니지만, 실습과 문서에서 자주 등장하므로 초반에는 그대로 따라가는 편이 좋다.
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws여기서 중요한 부분은 src 디렉터리다. ROS2 패키지의 소스 코드는 보통 워크스페이스 루트 바로 아래가 아니라 src 안에 둔다. 처음 만든 워크스페이스는 이렇게 단순하다.
ros2_ws/
└── src/아직 로봇은 움직이지 않는다. 노드도 없고, 토픽도 없다. 하지만 이 작은 구조가 앞으로 우리가 만들 모든 로봇 소프트웨어의 출발점이 된다.
워크스페이스를 이해할 때 가장 먼저 버려야 할 생각이 있다. 워크스페이스는 단순히 "코드를 모아 두는 폴더"가 아니다. 코드를 빌드하고, 빌드 결과를 설치하고, 실행 환경에 연결하는 흐름 전체를 담는 공간이다. 그래서 워크스페이스는 소스 코드뿐 아니라 빌드 중간 결과, 설치 결과, 로그까지 함께 관리한다.
Underlay와 Overlay: 기존 ROS2를 건드리지 않는 방법
ROS2를 설치하면 /opt/ros/humble, /opt/ros/jazzy 같은 위치에 기본 패키지들이 들어간다. 이 기본 설치 환경을 보통 underlay라고 부른다. 말 그대로 아래에 깔려 있는 기반 환경이다.
우리가 새로 만드는 ~/ros2_ws는 이 기반 환경 위에 올라가는 overlay가 된다. overlay는 기존 ROS2 설치를 직접 수정하지 않고도 새 패키지를 추가하거나 기존 패키지를 실험적으로 바꿔 볼 수 있게 해준다.
비유하자면 underlay는 건물의 콘크리트 기초이고, overlay는 그 위에 올리는 작업대다. 작업대에서 나사를 조이고 부품을 바꾸다가 실수하더라도 건물 기초를 부수지는 않는다. ROS2 개발에서 이 분리는 매우 중요하다.
기본 흐름은 다음과 같다.
source /opt/ros/humble/setup.bash
cd ~/ros2_ws
source install/setup.bash첫 번째 source는 기본 ROS2 설치 환경을 현재 터미널에 불러온다. 두 번째 source는 우리가 만든 워크스페이스의 빌드 결과를 현재 터미널에 추가한다. 이렇게 하면 터미널은 기본 ROS2 패키지와 내가 만든 패키지를 함께 찾을 수 있다.
공식 문서에서는 기존 ROS2 설치를 underlay로 두고, 새 워크스페이스를 overlay로 source하여 개발과 테스트를 진행하는 방식을 설명한다. 이때 overlay에 있는 패키지는 underlay에 있는 같은 이름의 패키지보다 우선할 수 있다.1 이 특성 덕분에 기본 설치를 망가뜨리지 않고도 특정 패키지를 수정해 실험할 수 있다.
예를 들어 /opt/ros/humble에 이미 turtlesim이 설치되어 있다고 하자. 그런데 ~/ros2_ws/src에 turtlesim 소스 코드를 가져와 수정한 뒤 빌드하고 overlay로 source하면, 터미널은 기본 설치의 turtlesim보다 워크스페이스 안의 turtlesim을 먼저 사용할 수 있다. 실험이 끝나면 새 터미널을 열고 기본 ROS2만 source하면 원래 환경으로 돌아갈 수 있다.
이 방식은 자율주행 로봇 프로젝트에서도 자주 쓰인다. 예를 들어 Nav2 설정을 바꿔 실험하거나, 센서 드라이버를 수정하거나, 특정 로봇 모델을 위한 패키지를 추가할 때 기본 ROS2 설치를 직접 건드리지 않는다. 모든 실험은 내 워크스페이스 안에서 한다.
워크스페이스의 네 가지 디렉터리
처음 만든 ros2_ws에는 src만 있다. 하지만 colcon build를 실행하면 새로운 디렉터리가 생긴다.
cd ~/ros2_ws
colcon build빌드가 끝나면 워크스페이스는 보통 다음과 같은 구조가 된다.
ros2_ws/
├── build/
├── install/
├── log/
└── src/이 네 디렉터리는 ROS2 개발 흐름을 이해하는 핵심이다.
src: 패키지 소스가 들어가는 곳
src는 우리가 직접 작성하거나 외부에서 가져온 ROS2 패키지의 소스 코드가 들어가는 곳이다. 앞으로 만들 Python 패키지, C++ 패키지, 로봇 모델 패키지, 시뮬레이션 패키지, Nav2 설정 패키지 모두 여기에 놓인다.
예를 들어 다음과 같은 구조가 될 수 있다.
ros2_ws/
└── src/
├── my_robot_bringup/
├── my_robot_description/
├── my_robot_simulation/
└── my_robot_navigation/처음에는 src 안에 패키지가 하나뿐일 수 있다. 하지만 책의 후반부로 갈수록 이 공간은 로봇 시스템의 부품 창고처럼 점점 풍성해진다.
build: 빌드 중간 산출물이 생기는 곳
build는 colcon이 패키지를 빌드하는 과정에서 사용하는 중간 파일이 저장되는 곳이다. C++ 패키지를 빌드할 때 CMake가 생성하는 파일, 컴파일 과정에서 생기는 임시 산출물 등이 여기에 들어간다. 패키지마다 별도의 하위 디렉터리가 만들어진다.
개발자가 직접 build 안의 파일을 수정하는 경우는 거의 없다. 빌드가 꼬였을 때 build, install, log를 지우고 다시 빌드하는 경우는 있지만, 평소에는 colcon에게 맡겨 두는 공간이라고 생각하면 된다.
install: 실행에 필요한 결과물이 설치되는 곳
install은 빌드된 패키지가 실제 실행 가능한 형태로 설치되는 곳이다. Python 노드의 실행 진입점, C++ 실행 파일, 라이브러리, 설정 파일, Launch 파일, 패키지 메타데이터 등이 이곳에 정리된다.
우리가 source install/setup.bash를 실행하는 이유도 바로 이 때문이다. ROS2 명령어가 새로 만든 패키지와 실행 파일을 찾으려면, install에 있는 설정 파일을 통해 환경 변수를 업데이트해야 한다.
빌드에 성공했는데도 다음 명령이 실패한다면 대부분 source를 잊은 것이다.
ros2 run my_package my_node이런 경우에는 워크스페이스 루트에서 다음 명령을 다시 실행한다.
source install/setup.bashlog: 빌드와 테스트 기록이 남는 곳
log는 colcon build나 colcon test를 실행했을 때 생기는 로그가 저장되는 곳이다. 빌드가 실패했을 때 터미널 출력만 보고 원인을 찾기 어려울 수 있다. 이때 log 디렉터리에 남은 기록을 확인하면 어느 패키지에서 어떤 오류가 발생했는지 더 자세히 볼 수 있다.
공식 colcon 문서에서도 build, install, log 디렉터리가 src와 같은 레벨에 생성되며, 각각 중간 파일, 설치 결과, 로그 정보를 담는다고 설명한다.2
초보자에게는 이 네 디렉터리가 낯설게 보일 수 있다. 하지만 기억할 것은 간단하다. 내가 코드를 쓰는 곳은 src, ROS2가 빌드하며 사용하는 곳은 build, 실행할 결과가 놓이는 곳은 install, 문제가 생겼을 때 흔적을 찾는 곳은 log다.
colcon: 워크스페이스를 빌드하는 도구
ROS2에서 워크스페이스를 빌드할 때 사용하는 대표 도구가 colcon이다. colcon은 워크스페이스 안의 패키지를 찾아 의존성 순서에 맞게 빌드해 준다. 패키지가 하나일 때는 단순해 보이지만, 패키지가 열 개, 스무 개로 늘어나면 빌드 순서를 사람이 직접 관리하기 어렵다. colcon은 이 작업을 대신한다.
먼저 설치되어 있지 않다면 다음 명령으로 설치한다.
sudo apt install python3-colcon-common-extensions워크스페이스 루트에서 빌드한다.
cd ~/ros2_ws
colcon build주의할 점은 워크스페이스 루트에서 실행해야 한다는 것이다. ~/ros2_ws/src 안에서 실행하는 것이 아니라, src의 상위 디렉터리인 ~/ros2_ws에서 실행한다.
개발 중에는 다음 옵션을 자주 사용한다.
colcon build --symlink-install--symlink-install은 특히 Python 코드나 설정 파일처럼 컴파일이 필요 없는 파일을 자주 수정할 때 유용하다. 설치 공간에 파일을 복사하는 대신 심볼릭 링크를 사용해, 수정 후 반복 빌드 부담을 줄여 준다. ROS2 공식 문서에서도 이 옵션을 Python 스크립트나 컴파일되지 않는 리소스를 빠르게 반복 수정할 때 유용한 옵션으로 소개한다.2
패키지가 많아지면 전체 워크스페이스를 매번 빌드하는 것도 부담스럽다. 이때는 특정 패키지만 빌드할 수 있다.
colcon build --packages-select my_robot_controller또는 특정 패키지와 그 패키지가 의존하는 패키지까지 빌드할 수 있다.
colcon build --packages-up-to my_robot_navigation--packages-select는 "이 패키지만 빌드해"에 가깝고, --packages-up-to는 "이 패키지를 쓰기 위해 필요한 것까지 빌드해"에 가깝다. 작은 프로젝트에서는 차이가 크게 느껴지지 않지만, Nav2나 시뮬레이션 패키지처럼 의존성이 많은 프로젝트에서는 빌드 시간을 줄이는 데 도움이 된다.
빌드 후에는 반드시 source한다
ROS2 초보자가 가장 자주 만나는 문제 중 하나가 이것이다.
Package 'my_package' not found분명히 패키지를 만들었고, colcon build도 성공했는데 ROS2가 패키지를 찾지 못한다. 대부분 원인은 간단하다. 빌드한 워크스페이스를 현재 터미널에 source하지 않았기 때문이다.
빌드 후에는 다음 명령을 실행한다.
cd ~/ros2_ws
source install/setup.bash그러면 현재 터미널에서 ros2 run, ros2 launch, ros2 pkg list 같은 명령이 워크스페이스 안의 패키지를 찾을 수 있다.
확인해 보자.
ros2 pkg list | grep my_package패키지가 보이면 현재 터미널 환경에 잘 연결된 것이다.
여기서 setup.bash와 local_setup.bash의 차이를 가볍게 짚고 넘어가자.
source install/setup.bash
source install/local_setup.bashsetup.bash는 해당 워크스페이스와 그 워크스페이스가 만들어질 때 연결되어 있던 underlay 환경까지 함께 고려한다. 반면 local_setup.bash는 현재 overlay 자체의 설정을 추가하는 데 초점이 있다. 공식 문서에서는 underlay를 먼저 source한 뒤 overlay의 local_setup.bash를 source하는 흐름과, overlay의 setup.bash를 source하는 방식의 관계를 설명한다.1
초반 실습에서는 다음처럼 기억하면 충분하다.
source /opt/ros/humble/setup.bash
cd ~/ros2_ws
source install/setup.bash여러 워크스페이스를 동시에 다루게 되면 어떤 워크스페이스를 source했는지 헷갈릴 수 있다. 예를 들어 slam_ws, nav2_ws, my_robot_ws를 모두 .bashrc에 넣어 자동 source하면, 나중에 어떤 패키지가 어디서 실행되는지 추적하기 어려워질 수 있다. 처음에는 편의를 위해 .bashrc에 추가할 수 있지만, 프로젝트가 많아지면 필요한 워크스페이스만 명시적으로 source하는 습관도 중요하다.
패키지란 무엇인가
워크스페이스가 작업장이라면 패키지는 작업장 안의 공구 상자다. 하나의 패키지는 관련된 노드, 라이브러리, 설정 파일, Launch 파일, 메시지 정의, 테스트, 리소스를 묶는다.
예를 들어 바퀴 속도를 제어하는 노드, 모터 드라이버 설정, 관련 테스트를 하나의 my_robot_controller 패키지에 넣을 수 있다. 로봇의 URDF와 xacro 파일은 my_robot_description 패키지에 넣을 수 있다. Gazebo 월드와 센서 시뮬레이션 설정은 my_robot_simulation 패키지에 넣을 수 있다.
패키지는 ROS2에서 재사용과 의존성 관리의 단위다. "이 노드를 실행하려면 어떤 라이브러리가 필요한가?", "이 설정 파일은 어떤 로봇 모델에 속하는가?", "이 패키지를 다른 프로젝트에서도 쓸 수 있는가?" 같은 질문에 답하기 위해 패키지 구조가 필요하다.
새 Python 패키지를 만들어 보자.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python my_robot_controller --dependencies rclpy geometry_msgs이 명령은 my_robot_controller라는 ROS2 패키지를 만든다. --build-type ament_python은 Python 패키지로 만들겠다는 뜻이고, --dependencies rclpy geometry_msgs는 이 패키지가 rclpy와 geometry_msgs에 의존한다는 뜻이다.
생성된 구조는 대략 다음과 비슷하다.
my_robot_controller/
├── my_robot_controller/
│ └── __init__.py
├── package.xml
├── resource/
│ └── my_robot_controller
├── setup.cfg
├── setup.py
└── test/아직 로봇을 움직이는 코드는 없지만, ROS2 패키지로서 필요한 기본 뼈대가 만들어졌다.
package.xml: 패키지의 신분증
모든 ROS2 패키지에는 package.xml이 있다. 이 파일은 패키지의 신분증과 같다. 패키지 이름, 버전, 설명, 관리자, 라이선스, 의존성 같은 정보를 선언한다.
예를 들어 my_robot_controller/package.xml은 다음과 비슷한 형태를 가진다.
<?xml version="1.0"?>
<package format="3">
<name>my_robot_controller</name>
<version>0.0.0</version>
<description>Controller package for my mobile robot</description>
<maintainer email="[email protected]">robot developer</maintainer>
<license>Apache-2.0</license>
<depend>rclpy</depend>
<depend>geometry_msgs</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>여기서 중요한 부분은 의존성이다.
<depend>rclpy</depend>
<depend>geometry_msgs</depend>rclpy는 Python으로 ROS2 노드를 만들 때 사용하는 클라이언트 라이브러리다. geometry_msgs는 속도 명령인 Twist 같은 로봇 기하 메시지를 제공하는 패키지다. 나중에 우리가 /cmd_vel 토픽으로 속도 명령을 보낼 때 geometry_msgs/msg/Twist를 사용하게 된다.
의존성을 package.xml에 적어 두면, 다른 개발자가 이 패키지를 가져갔을 때 무엇이 필요한지 알 수 있다. 또한 rosdep 같은 도구가 package.xml을 읽고 필요한 시스템 의존성을 설치하는 데 도움을 줄 수 있다. 공식 워크스페이스 튜토리얼에서도 패키지 의존성이 package.xml에 선언되며, rosdep이 이 선언을 따라 누락된 의존성을 확인한다고 설명한다.1
초보자는 처음에 의존성을 대충 넘기기 쉽다. 코드에서 import rclpy를 했는데 package.xml에 rclpy를 적지 않거나, geometry_msgs를 사용하면서 의존성을 빠뜨리는 식이다. 내 컴퓨터에서는 우연히 동작할 수 있지만, 다른 컴퓨터나 CI 환경에서는 빌드가 실패할 수 있다. 로봇 개발에서는 이런 작은 누락이 현장에서 큰 시간을 잡아먹는다.
패키지를 만들 때는 습관처럼 질문하자.
"이 패키지는 무엇에 의존하는가?"
ament_python 패키지와 setup.py
ROS2에서 Python 패키지를 만들 때는 보통 ament_python 빌드 타입을 사용한다. Python 노드는 컴파일하지 않는다. 대신 setup.py를 통해 어떤 Python 모듈을 설치할지, 어떤 함수를 실행 파일처럼 등록할지 정의한다.
예를 들어 setup.py에는 다음과 같은 부분이 있다.
from setuptools import find_packages, setup
package_name = 'my_robot_controller'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='robot developer',
maintainer_email='[email protected]',
description='Controller package for my mobile robot',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'simple_controller = my_robot_controller.simple_controller:main',
],
},
)가장 눈여겨볼 곳은 entry_points다.
'simple_controller = my_robot_controller.simple_controller:main'이 한 줄은 ROS2에게 이렇게 알려준다.
"simple_controller라는 실행 이름으로 호출하면, my_robot_controller 패키지 안의 simple_controller.py 파일에서 main 함수를 실행해."
그래서 빌드와 source가 끝난 뒤 다음 명령을 사용할 수 있다.
ros2 run my_robot_controller simple_controller여기서 simple_controller는 Python 파일 이름 자체가 아니라, setup.py의 entry_points에 등록한 실행 이름이다. 파일을 만들었는데 ros2 run에서 보이지 않는다면 entry_points를 확인해야 한다.
Python 패키지에서 자주 쓰는 디렉터리도 함께 기억해 두자.
my_robot_controller/
├── my_robot_controller/
│ ├── __init__.py
│ └── simple_controller.py
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
└── test/Python 소스 파일은 패키지 이름과 같은 디렉터리 안에 둔다. 위 예시에서는 my_robot_controller/my_robot_controller/simple_controller.py가 된다. 처음 보면 같은 이름이 두 번 반복되어 이상해 보이지만, 바깥쪽은 ROS2 패키지 디렉터리이고 안쪽은 Python 모듈 디렉터리라고 이해하면 된다.
ament_cmake 패키지와 CMakeLists.txt
C++ 노드를 만들 때는 보통 ament_cmake 빌드 타입을 사용한다. C++ 코드는 컴파일이 필요하므로, 어떤 소스 파일을 빌드할지, 어떤 라이브러리에 의존할지, 결과 실행 파일을 어디에 설치할지 CMakeLists.txt에 정의한다.
C++ 패키지는 다음 명령으로 만들 수 있다.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_robot_cpp_controller --dependencies rclcpp geometry_msgs생성된 구조는 대략 다음과 같다.
my_robot_cpp_controller/
├── CMakeLists.txt
├── include/
│ └── my_robot_cpp_controller/
├── package.xml
└── src/Python 패키지와 비교하면 setup.py 대신 CMakeLists.txt가 중심에 있다. C++ 소스 파일은 보통 src에 두고, 헤더 파일은 include/패키지명 아래에 둔다.
간단한 CMake 설정은 다음과 같은 형태를 가진다.
cmake_minimum_required(VERSION 3.8)
project(my_robot_cpp_controller)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
add_executable(simple_cpp_controller src/simple_cpp_controller.cpp)
ament_target_dependencies(simple_cpp_controller rclcpp geometry_msgs)
install(TARGETS
simple_cpp_controller
DESTINATION lib/${PROJECT_NAME}
)
ament_package()여기서 add_executable은 어떤 C++ 파일을 실행 파일로 만들지 정의한다. ament_target_dependencies는 이 실행 파일이 어떤 ROS2 패키지에 의존하는지 알려준다. install 규칙은 빌드된 실행 파일을 ROS2가 찾을 수 있는 위치에 설치한다.
빌드 후에는 Python 패키지와 마찬가지로 source한 뒤 실행한다.
cd ~/ros2_ws
colcon build --packages-select my_robot_cpp_controller
source install/setup.bash
ros2 run my_robot_cpp_controller simple_cpp_controller공식 colcon 문서에서는 ROS2 패키지의 권장 빌드 타입으로 ament_cmake와 ament_python을 소개하며, Python 패키지는 setup.py를 주요 진입점으로 사용하고 C++ 패키지는 CMake 기반 빌드 방식을 사용한다고 설명한다.2
정리하면 이렇다. Python 노드를 만들면 ament_python, setup.py, entry_points를 기억한다. C++ 노드를 만들면 ament_cmake, CMakeLists.txt, add_executable, install을 기억한다.
첫 번째 빈 패키지를 빌드해 보기
이제 실제 흐름을 한 번 연결해 보자. 아직 복잡한 노드를 만들 필요는 없다. 패키지를 만들고, 빌드하고, source하고, ROS2가 패키지를 인식하는지 확인하는 것만으로도 충분하다.
먼저 ROS2 기본 환경을 source한다.
source /opt/ros/humble/setup.bash워크스페이스를 만든다.
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/srcPython 패키지를 만든다.
ros2 pkg create --build-type ament_python my_robot_controller --dependencies rclpy geometry_msgs워크스페이스 루트로 이동해 빌드한다.
cd ~/ros2_ws
colcon build --symlink-install빌드가 끝나면 source한다.
source install/setup.bash패키지가 보이는지 확인한다.
ros2 pkg list | grep my_robot_controller출력에 my_robot_controller가 보이면 성공이다. 아직 아무 노드도 실행하지 않았지만, 우리는 이미 중요한 일을 했다. 단발성 파일이 아니라 ROS2가 인식할 수 있는 패키지 단위의 소프트웨어를 만든 것이다.
여기서 터미널을 새로 열면 다시 source해야 한다. 새 터미널은 이전 터미널의 환경 변수를 이어받지 않기 때문이다.
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash매번 입력하기 번거롭다면 .bashrc에 추가할 수 있다.
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc
source ~/.bashrc다만 여러 워크스페이스를 사용한다면 신중해야 한다. 자동 source는 편하지만, 잘못된 워크스페이스가 먼저 잡혀 예상과 다른 패키지가 실행될 수 있다. 실습 초반에는 편의성이 크고, 프로젝트가 늘어나면 명시적으로 source하는 방식이 더 안전할 때도 있다.
로봇 프로젝트를 패키지로 나누는 기준
이제 중요한 질문이 남는다.
"패키지는 어떤 기준으로 나눠야 할까?"
처음에는 모든 것을 하나의 패키지에 넣고 싶어진다. my_robot이라는 패키지 하나를 만들고, 그 안에 노드, URDF, Gazebo 월드, Nav2 설정, Launch 파일을 모두 넣는 방식이다. 작은 실습에서는 가능하다. 하지만 자율주행 로봇 프로젝트에서는 곧 한계가 온다.
좋은 패키지 분리는 기능과 책임을 기준으로 한다. 자율주행 이동 로봇에서는 보통 다음과 같은 구조를 생각할 수 있다.
my_robot_ws/
└── src/
├── my_robot_bringup/
├── my_robot_description/
├── my_robot_simulation/
├── my_robot_navigation/
├── my_robot_slam/
└── my_robot_controller/my_robot_bringup은 로봇 시스템을 실제로 실행하는 Launch 파일을 담는다. 여러 노드를 한 번에 켜고, 필요한 파라미터 파일을 연결하고, 실제 로봇 모드와 시뮬레이션 모드를 구분하는 역할을 맡는다. "로봇을 켠다"는 관점의 패키지다.
my_robot_description은 로봇의 형태를 설명하는 URDF나 xacro 파일을 담는다. 바퀴가 어디에 붙어 있는지, LiDAR가 어느 높이에 있는지, 카메라 좌표계가 어디인지 같은 정보가 여기에 들어간다. 이 패키지는 RViz, Gazebo, TF와 깊이 연결된다.
my_robot_simulation은 Gazebo 월드, 시뮬레이션용 센서 설정, 가상 로봇 실행 Launch 파일을 담는다. 실제 로봇 없이 실험할 때 사용한다. 현실에서 로봇이 벽에 부딪히면 마음이 아프지만, 시뮬레이션에서는 다시 실행하면 된다.
my_robot_navigation은 Nav2 설정을 담는다. 전역 costmap, 지역 costmap, planner, controller, behavior tree 설정 파일이 여기에 들어갈 수 있다. 목표 지점까지 이동하는 자율주행의 핵심 설정이 모이는 곳이다.
my_robot_slam은 SLAM 실행과 지도 저장 관련 Launch 파일, SLAM 파라미터를 담을 수 있다. 지도 작성과 위치 추정을 분리해서 관리하면, 나중에 SLAM 알고리즘을 바꾸거나 파라미터를 조정할 때 편하다.
my_robot_controller는 바퀴 제어, 속도 제한, 간단한 제어 노드 등을 담을 수 있다. 실제 하드웨어와 가까운 코드가 여기에 들어간다.
이렇게 나누면 처음에는 폴더가 많아져 복잡해 보인다. 하지만 시간이 지날수록 장점이 커진다. 로봇 모델만 다른 프로젝트에서 재사용할 수 있고, 시뮬레이션 설정만 따로 고칠 수 있으며, Nav2 설정을 실험하다가도 하드웨어 제어 코드를 건드리지 않을 수 있다.
좋은 패키지 분리는 로봇 개발자의 보험과 같다. 당장은 조금 귀찮지만, 프로젝트가 커졌을 때 문제를 빠르게 찾고 안전하게 수정할 수 있게 해준다.
패키지 안에는 무엇을 둘 수 있을까
패키지 안에는 노드 코드만 들어가는 것이 아니다. ROS2 프로젝트가 커질수록 패키지는 다양한 리소스를 담는다.
my_robot_navigation/
├── config/
│ └── nav2_params.yaml
├── launch/
│ └── navigation.launch.py
├── maps/
│ ├── office.yaml
│ └── office.pgm
├── package.xml
├── setup.py 또는 CMakeLists.txt
└── resource/config에는 YAML 설정 파일을 둔다. Nav2 파라미터, 센서 필터 설정, 제어기 설정 등이 들어갈 수 있다.
launch에는 여러 노드를 한 번에 실행하는 Launch 파일을 둔다. 로봇 프로젝트에서는 노드를 하나씩 손으로 실행하는 일이 거의 없다. Launch 파일로 전체 시스템을 켜고 끄는 방식이 기본이 된다.
maps에는 SLAM으로 만든 지도 파일을 둘 수 있다. 지도는 이미지 파일과 메타데이터 YAML 파일로 구성되는 경우가 많다.
urdf나 description 관련 패키지에는 로봇 모델 파일을 둔다. rviz 디렉터리에는 RViz 설정 파일을 둘 수 있고, worlds에는 Gazebo 월드 파일을 둘 수 있다.
중요한 것은 "아무 데나 넣지 않는 것"이다. 설정은 config, 실행 구성은 launch, 지도는 maps, 로봇 모델은 urdf 또는 description 성격의 패키지에 둔다. 이렇게 하면 다른 사람이 프로젝트를 열었을 때도 길을 잃지 않는다. 미래의 나도 마찬가지다.
자주 만나는 실수들
ROS2 워크스페이스와 패키지를 처음 다룰 때는 비슷한 실수를 반복하게 된다. 실수 자체는 나쁘지 않다. 다만 원인을 빨리 알아차리는 것이 중요하다.
첫 번째 실수는 src 안에서 colcon build를 실행하는 것이다. 빌드는 워크스페이스 루트에서 해야 한다.
cd ~/ros2_ws
colcon build두 번째 실수는 빌드 후 source를 잊는 것이다. 패키지를 만들거나 실행 파일을 추가했다면 빌드 후 다음 명령을 실행해야 한다.
source ~/ros2_ws/install/setup.bash세 번째 실수는 Python 노드를 만들고 setup.py의 entry_points에 등록하지 않는 것이다. 파일은 존재하지만 ros2 run이 실행 이름을 모르면 실행할 수 없다.
네 번째 실수는 의존성을 package.xml에 적지 않는 것이다. 코드에서 rclpy, std_msgs, geometry_msgs를 사용한다면 패키지 의존성에도 반영해야 한다.
다섯 번째 실수는 여러 워크스페이스를 동시에 source해 놓고 어떤 패키지가 실행되는지 헷갈리는 것이다. 이럴 때는 새 터미널을 열고 필요한 underlay와 overlay만 차례로 source하는 것이 가장 깔끔하다.
문제가 생겼을 때는 당황하지 말고 세 가지를 확인하자. 현재 위치가 워크스페이스 루트인지, 빌드가 성공했는지, 현재 터미널에서 올바른 setup.bash를 source했는지. 이 세 가지만 확인해도 초반 문제의 상당수가 해결된다.
작은 구조가 큰 로봇을 만든다
이 장에서 배운 내용은 겉으로 보기에는 화려하지 않다. 로봇이 움직이지도 않았고, LiDAR가 돌아가지도 않았고, 지도도 만들지 않았다. 하지만 지금 배운 워크스페이스와 패키지 구조는 앞으로 모든 실습의 바닥이 된다.
ROS2 프로젝트는 워크스페이스 안에서 자란다. 패키지는 기능별로 나뉘고, colcon은 그 패키지들을 빌드하며, install/setup.bash는 빌드 결과를 실행 환경에 연결한다. package.xml은 패키지의 정체성과 의존성을 설명하고, setup.py나 CMakeLists.txt는 Python과 C++ 코드가 어떻게 실행 가능한 형태가 되는지 정의한다.
자율주행 로봇은 거대한 하나의 프로그램이 아니다. 잘 나뉜 작은 패키지들이 토픽, 서비스, 액션, TF를 통해 협력하는 시스템이다. 워크스페이스와 패키지 구조를 이해하면, 이제 우리는 로봇 소프트웨어를 "파일 하나"가 아니라 "시스템"으로 바라볼 준비를 갖추게 된다.
다음 장에서는 ROS2 명령어를 본격적으로 익힌다. 지금 만든 패키지와 앞으로 실행할 노드들이 ROS2 그래프 안에서 어떻게 보이는지, 토픽과 서비스와 파라미터를 어떻게 관찰하는지 살펴볼 것이다. 터미널은 더 이상 단순한 검은 화면이 아니다. 로봇의 내부를 들여다보는 계기판이 될 것이다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.