실험실 안전수칙
이론 강의로봇 연구실은 일반 소프트웨어 개발실과 다릅니다. 실제로 움직이는 하드웨어, 배터리, 전원이 있고, 실수는 장비가 아니라 사람을 다치게 할 수 있습니다. 그래서 기술 환경 셋업보다 먼저 이 페이지부터 시작합니다.
먼저 읽을 것
DOC DOC두 번째 링크는 실제 대학 로봇 연구실이 공개한 안전수칙입니다. 우리 연구실에도 별도의 안전 교육이나 문서가 있다면 반드시 함께 확인하세요 — 이 페이지는 일반 원칙만 다룹니다.
핵심 원칙 5가지
- 항상 손이 닿는 곳에 E-stop을 둔다 — 새로 작성한 코드를 처음 실행할 때는 반드시 비상정지 버튼에 손을 올린 채로 로봇을 지켜봅니다.
- 리튬 배터리는 화재 위험 물질로 다룬다 — 부풀거나 손상된 배터리는 절대 사용하지 않고, 충전 중에는 자리를 비우지 않습니다.
- 로봇이 움직이는 동안 작업 공간에 들어가지 않는다 — 특히 자율 동작 모드에서는 더 엄격하게 지킵니다.
- 전원을 켜기 전에 주변을 정리한다 — 케이블, 공구, 사람의 손이 로봇 가동 범위 안에 있지 않은지 확인합니다.
- 이상 상황이 생기면 먼저 정지, 그다음 원인 파악 — 당황해서 같은 명령을 반복해서 보내지 않습니다.
Ctrl+C나 소프트웨어 종료 버튼은 노트북이 멈추거나 네트워크가 끊기면 함께 무력화됩니다. 실제 하드웨어를 다룰 때는 반드시 물리적인 E-stop 버튼을 손이 닿는 곳에 두고, 데모나 첫 실행 전에 정지 방법이 실제로 작동하는지 미리 테스트해두세요.
ISO 10218, 협동로봇(코봇) 관련해서는 ISO/TS 15066이 널리 쓰입니다. 지금 당장 표준 문서를 다 읽을 필요는 없지만, 연구실에서 산업용 로봇 팔을 다루게 된다면 이런 표준이 존재한다는 것 정도는 알아두세요.
간단한 퀴즈
1. 새로 작성한 코드를 로봇에서 처음 실행할 때, 가장 먼저 해야 할 일은?
2. 키보드 인터럽트(Ctrl+C)에 대한 설명으로 옳은 것은?
3. 리튬 배터리를 다룰 때의 안전 원칙으로 옳은 것은?
로봇을 위한 수학 기초
이론 강의이 온보딩 전체에서 계속 마주칠 두 가지 수학 언어를 미리 짚고 갑니다. 선형대수는 로봇의 위치·자세·힘을 표현하는 문법이고, 미분방정식은 로봇이 시간에 따라 어떻게 움직이는지를 표현하는 문법입니다. Level 3의 기구학·자코비안·MuJoCo 페이지가 전부 이 위에 서 있습니다.
선형대수학 — 로봇에서 어떻게 쓰이나
로봇의 위치는 벡터, 회전은 행렬로 표현됩니다. 관절을 하나 돌리면 손끝이 어디로 가는지 계산하는 것(순기구학)도, 결국 벡터에 행렬을 곱하는 일입니다.
import numpy as np # 로봇이 90도 회전했을 때, "앞" 방향 벡터가 월드 좌표계에서 어디를 가리키는지 theta = np.deg2rad(90) R = np.array([ [np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)], ]) v_local = np.array([1, 0]) # 로봇 기준 "앞" v_world = R @ v_local # 회전행렬을 곱해 월드 좌표로 변환 print(v_world) # [0, 1] — 앞이 90도 돌아 월드 y축과 일치
여러 좌표계를 잇는 변환도 마찬가지입니다 — T_world_link1 = T_world_base @ T_base_link1처럼, 변환 행렬을 순서대로 곱하기만 하면 됩니다. Level 3의 URDF·TF 페이지에서 보게 될 좌표계 트리가 바로 이 곱셈 체인입니다.
미분방정식 — 로봇에서 어떻게 쓰이나
뉴턴의 운동법칙 F = ma에서, 가속도 a는 위치를 시간에 대해 두 번 미분한 값입니다. 그래서 로봇의 움직임을 정확히 기술하려면 미분방정식을 풀어야 합니다.
# 스프링-댐퍼 모델: 관절의 탄성·감쇠를 나타내는 2차 미분방정식 # m*x'' + c*x' + k*x = 0 (외력 없이 놓아뒀을 때) m, c, k = 1.0, 0.5, 4.0 # 질량, 감쇠, 강성 x, v = 1.0, 0.0 # 초기 위치, 속도 dt = 0.01 for step in range(500): a = -(c * v + k * x) / m # F=ma 에서 가속도를 구함 v += a * dt # 속도 = 가속도를 적분 x += v * dt # 위치 = 속도를 적분
이 오일러 적분(작은 시간 간격 dt만큼 조금씩 앞으로 계산해나가는 방식)이 바로 Level 3에서 볼 MuJoCo의 mj_step이 내부적으로 하는 일과 같은 개념입니다. 미분방정식을 손으로 풀 수 없어도, 이렇게 수치적으로 한 스텝씩 풀어나갈 수 있습니다.
로봇운동학 (Kinematics / Screw Theory) — 더 깊이 들어가고 싶다면
Level 3에서 다룰 순기구학·역기구학·자코비안은, 회전과 이동을 각각 따로 다루는 대신 하나의 통합된 수학적 틀로 묶어서 다루는 나사이론(Screw Theory) 위에서 훨씬 깔끔하게 정리됩니다. 아래 Modern Robotics 교재가 이 접근을 가장 널리 쓰이는 방식으로 정리해둔 무료 자료입니다.
참고 강의 · 교재
KO EN EN DOC PDF이 강의들은 온보딩 실습과 병행해서 여유 있을 때 조금씩 보시면 됩니다 — Level 3에 도달할 때쯤 한 번 더 필요한 부분만 되짚어보는 것도 좋은 방법입니다.
간단한 퀴즈
1. 벡터에 회전행렬을 곱하는 것은 무엇을 의미하나요?
2. F = ma가 미분방정식인 이유는?
3. 미분방정식을 라플라스 변환으로 주파수 영역으로 옮기는 이유는?
Ubuntu 환경 구축
실습 가이드본격적인 실습에 들어가기 전에 우분투부터 갖춥니다. 설치하고, 최신 상태로 만들고, 터미널이 잘 열리는지, 기본 개발 도구들이 잘 잡히는지 확인합니다. 에디터(VS Code) 연결은 바로 다음 페이지에서 다룹니다.
- Ubuntu를 설치하고 최신 상태로 업데이트할 수 있다
- 터미널을 열고 기본 개발 도구가 잘 설치되어 있는지 확인할 수 있다
설치와 업데이트
윈도우를 쓴다면 WSL2 위에 Ubuntu를 설치하는 것이 가장 간단합니다. PowerShell을 관리자 권한으로 열고:
PS> wsl --install # 재부팅 후 Ubuntu가 자동 실행되며, 최초 1회 리눅스 사용자 계정을 만듭니다 PS> wsl -l -v # VERSION이 2인지 확인
이미 Ubuntu가 있다면 설치는 건너뛰고, 최신 상태인지만 확인하세요:
$ sudo apt update && sudo apt upgrade -yDOC
터미널 실행 확인
$ pwd $ whoami $ ls -la
터미널이 뜨고 위 세 명령이 정상 출력되면 기본 환경은 준비된 것입니다.
기본 개발환경 확인
$ python3 --version $ git --version $ gcc --version
GPU가 있는 컴퓨터라면 nvidia-smi로 인식 여부도 확인해두세요.
$ nvidia-smi
pip install하지 말고, uv로 프로젝트별 환경을 관리하는 걸 권장합니다. 자세한 사용법은 Python 기초 3 페이지에서 다룹니다.
자가 점검
- Ubuntu를 설치하고
apt update/upgrade로 최신화했다 - 터미널에서 기본 명령이 정상 동작한다
python3,git이 설치되어 있는지 확인했다
인증 스크린샷 첨부
wsl -l -v 결과나 python3 --version 등 기본 도구 버전 확인 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
VS Code 개발환경
실습 가이드앞으로 모든 코드를 여기서 씁니다. VS Code는 로컬(Windows)에 설치하지만, 실제 코드는 WSL·원격 서버·컨테이너 안에서 돕니다. 이 세 가지를 다루는 원격 개발 확장을 지금 한 번에 세팅해둡니다.
- VS Code를 WSL에 연결해
code .로 프로젝트를 열 수 있다 - Remote-SSH / Dev Containers 확장을 설치해뒀다
- 확장이 왜 "원격 쪽"에 설치되는지 설명할 수 있다
- 파이썬 인터프리터를 선택하고 디버거를 쓸 수 있다
VS Code 원격 개발 확장 (Remote Development)
VS Code는 로컬에 설치하지만, 실제 코드는 대부분 WSL·원격 서버·컨테이너 안에서 돕니다. Remote Development 확장 팩 하나로 이 세 가지를 전부 다룹니다. 지금은 세 확장을 모두 설치해두고, WSL 연결까지만 확인합니다 — SSH와 Docker 연결은 각각의 주제를 배운 뒤 이 레벨 뒤쪽에서 바로 실습합니다.
DOC① WSL — 지금 바로 확인합니다.
- Windows에 VS Code 설치, WSL 확장 설치
- Ubuntu 터미널에서 프로젝트 폴더로 이동 후
code .실행 - VS Code 좌측 하단에 WSL: Ubuntu 표시가 뜨는지 확인 — 이게 뜨면 원격 연결 성공
② SSH — Remote - SSH 확장을 지금 설치해두세요. 실제 접속은 Level 2의 SSH 페이지에서 키를 만든 뒤 바로 실습합니다. Ctrl+Shift+P → Remote-SSH: Connect to Host로 원격 서버를 로컬처럼 엽니다.
③ Docker — Dev Containers 확장을 지금 설치해두세요. devcontainer.json 하나로 컨테이너 안에서 그대로 개발하는 법은 이 레벨 마지막 도커 페이지에서 다룹니다.
파이썬 인터프리터 선택
프로젝트마다 파이썬 환경이 다르므로, VS Code에 "이 프로젝트는 이 파이썬을 쓴다"고 알려줘야 자동완성과 디버깅이 제대로 동작합니다.
- Python 확장을 설치한다 (WSL 쪽에 설치되는지 확인)
Ctrl+Shift+P→ Python: Select Interpreter- 프로젝트의
.venv안에 있는 인터프리터를 선택한다
목록에 안 뜨면 Developer: Reload Window로 새로고침하거나, "Enter interpreter path"로 .venv/bin/python 경로를 직접 입력하면 됩니다.
알아두면 좋은 단축키
| 단축키 | 기능 |
|---|---|
Ctrl+Shift+P | 명령 팔레트 — VS Code의 모든 기능이 여기 있습니다 |
Ctrl+` | 통합 터미널 열기/닫기 |
Ctrl+P | 파일 이름으로 빠르게 열기 |
F5 | 디버깅 시작 (중단점을 찍고 변수를 들여다볼 수 있음) |
F9 | 현재 줄에 중단점(breakpoint) 설정 |
print()를 여기저기 넣는 대신, 의심되는 줄에 F9로 중단점을 찍고 F5로 실행해보세요. 그 시점의 모든 변수값을 좌측 패널에서 한 번에 볼 수 있습니다. Python 기초 2의 Traceback 읽기와 함께 쓰면 디버깅 속도가 크게 달라집니다.
자가 점검
- WSL 확장으로 연결하고 좌측 하단
WSL: Ubuntu를 확인했다 - Remote-SSH, Dev Containers 확장을 설치해뒀다
- 확장이 로컬이 아니라 원격 쪽에 설치되는 이유를 설명할 수 있다
- Python: Select Interpreter로 인터프리터를 지정했다
- 중단점을 찍고
F5로 디버깅해봤다
인증 스크린샷 첨부
좌측 하단에 WSL: Ubuntu가 표시된 VS Code 화면이나, 디버거가 중단점에서 멈춘 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Linux 기초
실습 가이드터미널에서 살아남기 위한 최소한의 지식입니다. 경로와 파일, 권한, 패키지 설치, 환경변수, 프로세스까지 한 번에 정리합니다.
- 경로를 이동하고 파일을 다루고, 권한을 읽고 바꿀 수 있다
apt로 패키지를 설치·관리할 수 있다- 환경변수와 프로세스의 기본 개념을 이해한다
- udev rule로 시리얼 장치 이름을 고정할 수 있다
경로와 파일
| 명령어 | 하는 일 |
|---|---|
pwd | 현재 경로 출력 |
ls -la | 숨김 파일까지 자세히 목록 출력 |
cd 경로 | 이동 (cd .. 상위, cd ~ 홈) |
mkdir / touch | 디렉토리 / 빈 파일 생성 |
cp / mv / rm | 복사 / 이동·이름변경 / 삭제 |
cat | 파일 내용 출력 |
권한과 그룹
리눅스는 다중 사용자 시스템이라, 파일과 디바이스에 누가 무엇을 할 수 있는지가 항상 명확해야 합니다.
$ ls -l run.sh -rwxr-x--- 1 you labmembers 0 Jul 15 10:00 run.sh # 소유자(rwx) 그룹(r-x) 기타(---) — r=4 w=2 x=1, 750 = rwxr-x--- $ chmod 750 run.sh $ chown you:labmembers run.sh
특히 로봇 하드웨어 접근은 그룹 단위로 제어됩니다 — 시리얼 포트는 dialout, 도커는 docker, 카메라/GPU는 video 그룹입니다.
$ id # 내가 속한 그룹 확인 $ sudo usermod -aG dialout $USER $ newgrp dialout # 또는 재로그인해야 반영됨
udev rules로 시리얼 장치 이름 고정하기
USB로 모터 드라이버나 센서를 여러 개 꽂으면 /dev/ttyUSB0, /dev/ttyUSB1처럼 이름이 꽂는 순서·타이밍에 따라 바뀝니다. 재부팅하거나 순서가 바뀌면 코드에서 참조하던 이름이 어긋나버립니다. udev rule을 만들면 장치의 고유 정보(vendor/product ID, 시리얼 번호)로 항상 같은 이름을 붙일 수 있습니다.
# 1. 장치의 고유 정보 확인 $ udevadm info -a -n /dev/ttyUSB0 | grep -E "idVendor|idProduct|serial" # 2. 규칙 파일 작성 $ sudo nano /etc/udev/rules.d/99-motor.rules
/* /etc/udev/rules.d/99-motor.rules */
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A1024emu", SYMLINK+="motor_controller"
# 3. 규칙 적용 $ sudo udevadm control --reload-rules $ sudo udevadm trigger # 4. 확인 — motor_controller가 ttyUSB0을 가리키는지 $ ls -l /dev/motor_controller
/dev/ttyUSB0 대신 /dev/motor_controller를 참조하면, 몇 번째로 꽂았든 재부팅하든 항상 같은 장치를 정확히 가리킵니다. 장치를 여러 개 쓰는 로봇이라면 장치마다 규칙을 하나씩 만들어두세요.
apt 패키지 관리
| 명령어 | 하는 일 |
|---|---|
apt update | 패키지 목록(어떤 버전이 있는지)을 최신화 |
apt upgrade | 설치된 패키지를 최신 버전으로 갱신 |
apt install 이름 | 패키지 설치 |
apt remove 이름 | 패키지 삭제 |
apt search 키워드 | 패키지 이름 검색 |
환경변수
$ echo $PATH # 명령어를 찾는 경로 목록 $ echo $HOME $ export MY_VAR="hello" # 현재 셸에서만 유효 # 영구히 등록하려면 ~/.bashrc 맨 끝에 추가 $ echo 'export MY_VAR="hello"' >> ~/.bashrc $ source ~/.bashrc
프로세스
$ ps aux | grep python # 실행 중인 파이썬 프로세스 찾기 $ kill 1234 # PID 1234 종료 $ long_task.sh & # 백그라운드 실행 $ jobs # 백그라운드 작업 목록
실시간으로 CPU/메모리를 보고 싶다면 Level 2의 CLI 도구 페이지에서 다루는 htop이 더 편합니다.
자가 점검
- 파일 권한(rwx, 8진법)을 읽고 바꿀 수 있다
id/usermod -aG로 그룹을 확인·추가할 수 있다- udev rule로 시리얼 장치에 고정 이름을 붙일 수 있다
apt로 패키지를 설치·삭제할 수 있다- 환경변수를 확인하고 영구 등록할 수 있다
ps/kill로 프로세스를 확인하고 종료할 수 있다
인증 스크린샷 첨부
ls -l /dev/모터이름으로 udev 심볼릭 링크가 잡힌 화면이나, 위 실습 명령 실행 결과를 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Python 기초 1
실습 가이드로봇 코드 대부분은 파이썬으로 짜여 있습니다. 문법을 처음부터 전부 다루는 게 아니라, 로봇 코드를 읽고 쓰는 데 최소로 필요한 것부터 빠르게 훑습니다.
- 변수와 기본 자료형을 다룰 수 있다
- 리스트와 딕셔너리를 만들고 순회할 수 있다
- 조건문과 반복문으로 흐름을 제어할 수 있다
변수와 자료형
x = 3 # int speed = 1.5 # float name = "turtle" # str is_moving = True # bool print(type(speed)) # <class 'float'>
리스트와 딕셔너리
distances = [1.2, 0.8, 2.1, 0.5] distances.append(1.0) print(distances[0]) # 1.2 (인덱싱) print(distances[1:3]) # [0.8, 2.1] (슬라이싱) joint_limits = {"shoulder": 1.57, "elbow": 2.0} print(joint_limits["elbow"]) for name, limit in joint_limits.items(): print(name, limit)
조건문과 반복문
for d in distances:
if d < 1.0:
print("가까움")
elif d < 2.0:
print("보통")
else:
print("멀음")
i = 0
while i < 3:
print(i)
i += 1
입출력
name = input("이름을 입력하세요: ")
print(f"안녕하세요, {name}님") # f-string
실습 과제 — 센서값 평균 구하기
readings = [0.9, 1.1, 1.0, 0.95, 1.05]
total = 0
for r in readings:
total += r
average = total / len(readings)
print(f"평균 거리: {average:.2f}m")
자가 점검
- 기본 자료형(int/float/str/bool)을 구분할 수 있다
- 리스트/딕셔너리를 만들고 인덱싱·순회할 수 있다
- if/for/while로 흐름을 제어할 수 있다
- f-string으로 값을 출력할 수 있다
인증 스크린샷 첨부
센서값 평균 구하기 스크립트를 실행한 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Python 기초 2
실습 가이드코드가 길어지면 한 파일에 다 넣을 수 없습니다. 함수로 쪼개고, 모듈로 나누고, 최소한의 클래스 개념까지 — 그리고 문제가 생겼을 때 에러 메시지를 읽는 법까지 다룹니다.
- 함수를 정의하고 사용할 수 있다
- 코드를 여러 파일로 나누고 import할 수 있다
- 클래스의 최소 개념(
__init__,self)을 이해한다 - Traceback을 읽고 원인을 찾을 수 있다
- PEP가 무엇이고 왜 코드 스타일을 맞추는지 안다
함수
def clamp(value, min_val, max_val):
if value < min_val:
return min_val
if value > max_val:
return max_val
return value
angle = clamp(3.5, -1.57, 1.57)
모듈과 파일 분리
관련 없는 기능을 한 파일에 다 넣지 마세요. utils.py처럼 나누고 필요한 곳에서 import합니다.
# utils.py def clamp(value, min_val, max_val): ... # main.py from utils import clamp print(clamp(2.0, 0, 1))
클래스 최소 개념
다음 레벨에서 다룰 상태 머신·ROS2 노드는 전부 클래스로 만들어집니다. 지금은 딱 이 정도만 알면 됩니다.
class Robot:
def __init__(self, name):
self.name = name # self = 이 객체 자신
self.battery = 100
def move(self):
self.battery -= 1
print(f"{self.name} moving, battery={self.battery}")
r = Robot("omy")
r.move()
예외와 오류 메시지 읽기
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"계산 오류: {e}")
Traceback이 뜨면 당황하지 말고 맨 아래 줄부터 읽으세요 — 실제 에러 종류와 메시지가 거기 있습니다. 그 위의 File "...", line N이 문제가 발생한 파일과 줄 번호입니다.
Traceback (most recent call last):
File "main.py", line 12, in <module>
result = 10 / 0
ZeroDivisionError: division by zero ← 여기부터 먼저 읽는다
PEP란? — 코드 스타일 규칙
PEP(Python Enhancement Proposal)는 파이썬 언어와 생태계를 바꾸자는 공식 제안서 형식입니다. 수백 개가 있지만, 신입 개발자가 실제로 알아야 하는 건 거의 하나 — PEP 8입니다.
DOC| 규칙 | 예시 |
|---|---|
| 들여쓰기 | 스페이스 4칸 (탭 금지) |
| 변수·함수명 | snake_case — my_variable, compute_angle() |
| 클래스명 | PascalCase — RobotController |
| 상수 | UPPER_CASE — MAX_SPEED |
| import 순서 | 표준 라이브러리 → 서드파티 → 내 프로젝트 모듈 |
$ uv add --dev ruff $ ruff format . # 코드 스타일을 자동으로 맞춤 $ ruff check . # PEP 8 위반, 안 쓰는 import 등을 검사
python -c "import this"를 쳐보세요. 파이썬 설계 철학을 담은 짧은 원칙 목록이 나옵니다 — 요약하면 "명확하고 읽기 쉬운 코드가, 영리하지만 알아보기 힘든 코드보다 낫다"는 태도입니다. 코드를 짤 때 애매하면 이 원칙을 떠올려보세요.
자가 점검
- 함수를 정의하고 매개변수·반환값을 쓸 수 있다
- 코드를 여러 파일로 나누고 import할 수 있다
class/__init__/self의 역할을 설명할 수 있다- Traceback에서 원인을 찾아낼 수 있다
- PEP 8이 무엇이고 왜 따르는지 설명할 수 있다
ruff format/ruff check를 실행해봤다
인증 스크린샷 첨부
직접 만든 클래스를 실행한 화면이나, 일부러 에러를 내고 Traceback을 읽어본 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Python 기초 3
실습 가이드로봇 데이터는 대부분 숫자 배열입니다. NumPy로 행렬·삼각함수를 다루고, Matplotlib으로 그래프를 그리고, CSV로 로그를 남기는 법을 익힙니다. 뒤에 나올 기구학·MuJoCo 페이지에서 그대로 쓰입니다.
- NumPy로 배열·행렬 연산과 삼각함수를 쓸 수 있다
- Matplotlib으로 그래프를 그리고 저장할 수 있다
- CSV로 데이터를 기록할 수 있다
NumPy 기초
import numpy as np v = np.array([1.0, 2.0, 3.0]) M = np.array([[0, -1], [1, 0]]) # 90도 회전 행렬 print(M @ v[:2]) # 행렬곱 print(M.T) # 전치 theta = np.deg2rad(30) print(np.sin(theta), np.cos(theta))
행렬곱(@)과 삼각함수(np.sin, np.cos)는 뒤에 나올 기구학 계산에서 그대로 쓰입니다.
Matplotlib로 그래프 그리기
import matplotlib.pyplot as plt
t = np.linspace(0, 10, 100)
y = np.sin(t)
plt.plot(t, y)
plt.xlabel("time (s)")
plt.ylabel("angle (rad)")
plt.savefig("joint_angle.png")
CSV로 데이터 기록
import csv
with open("log.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["time", "angle"])
for i, ti in enumerate(t):
writer.writerow([ti, y[i]])
숫자 배열만 저장한다면 np.savetxt("log.csv", data, delimiter=",")가 더 간단합니다.
자가 점검
uv add로 NumPy/Matplotlib을 설치했다- 행렬곱과 삼각함수를 NumPy로 계산할 수 있다
- Matplotlib으로 그래프를 그리고 이미지로 저장할 수 있다
- CSV로 데이터를 기록할 수 있다
인증 스크린샷 첨부
저장된 그래프 이미지나 CSV 파일 내용을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
파이썬 환경 관리 (uv/venv/conda)
실습 가이드이 온보딩은 uv를 기본으로 씁니다. 하지만 venv와 conda로 만든 프로젝트를 만날 일이 훨씬 많으므로, 세 가지가 내부적으로 무엇을 하는지 구조까지 이해하고 넘어갑니다.
- uv로 프로젝트를 초기화하고 의존성을 추가할 수 있다
activate가 내부적으로 PATH를 바꾸는 것뿐이라는 걸 설명할 수 있다- venv(경로 기반)와 conda(이름 기반)의 관리 방식 차이를 설명할 수 있다
먼저 읽을 것
DOCuv로 프로젝트 시작하기
$ curl -LsSf https://astral.sh/uv/install.sh | sh
$ source ~/.bashrc
$ uv init robot-project
$ cd robot-project
$ uv add numpy opencv-python
$ uv run python main.py # activate 없이 바로 실행
venv 구조 이해하기
uv도 내부적으로는 venv와 같은 방식의 디렉토리를 만듭니다. 안에서 무슨 일이 일어나는지 알아두면 문제가 생겼을 때 훨씬 빨리 원인을 찾을 수 있습니다.
.venv/ ├── bin/ # python, pip 등 실행 파일이 여기 복사/링크됨 │ ├── python │ ├── pip │ └── activate # 이 venv를 켜는 셸 스크립트 ├── lib/ │ └── python3.11/ │ └── site-packages/ # pip install한 패키지가 전부 여기로 └── pyvenv.cfg # 어떤 파이썬을 기반으로 만들었는지 기록
source .venv/bin/activate가 실제로 하는 일은 두 가지뿐입니다: ① PATH 맨 앞에 .venv/bin을 추가해서 python/pip가 이 venv 것을 먼저 찾게 하고, ② VIRTUAL_ENV 환경변수를 설정합니다. deactivate는 이걸 되돌리는 것뿐입니다 — 마법이 아니라 단순한 환경변수 조작입니다.
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ which python # .venv/bin/python 이 나오는지 확인
(.venv) $ pip install requests
(.venv) $ deactivate
conda 구조 이해하기
conda는 venv와 다르게, 모든 환경을 한 곳에 모아서 관리합니다. 프로젝트 폴더 안이 아니라 conda 설치 경로 아래에 생깁니다.
~/miniconda3/ ├── envs/ │ ├── robot-perception/ # conda create -n robot-perception 으로 생성됨 │ │ ├── bin/ │ │ └── lib/ │ └── robot-control/ └── pkgs/ # 여러 환경이 공유하는 패키지 캐시
그래서 conda는 conda activate 이름처럼 경로가 아니라 이름으로 환경을 켭니다. 패키지를 어디서 받아올지 지정하는 channel 개념도 있습니다(예: conda-forge) — pip의 PyPI에 해당하는 저장소입니다.
$ conda create -n robot-perception python=3.11
$ conda activate robot-perception
(robot-perception) $ conda install -c conda-forge numpy opencv
(robot-perception) $ conda env export > environment.yml # venv의 requirements.txt에 해당
(robot-perception) $ conda deactivate
세 가지 비교
| uv | venv | conda | |
|---|---|---|---|
| 속도 | 매우 빠름 (Rust) | 보통 | 느린 편 |
| 관리 방식 | 경로 기반 (venv와 동일) | 경로 기반 | 이름 기반, 중앙 저장소 |
| Python 버전 관리 | 내장 (uv python install) | 없음 | 내장 |
| 비-파이썬 의존성 (CUDA 등) | 관리 불가 | 관리 불가 | 관리 가능 |
| 연구실에서 언제 쓰나 | 기본으로 이걸 씁니다 | uv 없이 예전 프로젝트를 읽을 때 | CUDA·시스템 라이브러리까지 격리해야 할 때 |
.venv를 만들고 PATH만 바꿔치기하는 가장 단순한 격리 방법입니다. conda는 이름으로 관리되는 중앙 저장소 방식이고, 비-파이썬 라이브러리까지 설치할 수 있습니다. uv는 venv와 같은 구조를 만들되, 설치·버전 관리·lockfile까지 자동으로 훨씬 빠르게 처리해주는 도구입니다.
sudo pip install로 시스템 파이썬에 바로 설치하면, 다른 프로젝트의 패키지 버전과 충돌하거나 시스템 도구 자체가 망가질 수 있습니다.
자가 점검
- uv로 프로젝트를 초기화하고 의존성을 추가했다
activate가 PATH를 바꾸는 것뿐이라는 걸 설명할 수 있다- venv(경로 기반)와 conda(이름 기반)의 차이를 설명할 수 있다
- 세 가지 중 언제 무엇을 쓸지 설명할 수 있다
인증 스크린샷 첨부
uv add 실행 결과나 venv/conda 활성화 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
SSH 유저 입출력
실습 가이드연구실 서버나 로봇 탑재 PC에는 대부분 원격으로 접속합니다. SSH는 그 원격 접속의 표준이고, 로봇 GUI 도구(RViz 등)를 원격으로 띄우거나 파일을 옮기는 데도 계속 쓰이게 됩니다.
- 비밀번호 대신 키 기반 인증으로 접속할 수 있다
~/.ssh/config로 접속 명령을 단순화할 수 있다- 원격 GUI(X11 forwarding)와 파일 전송(scp/rsync)을 할 수 있다
키 기반 인증이 필요한 이유
비밀번호는 무차별 대입 공격에 취약하고, 매번 입력해야 하며, 스크립트로 자동화하기 어렵습니다. 공개키/개인키 쌍을 쓰면 개인키가 없으면 접속 자체가 불가능하고, 비밀번호 없이 자동 로그인이 가능해집니다.
$ ssh-keygen -t ed25519 -C "your_email@lab.ac.kr" $ ssh-copy-id user@robot-pc.local # TODO: user@robot-pc.local 을 실제 접속 주소로 바꾸세요 # 이후부터는 비밀번호 없이 접속 $ ssh user@robot-pc.local
자주 나는 오류
| 증상 | 주로 원인 |
|---|---|
Permission denied (publickey) | 공개키가 서버에 등록되지 않음 — ssh-copy-id를 다시 실행하거나 ~/.ssh/authorized_keys를 직접 확인 |
WARNING: UNPROTECTED PRIVATE KEY FILE | 개인키 파일 권한이 너무 열려있음 — chmod 600 ~/.ssh/id_ed25519 |
| 매번 비밀번호를 또 물어봄 | ssh-copy-id를 안 했거나, 서버의 authorized_keys에 공개키가 없음 |
Connection refused | 서버의 SSH 데몬이 꺼져있거나 방화벽이 22번 포트를 막고 있음 |
~/.ssh/config로 단순화
# ~/.ssh/config Host robot HostName 192.168.1.42 User you Port 22 ForwardX11 yes # TODO: HostName/User를 실제 서버 정보로 바꾸세요 # 이제 아래처럼 짧게 접속 $ ssh robot
원격 GUI와 파일 전송
- X11 forwarding —
ssh -X robot으로 접속하면 로봇 PC에서 띄운 GUI 창(RViz 등)을 내 화면에 그대로 표시할 수 있다 scp— 파일 한두 개를 빠르게 복사:scp file.txt robot:~/rsync— 폴더 동기화, 변경분만 전송해서 반복 작업에 효율적:rsync -avz ./ws/ robot:~/ws/
VSCode + SSH (Remote-SSH)
앞의 VS Code 페이지에서 Remote-SSH 확장을 이미 설치해뒀습니다. 이제 방금 만든 ~/.ssh/config의 robot alias로 실제로 연결해봅니다.
Ctrl+Shift+P→ Remote-SSH: Connect to Hostrobotalias를 선택한다- 새 창이 뜨고 좌측 하단에 SSH: robot 표시가 나타나면 연결 완료
자가 점검
- 키 기반 인증을 설정하고 비밀번호 없이 접속했다
~/.ssh/config로 접속 alias를 만들었다- X11 forwarding으로 원격 GUI를 띄워봤다
scp와rsync의 차이를 설명할 수 있다- Remote-SSH로 원격 서버에 접속해 VS Code 창을 띄웠다
인증 스크린샷 첨부
키 기반 접속 성공 화면, 원격 GUI 화면, 또는 좌측 하단에 "SSH: robot"이 표시된 VS Code 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
tmux & 세션 관리
실습 가이드SSH로 접속해서 오래 걸리는 학습이나 로그 수집을 돌리다가, 연결이 끊겨서 프로세스가 같이 죽어본 적 있나요? tmux는 터미널 세션을 원격 서버 쪽에 남겨두는 도구입니다.
- tmux 세션을 만들고 분리(detach)·재접속(attach)할 수 있다
- 세션이 SSH 연결과 독립적으로 유지된다는 것을 이해한다
먼저 읽을 것
DOC실습 과제
$ tmux new -s train $ python train.py # 오래 걸리는 작업 실행 # Ctrl+b, d 로 분리(detach) — 작업은 백그라운드에서 계속 실행됨 $ exit # SSH 연결을 끊어도 안전 # 나중에 다시 접속해서 $ tmux ls $ tmux attach -t train # 그대로 이어서 확인
자가 점검
- 세션을 만들고 이름을 지정할 수 있다
- detach/attach를 할 수 있다
tmux ls로 세션 목록을 확인하고 원하는 세션에 다시 붙을 수 있다- SSH 연결이 끊겨도 tmux 세션 속 프로세스가 계속 도는 이유를 설명할 수 있다
인증 스크린샷 첨부
tmux ls로 세션이 유지되고 있는 걸 보여주는 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
유용한 CLI 도구
실습 가이드tmux처럼, 알아두면 작업 속도가 확 달라지는 커맨드라인 도구들이 몇 가지 더 있습니다. 전부 필수는 아니지만, 연구실 서버 작업에서 실제로 자주 쓰입니다. 오래된 명령어를 대체하는 "더 나은 버전"들이라고 생각하면 됩니다.
- 최소 2~3개의 도구를 설치하고 실제로 써봤다
- 각 도구가 기존 명령어 대비 어떤 점이 나은지 설명할 수 있다
도구 모음
| 도구 | 대체하는 명령어 | 한 줄 설명 | 설치 (Ubuntu) |
|---|---|---|---|
| htop | top | 프로세스·CPU·메모리를 실시간으로, 색깔과 마우스 지원까지 곁들여 보여준다 | sudo apt install htop |
| ripgrep (rg) | grep -r | 훨씬 빠르고, .gitignore에 있는 파일은 자동으로 건너뛴다 | sudo apt install ripgrep |
| fzf | 수동 탐색 | 파일·명령어 히스토리·Git 브랜치 등 어떤 목록이든 타이핑으로 실시간 좁혀가며 고를 수 있게 해준다 | sudo apt install fzf |
| bat | cat | 문법 하이라이팅과 줄 번호가 있는 파일 뷰어 | sudo apt install bat |
| ncdu | du -sh | 어떤 폴더가 용량을 많이 차지하는지 대화형으로 파고들어 찾아준다 | sudo apt install ncdu |
| jq | 수동 파싱 | 터미널에서 JSON을 파싱·필터링·예쁘게 출력한다 (설정 파일, API 응답 확인에 유용) | sudo apt install jq |
왜 알아두면 좋은가
- htop —
nvidia-smi가 GPU를 보여준다면,htop은 CPU/메모리를 실시간으로 보여줍니다. 학습 스크립트가 CPU 병목인지 GPU 병목인지 감을 잡을 때 둘을 나란히 켜두면 좋습니다. - ripgrep — 코드베이스에서 특정 토픽 이름이나 파라미터가 어디서 쓰이는지 찾을 때,
grep -r보다 체감상 훨씬 빠릅니다. - fzf —
Ctrl+R과 조합하면 예전에 쳤던 긴 명령어를 다시 타이핑하지 않고 검색해서 바로 불러올 수 있습니다. - ncdu — 연구실 공용 서버에서 "디스크가 꽉 찼다"는 알림을 받았을 때, 어떤 폴더가 범인인지 가장 빠르게 찾는 방법입니다.
실습 과제
$ htop # CPU/메모리 실시간 모니터링, q로 종료 $ rg "cmd_vel" ~/robot-ws # 코드베이스에서 특정 문자열 검색 $ history | fzf # 과거 명령어를 fuzzy 검색 (또는 Ctrl+R) $ ncdu ~ # 홈 디렉토리 용량을 대화형으로 탐색 $ cat config.json | jq '.' # JSON을 보기 좋게 정리해서 출력
htop과 ripgrep 정도만 손에 익혀도 충분하고, 나머지는 필요할 때 하나씩 꺼내 써보면 됩니다.
자가 점검
- 최소 2개 이상의 도구를 설치했다
htop으로 프로세스별 CPU/메모리 사용량을 확인할 수 있다ripgrep과grep -r의 차이를 설명할 수 있다- 실제로
fzf나ncdu중 하나를 써서 뭔가를 찾아봤다
인증 스크린샷 첨부
htop 실행 화면이나 rg/ncdu 사용 결과를 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
GitHub 사용법
실습 가이드이제 내 코드를 팀(또는 세상)에 내놓는 법을 배웁니다. 연구실 코드는 대부분 Git으로 관리되고, GitHub은 그 코드가 실제로 모이는 곳입니다.
- GitHub에서 필요한 코드·저장소를 찾을 수 있다
- 저장소를 clone하고 커밋·푸시할 수 있다
- 내 코드를 새 저장소로 올릴 수 있다
코드·저장소 검색하기
GitHub 검색창에 키워드를 넣고, 언어(Language)·스타(Stars) 필터로 좁혀보세요. 예: ros2 nav2 language:Python
README와 Issues 먼저 확인하기
저장소에 들어가면 README부터 읽으세요 — 이 코드가 뭘 하는지, 어떻게 실행하는지 대부분 여기 있습니다. 궁금한 점이나 버그가 있으면 코드를 뒤지기 전에 Issues 탭에서 이미 같은 질문이 있는지 검색하세요.
Clone
$ git clone git@github.com:yourlab/robot-ws.git $ cd robot-ws
git@github.com:... 형태는 SSH 키 인증을 씁니다. Level 2의 SSH 페이지에서 만든 키를 그대로 씁니다.
add · commit · push
$ git config --global user.name "Your Name"
$ git config --global user.email "you@lab.ac.kr"
$ git checkout -b feature/my-first-change
# 파일 수정 후
$ git add .
$ git commit -m "feat: add my first change"
$ git push origin feature/my-first-change
커밋 메시지는 타입(범위): 설명 형식(Conventional Commits)을 권장합니다 — feat, fix, docs, chore 등.
내 코드 올리기 — 새 저장소 만들기
$ git init
$ git add .
$ git commit -m "chore: initial commit"
# GitHub에서 New repository로 빈 저장소를 만든 뒤
$ git remote add origin git@github.com:yourid/my-project.git
$ git push -u origin main
main에 직접 푸시하지 않고, 브랜치를 따서 작업한 뒤 Pull Request(PR)로 리뷰를 받는 흐름을 씁니다. README에는 프로젝트 목적·실행 방법·의존성을 간단히라도 남겨두세요 — 다음 사람(미래의 나 포함)을 위한 최소한의 배려입니다.
자가 점검
- GitHub에서 원하는 저장소를 검색하고 README를 읽었다
- 저장소를 clone하고 커밋·푸시했다
- 새 저장소를 만들어 내 코드를 올렸다
- 브랜치 + PR 흐름이 왜 필요한지 설명할 수 있다
인증 스크린샷 첨부
git log --oneline 결과나 GitHub에 올라간 내 저장소 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
도커 (Docker)
실습 가이드로봇 소프트웨어 스택은 의존성이 매우 복잡합니다(특정 CUDA 버전, 특정 ROS2 배포판, 특정 라이브러리 조합). 도커는 이 조합을 이미지로 굳혀서, "내 컴퓨터에서는 됐는데" 문제를 근본적으로 줄여줍니다.
- 컨테이너가 VM과 무엇이 다르고, 리눅스 커널의 어떤 기능 위에서 동작하는지 안다
- 이미지와 컨테이너의 차이, 레이어가 왜 캐시되는지 설명할 수 있다
- Dockerfile을 직접 읽고 쓸 수 있다
- 멀티스테이지 빌드로 이미지 크기를 줄이는 최신 방식을 안다
- WSL2에서 GPU를 인식하는 도커 컨테이너를 실행할 수 있다
Docker 기본 원리
도커 컨테이너는 가상머신(VM)이 아닙니다. VM은 하드웨어 전체를 흉내 내서 그 위에 별도의 커널을 통째로 띄우지만, 컨테이너는 호스트와 리눅스 커널을 그대로 공유하면서 프로세스를 서로 격리시킵니다. 이걸 가능하게 하는 건 리눅스 커널의 두 기능입니다.
- namespaces — 프로세스마다 자기만의 파일시스템·네트워크·프로세스 목록을 보게 만드는 "시야 격리". 컨테이너 안에서
ps aux를 치면 호스트의 다른 프로세스는 안 보입니다. - cgroups (control groups) — CPU·메모리 사용량에 상한선을 그어주는 "자원 격리". 컨테이너 하나가 호스트 전체 메모리를 잡아먹는 걸 막습니다.
그래서 컨테이너는 VM보다 훨씬 가볍고 시작이 빠릅니다 — 별도의 커널을 부팅하는 게 아니라, 커널은 하나 그대로 두고 "이 프로세스는 이 영역만 보게" 나눠주는 것뿐이기 때문입니다.
이미지, 레이어, 레지스트리
- 이미지(Image) — 실행에 필요한 파일시스템과 설정을 굳혀놓은 읽기 전용 템플릿.
Dockerfile로 정의한다. - 컨테이너(Container) — 이미지를 실제로 실행한 인스턴스. 같은 이미지로 컨테이너를 여러 개 띄울 수 있다.
- 레이어(Layer) — 이미지는 한 덩어리가 아니라, Dockerfile의 명령어 한 줄마다 쌓이는 레이어들의 집합이다. 겹쳐진 레이어들을 하나의 파일시스템처럼 보여주는 게 union filesystem이다.
- 레지스트리(Registry) — 이미지를 저장·배포하는 서버. Docker Hub가 가장 흔하고, 회사/연구실이 자체 레지스트리를 운영하기도 한다.
docker pull/push가 여기와 통신한다.
Dockerfile이란?
Dockerfile은 "이미지를 어떻게 만들지"를 적은 레시피입니다. 한 줄 한 줄이 레이어 하나가 되고, 도커는 바뀌지 않은 레이어를 재사용(캐시)해서 재빌드 속도를 높입니다. 그래서 자주 안 바뀌는 것(의존성 설치)을 먼저, 자주 바뀌는 것(내 코드)을 나중에 적는 순서가 중요합니다.
| 명령어 | 하는 일 |
|---|---|
FROM | 어떤 베이스 이미지 위에서 시작할지 (예: python:3.12-slim) |
WORKDIR | 이후 명령어들이 실행될 기준 디렉토리 설정 |
COPY | 호스트의 파일을 이미지 안으로 복사 |
RUN | 이미지를 만드는 도중 실행할 명령 (패키지 설치 등) |
ENV | 컨테이너 안에서 쓸 환경변수 설정 |
USER | 이후 명령/실행을 어떤 사용자 권한으로 할지 (root 대신 지정 권장) |
CMD | 컨테이너가 시작될 때 실행할 기본 명령 |
# Dockerfile — 가장 기본적인 형태 FROM python:3.12-slim WORKDIR /app # 의존성 설치를 코드 복사보다 먼저 — requirements.txt가 안 바뀌면 이 레이어는 캐시된다 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 코드는 가장 자주 바뀌므로 맨 마지막에 COPY . . CMD ["python", "main.py"]
$ docker build -t my-app . $ docker run --rm my-app
최신 트렌드 (2026) — 멀티스테이지 빌드 + uv
요즘 파이썬 프로젝트를 도커화할 때 표준처럼 쓰이는 방식은 멀티스테이지 빌드입니다. 빌드에만 필요한 도구(컴파일러, uv 자체)와, 실행에만 필요한 것(가상환경, 코드)을 단계로 분리해서, 최종 이미지에는 실행에 필요한 것만 남깁니다. Level 1에서 배운 uv를 그대로 활용합니다.
# syntax=docker/dockerfile:1 # ---- 1단계: 빌드 전용 (uv로 의존성만 설치) ---- FROM python:3.12-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 WORKDIR /app COPY pyproject.toml uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-install-project --no-editable COPY . . RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-editable # ---- 2단계: 실행 전용 (가상환경만 복사, uv는 안 들어감) ---- FROM python:3.12-slim WORKDIR /app COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app . ENV PATH="/app/.venv/bin:$PATH" CMD ["python", "main.py"]
이렇게 하면 최종 이미지에는 uv나 컴파일러가 남지 않아 크기가 훨씬 작아지고, --mount=type=cache(BuildKit 기능)로 uv 캐시를 재사용해 재빌드도 빨라집니다.
.dockerignore로.git,__pycache__같은 불필요한 파일을 빌드 컨텍스트에서 제외- 이미지 태그를
latest가 아니라python:3.12-slim처럼 버전을 못 박아 재현성 확보 USER로 root가 아닌 계정을 지정해 보안 위험 축소docker-compose가 아니라docker compose(하이픈 없이, Docker CLI에 내장된 플러그인)를 사용
실습 과제
$ docker run --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
→ 컨테이너 내부에서도 호스트 GPU가 보이면 성공
--gpus all 없이 실행한 컨테이너에서는 GPU가 절대 보이지 않습니다. 실행할 때마다 옵션을 빠뜨리지 않았는지 확인하는 습관을 들이세요.
자주 나는 오류
| 증상 | 주로 원인 |
|---|---|
docker: command not found | Docker가 설치 안 됐거나 WSL2 통합이 꺼져있음 — Docker Desktop 설정에서 WSL Integration 확인 |
permission denied while trying to connect to the Docker daemon socket | 사용자가 docker 그룹에 없음 — Linux 기초 페이지의 usermod -aG docker $USER 다시 확인 |
컨테이너 안에서 nvidia-smi: command not found | --gpus all을 빠뜨렸거나, NVIDIA Container Toolkit이 설치 안 됨 |
| 이미지 pull이 너무 느리거나 실패함 | 네트워크 문제 — 태그를 정확히 확인(오타가 흔함), 사내망이면 프록시 설정 확인 |
Docker Desktop (GUI)
지금까지는 전부 CLI(docker run 등)로 다뤘지만, Docker Desktop은 이미지·컨테이너·볼륨을 눈으로 보고 클릭으로 관리할 수 있는 GUI 앱입니다. 위에서 설치한 WSL2 backend가 바로 이 Docker Desktop이 쓰는 엔진입니다.
- Containers 탭 — 실행 중인 컨테이너 목록, 클릭 한 번으로 로그 보기·터미널 열기·정지/삭제
- Images 탭 — 로컬에 받아둔 이미지 목록과 용량, 필요 없는 이미지 정리
- Volumes 탭 — 컨테이너가 쓰는 영속 데이터 확인
여러 컨테이너를 동시에 띄워놓고 상태를 한눈에 보고 싶거나, 로그를 스크롤하며 읽고 싶을 때 CLI보다 편합니다. CLI와 GUI는 같은 도커 엔진을 보는 두 개의 창일 뿐이라, 아무 때나 편한 쪽을 쓰면 됩니다.
CUDA 버전 도커 이미지
공식 nvidia/cuda 이미지는 태그로 CUDA 버전과 용도를 구분합니다. 태그 구조는 다음과 같습니다:
nvidia/cuda:<CUDA버전>-<flavor>-<OS>
# 예시
nvidia/cuda:12.6.3-base-ubuntu22.04
nvidia/cuda:12.6.3-runtime-ubuntu22.04
nvidia/cuda:12.6.3-cudnn-devel-ubuntu22.04
| flavor | 포함 내용 | 언제 쓰나 |
|---|---|---|
base | CUDA 드라이버 API만 최소한으로 포함 | 이미지 용량이 중요할 때 |
runtime | base + CUDA 수학 라이브러리, NCCL 등 | 이미 빌드된 프로그램/모델을 그냥 실행할 때 |
devel | runtime + 컴파일러, 헤더, 정적 라이브러리 | 컨테이너 안에서 CUDA 코드를 직접 빌드할 때 |
$ docker pull nvidia/cuda:12.6.3-runtime-ubuntu22.04 $ docker run --gpus all nvidia/cuda:12.6.3-runtime-ubuntu22.04 nvidia-smi
nvidia-smi로 확인했던 값은 "호스트(WSL2) 드라이버가 지원하는 최대 CUDA 버전"입니다. 도커 이미지의 CUDA 버전이 그보다 높으면 컨테이너 안에서 GPU를 인식하지 못할 수 있습니다. 이미지를 고르기 전에 호스트 쪽 버전을 먼저 확인하세요.
VSCode + Docker (Dev Containers)
Dev Containers 확장을 쓰면 컨테이너 안에서 그대로 개발할 수 있습니다. devcontainer.json 파일 하나로 "이 프로젝트는 이 이미지, 이 옵션으로 개발한다"를 코드로 정의해두면, 팀원 누구나 똑같은 환경에서 시작합니다.
// .devcontainer/devcontainer.json
{
"name": "robot-dev",
"image": "nvidia/cuda:12.6.3-devel-ubuntu22.04",
"runArgs": ["--gpus", "all"],
"postCreateCommand": "pip install -r requirements.txt"
}
확장을 설치한 뒤 Ctrl+Shift+P → Dev Containers: Reopen in Container를 누르면 VS Code가 이 설정대로 컨테이너를 만들고 그 안에서 다시 열립니다. 이미 떠 있는 컨테이너에 붙고 싶을 땐 Dev Containers: Attach to Running Container를 씁니다.
자가 점검
- 컨테이너가 VM과 무엇이 다른지, namespaces/cgroups의 역할을 설명할 수 있다
- 이미지·컨테이너·레이어·레지스트리의 관계를 설명할 수 있다
- Dockerfile을 직접 작성하고 빌드할 수 있다
- 멀티스테이지 빌드가 왜 이미지 크기를 줄여주는지 설명할 수 있다
--gpus all이 왜 필요한지 설명할 수 있다- base/runtime/devel 태그의 차이를 설명할 수 있다
- Dev Containers로 컨테이너 안에서 VS Code를 열어봤다
인증 스크린샷 첨부
컨테이너 내부에서 GPU가 인식된 nvidia-smi 출력 화면이나, Dev Containers로 연결된 VS Code 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Level 1 미니 프로젝트
실습 가이드Level 1에서 배운 것들을 한 번에 엮어봅니다. 새로운 개념은 없습니다 — 지금까지 각자 배운 조각들을 실제로 이어붙이는 연습입니다.
- 가짜 센서값을 만들어 CSV로 기록하고 그래프로 저장하는 스크립트
- uv로 관리되는 프로젝트, Git으로 버전관리, GitHub에 공개 저장소로 업로드
- Docker 컨테이너 안에서 그대로 실행되는 환경
- (가능하다면) SSH로 접속한 원격 서버의 tmux 세션 안에서 실행
1. 프로젝트 뼈대
$ uv init sensor-logger $ cd sensor-logger $ uv add numpy matplotlib $ git init
2. 로깅 스크립트 작성
import csv, time, random
import numpy as np
import matplotlib.pyplot as plt
# TODO: 원하는 만큼 반복 횟수/주기를 조절하세요
readings = []
for i in range(50):
value = np.sin(i * 0.2) + random.uniform(-0.05, 0.05)
readings.append(value)
time.sleep(0.05)
with open("sensor_log.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["step", "value"])
for i, v in enumerate(readings):
writer.writerow([i, v])
plt.plot(readings)
plt.xlabel("step")
plt.ylabel("sensor value")
plt.savefig("sensor_plot.png")
print("done:", len(readings), "readings logged")
Linux 기초에서 배운 python3 file.py 대신, uv 프로젝트 안에서는 uv run python main.py로 실행합니다.
3. Docker로 감싸기
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install uv && uv sync
CMD ["uv", "run", "python", "main.py"]
$ docker build -t sensor-logger . $ docker run --rm -v $(pwd)/out:/app/out sensor-logger
4. (선택) 원격 서버 + tmux
연구실 서버 접속 권한이 있다면, SSH로 접속해 tmux 세션 안에서 돌려보세요. 연결이 끊겨도 계속 돕니다.
$ ssh robot
$ tmux new -s logger
$ uv run python main.py
# Ctrl+b, d 로 분리, 나중에 tmux attach -t logger 로 다시 확인
5. GitHub에 올리기
$ git add . $ git commit -m "feat: sensor logging pipeline with docker" $ git remote add origin git@github.com:yourid/sensor-logger.git $ git push -u origin main
자가 점검
- 스크립트가 CSV와 그래프 이미지를 정상적으로 생성한다
- Docker 컨테이너 안에서 동일하게 실행된다
- GitHub 저장소에 코드가 올라가 있다
인증 스크린샷 첨부
생성된 그래프, 도커 실행 결과, GitHub 저장소 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
통신이 필요한 이유
이론 강의로봇 소프트웨어는 하나의 프로그램이 아니라, 여러 개의 독립된 프로세스가 협력하는 분산 시스템입니다. 왜 하나로 합치지 않고 굳이 나누고, 그 사이를 통신으로 잇는지부터 이해해야 이후 모든 개념이 자연스러워집니다.
왜 하나의 프로그램으로 만들지 않는가
카메라로 사물을 인식하고, 그 결과로 경로를 계획하고, 모터를 구동하는 로봇을 생각해봅시다. 이 모든 걸 하나의 프로그램에 다 넣을 수도 있지만, 실제 연구실 로봇은 대부분 그렇게 하지 않습니다. 이유는 네 가지입니다.
- 이질적인 하드웨어 — 카메라 인지는 GPU가 있는 Jetson에서, 실시간 모터 제어는 별도의 마이크로컨트롤러에서 돌아갑니다. 애초에 같은 컴퓨터에 있지 않습니다.
- 이질적인 시간 요구 — 모터 제어는 1ms 단위로 반응해야 하지만, 경로 계획은 100ms에 한 번이어도 됩니다. 한 프로세스에 합치면 느린 쪽이 빠른 쪽을 방해합니다.
- 장애 격리 — 인지 모듈이 죽어도 제어 모듈은 안전 정지 루틴을 계속 돌려야 합니다. 한 프로세스면 하나가 죽을 때 전체가 죽습니다.
- 개발·재사용 효율 — 각 모듈을 다른 사람이 다른 언어로, 다른 일정으로 개발하고 교체할 수 있습니다.
정리
통신은 부가 기능이 아니라 로봇 시스템의 기본 골격입니다. 다음 페이지부터는 이 화살표 하나하나가 실제로 무엇으로 구현되는지 — OS 레벨의 TCP/IP부터, 그 위에 얹히는 DDS, 그리고 ROS2가 제공하는 토픽/서비스/액션까지 — 아래에서 위로 쌓아 올라가며 봅니다.
간단한 퀴즈
1. 로봇 소프트웨어를 여러 프로세스로 나누는 이유가 아닌 것은?
2. 카메라(Jetson) - 인지(GPU 서버) - 제어(실시간 PC) - 모터(MCU)가 각각 다른 컴퓨터에 있는 것이 자연스러운 가장 큰 이유는?
3. "통신은 로봇 시스템의 기본 골격이다"라는 말의 의미로 가장 알맞은 것은?
OS 레벨 통신 — TCP/IP/DDS
이론 강의로봇 소프트웨어에서 쓰는 "토픽 발행" 같은 개념도, 결국 바닥에서는 운영체제의 소켓과 네트워크 스택을 통해 전달됩니다. 위에서 아래로 한 층씩 내려가 봅니다.
TCP vs UDP — 로봇 관점에서
| TCP | UDP | |
|---|---|---|
| 특성 | 연결 지향, 순서 보장, 재전송으로 신뢰성 확보 | 비연결, 순서·도착 보장 없음, 대신 빠르고 오버헤드 적음 |
| 대가 | 재전송 대기 때문에 지연이 들쭉날쭉할 수 있음 | 패킷이 유실되면 그냥 사라짐 |
| 로봇에서 쓰는 예 | 설정 명령, 펌웨어 업로드처럼 "한 번은 반드시 정확히" 가야 하는 데이터 | 카메라 영상 스트림, 최신 값만 중요한 센서 스트림 |
직관: 영상 프레임 하나가 늦게 오느니 버리고 다음 최신 프레임을 받는 게 낫다 — 그래서 UDP. 반대로 "정지" 같은 명령은 한 번은 반드시 정확히 도착해야 한다 — 그래서 TCP.
DDS가 추가로 해주는 일
DDS는 TCP/UDP 소켓을 직접 다루는 번거로움을 대신 처리해주는 발행-구독(pub/sub) 미들웨어입니다. ROS2는 내부적으로 DDS 구현체(예: Fast DDS, Cyclone DDS) 위에서 동작합니다.
- 자동 Discovery — 발행자와 구독자가 서로의 IP/포트를 몰라도 자동으로 찾아 연결한다
- QoS (Quality of Service) — "이 토픽은 최근 값 10개만 유지", "이 토픽은 반드시 전달 보장" 등 데이터 전달 방식을 세밀하게 설정 가능
- 다대다 통신 — 발행자 1명, 구독자 여러 명 같은 구조를 소켓 코드 없이 선언적으로 구성
간단한 퀴즈
1. 카메라 영상 스트림처럼 "최신 값이 중요하고 약간의 유실은 괜찮은" 데이터에 더 적합한 것은?
2. DDS가 제공하는 기능이 아닌 것은?
3. ROS2는 어떤 계층 위에서 동작하는가?
ROS 2 시작
실습 가이드지금까지 DDS와 OS 레벨 통신 이론을 봤다면, 이제 그 위에서 실제로 동작하는 ROS 2를 직접 켜봅니다.
- ROS 2 환경을 source하고 정상 설치를 확인할 수 있다
- workspace/package/node 개념을 구분할 수 있다
- turtlesim으로 첫 노드를 실행해볼 수 있다
먼저 읽을 것 — 설치가 안 되어 있다면
DOCapt로 ros-jazzy-desktop 패키지 하나만 설치하면 turtlesim을 포함한 대부분의 기본 도구가 함께 들어옵니다. 이미 설치되어 있다면 아래로 넘어가세요.
환경 source하기
$ source /opt/ros/jazzy/setup.bash
$ echo $ROS_DISTRO # jazzy 가 나오면 정상
매번 치기 번거로우면 ~/.bashrc에 등록해두세요: echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc
워크스페이스와 패키지
~/ros2_ws/
└── src/
└── my_package/
├── package.xml # 패키지 정보, 의존성
├── setup.py # (Python 패키지의 경우)
└── my_package/
└── my_node.py
워크스페이스는 여러 패키지를 모아 빌드하는 작업 폴더, 패키지는 코드 묶음 하나, 노드는 그 안에서 실제로 실행되는 프로그램 하나입니다. 빌드는 다음 페이지(rclpy)에서 다룹니다.
실습 — turtlesim
$ ros2 pkg list | grep turtlesim $ ros2 run turtlesim turtlesim_node
다른 터미널을 열어서:
$ ros2 run turtlesim turtle_teleop_key
# 화살표 키로 거북이를 움직여봅니다
ros2 run / pkg / node 명령 정리
| 명령어 | 하는 일 |
|---|---|
ros2 run <pkg> <node> | 패키지 안의 특정 노드 실행 |
ros2 pkg list | 설치된 패키지 목록 |
ros2 node list | 현재 실행 중인 노드 목록 |
ros2 node info <name> | 그 노드가 쓰는 토픽·서비스 등 상세 정보 |
자가 점검
- ROS 2 환경을 source하고
$ROS_DISTRO를 확인했다 - workspace/package/node의 관계를 설명할 수 있다
- turtlesim을 실행하고 조작해봤다
ros2 node list/node info로 실행 중인 노드를 확인할 수 있다
인증 스크린샷 첨부
turtlesim 창과 조작 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
토픽, 서비스, 액션
이론 강의DDS라는 바닥 위에서, ROS2는 세 가지 통신 패턴을 제공합니다. 셋 다 "메시지를 주고받는다"는 점은 같지만, 데이터의 성격이 다르기 때문에 존재합니다.
비교표
| 패턴 | 동기/비동기 | 관계 | 적합한 데이터 |
|---|---|---|---|
| 토픽 | 비동기 | 다대다 (0명도 가능) | 계속 흐르는 센서값, 상태 스트림 |
| 서비스 | 동기 (블로킹) | 1:1 | 빠르게 끝나는 질의/계산 (예: 좌표 변환 1회 요청) |
| 액션 | 비동기 + 진행상황 보고 | 1:1 (서버는 여러 클라이언트 가능) | 오래 걸리고 중간에 취소할 수도 있는 작업 (예: 목적지까지 이동) |
간단한 퀴즈
1. 값이 계속 흐르는 센서 데이터(예: 라이다 스캔값)에 가장 적합한 통신 패턴은?
2. "저 좌표까지 이동해줘"처럼 오래 걸리고 중간에 취소할 수도 있는 작업에 적합한 것은?
3. 서비스(Service) 통신의 특징으로 옳은 것은?
ROS 2 Topic·CLI
실습 가이드앞 페이지에서 본 토픽 개념을 이제 turtlesim으로 직접 조작해봅니다. GUI 없이 터미널만으로 토픽을 살펴보고 값을 직접 발행하는 법을 익힙니다.
- topic list/info/type/echo/pub/hz를 쓸 수 있다
- 메시지 타입의 구조를 확인할 수 있다
- QoS가 무엇을 조절하는지 직관적으로 이해한다
topic CLI 명령어
$ ros2 topic list
$ ros2 topic info /turtle1/cmd_vel
$ ros2 topic type /turtle1/cmd_vel
$ ros2 topic echo /turtle1/pose
$ ros2 topic hz /turtle1/pose
# 터미널에서 직접 토픽에 값 발행하기
$ ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 2.0}, angular: {z: 1.8}}"
메시지 타입이란
토픽마다 정해진 메시지 타입이 있고, 그 구조는 CLI로 바로 확인할 수 있습니다.
$ ros2 interface show geometry_msgs/msg/Twist
Vector3 linear
Vector3 angular
QoS 직관
Level 2 초반의 OS 레벨 통신 페이지에서 본 DDS의 QoS를 실제로 확인해봅니다.
- Reliability —
reliable(전달 보장, 느릴 수 있음) vsbest_effort(유실 허용, 빠름) - History/Depth — 최근 몇 개의 메시지를 버퍼에 유지할지
$ ros2 topic info -v /turtle1/pose
# Publisher/Subscriber 각각의 QoS 프로파일이 출력됩니다
자가 점검
topic list/info/type/echo로 토픽을 살펴볼 수 있다topic pub으로 값을 직접 발행할 수 있다topic hz로 발행 주기를 확인할 수 있다- reliable과 best_effort의 차이를 설명할 수 있다
인증 스크린샷 첨부
ros2 topic pub으로 거북이가 움직인 화면이나 topic echo 출력 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
제어주기 (Control Loop)
이론 강의모든 로봇 제어는 결국 센싱 → 계산 → 구동을 정해진 주기로 반복하는 루프입니다. 이 페이지에서는 주기, 주파수, 지터라는 개념과, 그것이 왜 로봇의 안정성에 직결되는지를 다룹니다.
주기, 주파수, 지터
- 주기 (Period, T) — 루프 한 바퀴가 도는 데 걸리는 시간. 예: 1ms.
- 주파수 (Frequency) — 1초에 루프가 몇 번 도는지.
f = 1/T. 1ms 주기라면 1000Hz. - 지터 (Jitter) — 매 주기가 정확히 T초마다 도는 게 아니라 조금씩 흔들리는 정도. 지터가 크면 제어기가 예측한 대로 모터가 반응하지 않아 진동·불안정이 생긴다.
계층적 주기 구조
| 레벨 | 대표 주기 | 특징 |
|---|---|---|
| 모터/전류 제어 | 1~10 kHz | 지터에 극도로 민감, 전용 MCU/실시간 OS |
| 궤적/자세 제어 | 100Hz~1kHz | 준실시간, RT 패치 리눅스 등에서 처리 |
| 경로 계획 | 1~10Hz | 일반 리눅스/WSL2로도 충분 |
| 인지/의사결정 | 수 Hz 이하 | GPU 연산 위주, 지연에 비교적 관대 |
상위로 갈수록 느려지고, 지터에 관대해집니다. 이 페이지 이전에 본 통신 패턴(토픽/서비스/액션)도 이 계층 구조와 맞물립니다 — 고주파 모터 제어는 토픽보다 더 가벼운 직접 통신을 쓰는 경우도 많습니다.
간단한 퀴즈
1. 주기(Period)가 1ms일 때, 주파수는?
2. 지터(Jitter)에 대한 설명으로 옳은 것은?
3. WSL2로 고주파(수백~수천 Hz) 모터 제어를 직접 돌리기 어려운 가장 큰 이유는?
ROS 2 Python (rclpy)
실습 가이드지금까지는 커맨드라인으로 토픽을 조작했다면, 이제 파이썬 코드로 직접 노드를 만듭니다. 다음 페이지의 상태 머신도 이 rclpy 노드 위에서 동작합니다.
- rclpy로 퍼블리셔·구독자 노드를 만들 수 있다
- 타이머 콜백으로 주기적인 동작을 만들 수 있다
colcon build로 패키지를 빌드하고 실행할 수 있다
Publisher 예제
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class SimpleMover(Node):
def __init__(self):
super().__init__('simple_mover')
self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
self.timer = self.create_timer(0.5, self.on_timer) # 0.5초마다 호출
def on_timer(self):
msg = Twist()
msg.linear.x = 1.0
self.pub.publish(msg)
def main():
rclpy.init()
node = SimpleMover()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Subscriber 예제
from turtlesim.msg import Pose
class PoseListener(Node):
def __init__(self):
super().__init__('pose_listener')
self.create_subscription(Pose, '/turtle1/pose', self.on_pose, 10)
def on_pose(self, msg):
self.get_logger().info(f"x={msg.x:.2f}, y={msg.y:.2f}")
colcon build
$ cd ~/ros2_ws $ colcon build --packages-select my_package $ source install/setup.bash $ ros2 run my_package simple_mover
코드를 고칠 때마다 colcon build → source install/setup.bash를 반복합니다. 파이썬 전용 패키지는 --symlink-install 옵션을 쓰면 매번 다시 빌드하지 않아도 됩니다.
자주 나는 오류
| 증상 | 주로 원인 |
|---|---|
Package 'my_package' not found | source install/setup.bash를 안 했거나, 워크스페이스 밖에서 실행함 |
ModuleNotFoundError: No module named 'my_package' | setup.py의 packages 항목이나 entry_points 설정 오타 |
| 코드를 고쳤는데 반영이 안 됨 | colcon build를 다시 안 했거나, 새 터미널에서 source를 다시 안 함 — --symlink-install로 재빌드 없이 반영되게 할 수 있음 |
rclpy.spin()에서 아무 반응 없음 | 퍼블리셔/구독자의 토픽 이름이 오타로 서로 다름 — ros2 topic list로 실제 이름 확인 |
rclpy.spin() 안에서 이벤트(타이머, 메시지 도착 등)를 기다리다가, 이벤트가 오면 등록해둔 콜백 함수를 실행하는 구조입니다. Level 2 초반의 제어주기 페이지에서 본 "일정 주기로 반복"이 바로 create_timer로 구현됩니다.
자가 점검
- 퍼블리셔 노드를 만들고 실행할 수 있다
- 구독자 노드로 메시지를 받아 처리할 수 있다
- 타이머 콜백의 동작 방식을 설명할 수 있다
colcon build로 패키지를 빌드할 수 있다
인증 스크린샷 첨부
직접 만든 노드가 실행되어 거북이가 움직이거나 로그가 출력되는 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
상태 머신 (python-statemachine)
실습 가이드로봇 행동 로직은 대부분 몇 가지 상태를 오가는 구조입니다 — 대기, 탐색, 접근, 파지, 복귀처럼요. python-statemachine은 이 구조를 코드로 명확하게 표현하게 해주는 라이브러리입니다. 앞의 제어주기 페이지가 "얼마나 자주 도는지"를 다뤘다면, 이 페이지는 "지금 무엇을 하고 있는지"를 다룹니다.
- State와 transition으로 로봇 행동을 모델링할 수 있다
- 상태 진입/이탈 시 실행되는 콜백을 쓸 수 있다
- 조건(guard)으로 전이를 제한할 수 있다
설치
$ uv add python-statemachine
Level 1에서 배운 uv로 그대로 설치합니다.
실습 과제 — 로봇 행동 상태 머신
from statemachine import StateChart, State
class RobotBehavior(StateChart):
idle = State(initial=True)
searching = State()
approaching = State()
grasping = State()
returning = State(final=True)
start_search = idle.to(searching)
object_found = searching.to(approaching)
object_lost = approaching.to(searching)
in_reach = approaching.to(grasping)
grasp_done = grasping.to(returning)
def on_enter_searching(self):
print("탐색 시작")
def on_enter_grasping(self):
print("파지 시도")
robot = RobotBehavior()
robot.send("start_search") # idle -> searching
robot.send("object_found") # searching -> approaching
robot.send("in_reach") # approaching -> grasping
robot.send("grasp_done") # grasping -> returning
조건부 전이 (Guard)
단순히 상태를 옮기는 것뿐 아니라, 특정 조건이 맞을 때만 전이가 일어나도록 제한할 수 있습니다.
approach_or_retry = (
approaching.to(grasping, cond="close_enough")
| approaching.to(searching)
)
def close_enough(self, distance: float = 999) -> bool:
return distance < 0.1
distance가 0.1 미만이면 grasping으로, 아니면 다시 searching으로 돌아갑니다 — 파지 거리에 도달하지 못했으면 재탐색하는 흐름을 코드 한 줄로 표현한 것입니다.
if-elif 사슬은 어떤 상태에서 어떤 전이가 허용되는지 코드를 다 읽어야만 알 수 있게 됩니다. 상태 머신은 전이 목록 자체가 곧 문서이고, 허용되지 않은 전이를 시도하면 예외를 던져줘서 버그를 훨씬 일찍 잡을 수 있습니다.
ROS2 액션과 연동하기
실전에서는 상태 전이가 곧 ROS2 액션 호출로 이어지는 경우가 많습니다. 상태에 진입하면 액션 목표(goal)를 보내고, 액션이 끝나면 그 결과가 다음 상태 전이를 발생시키는 식입니다. 앞에서 본 토픽·서비스·액션 페이지의 액션 패턴이 실제로 이렇게 쓰입니다.
예시 1 — 로봇 팔 궤적 실행 (FollowJointTrajectory)
from rclpy.action import ActionClient
from control_msgs.action import FollowJointTrajectory
class RobotBehavior(StateChart):
idle = State(initial=True)
moving_arm = State()
done = State(final=True)
start_move = idle.to(moving_arm)
move_finished = moving_arm.to(done)
def __init__(self, node):
self.node = node
self._client = ActionClient(
node, FollowJointTrajectory, "arm_controller/follow_joint_trajectory"
)
super().__init__()
def on_enter_moving_arm(self):
goal = FollowJointTrajectory.Goal()
goal.trajectory = build_trajectory() # 관절 궤적 구성
self._client.wait_for_server()
future = self._client.send_goal_async(goal)
future.add_done_callback(self._on_goal_response)
def _on_goal_response(self, future):
goal_handle = future.result()
result_future = goal_handle.get_result_async()
result_future.add_done_callback(self._on_result)
def _on_result(self, future):
self.send("move_finished") # 액션 완료 -> 상태 전이
예시 2 — 목적지까지 이동 (Nav2 NavigateToPose)
from nav2_msgs.action import NavigateToPose
class RobotBehavior(StateChart):
idle = State(initial=True)
navigating = State()
arrived = State(final=True)
nav_failed = State()
start_nav = idle.to(navigating)
nav_done = navigating.to(arrived)
nav_error = navigating.to(nav_failed)
def on_enter_navigating(self):
goal = NavigateToPose.Goal()
goal.pose = self.target_pose
self._nav_client.wait_for_server()
future = self._nav_client.send_goal_async(
goal, feedback_callback=self._on_feedback
)
future.add_done_callback(self._on_nav_response)
def _on_feedback(self, feedback_msg):
remaining = feedback_msg.feedback.distance_remaining
print(f"남은 거리: {remaining:.2f}m")
def _on_nav_response(self, future):
goal_handle = future.result()
result_future = goal_handle.get_result_async()
result_future.add_done_callback(self._on_nav_result)
def _on_nav_result(self, future):
status = future.result().status
if status == 4: # STATUS_SUCCEEDED
self.send("nav_done")
else:
self.send("nav_error")
feedback(남은 거리, 진행률 등)은 로그를 찍거나 상태 머신의 컨텍스트 데이터로 저장해두면 디버깅에 유용합니다. 그리고 액션이 실패하거나 취소됐을 때를 위한 별도의 실패 상태(예: nav_failed)를 꼭 만들어두세요 — "성공만 가정한 상태 머신"은 실제 로봇에서 반드시 문제가 됩니다.
GUI로 현재 상태 확인하기
python-statemachine은 상태 다이어그램을 이미지로 그려주는 기능을 내장하고 있습니다. Graphviz 기반이라, 지금 어떤 상태에 있고 어떤 전이가 가능한지 그림으로 바로 확인할 수 있습니다.
$ sudo apt install graphviz $ uv add "python-statemachine[diagrams]"
robot = RobotBehavior()
robot.send("start_search")
# 현재 상태를 강조한 다이어그램을 이미지로 저장
robot._graph().write_png("robot_state.png")
터미널에서 바로 그리고 싶다면 CLI로도 가능합니다:
$ python -m statemachine.contrib.diagram my_module.RobotBehavior robot_state.png
on_enter_* 콜백 안에서 write_png를 다시 호출하면 이미지 파일이 계속 갱신되어 "거의 실시간"으로 볼 수 있고, Jupyter 노트북에서는 상태 머신 인스턴스를 셀에 그대로 출력하면 현재 상태가 강조된 다이어그램이 자동으로 렌더링됩니다.
자가 점검
- State와 transition으로 로봇 행동을 모델링할 수 있다
on_enter_X/on_exit_X콜백을 쓸 수 있다cond로 조건부 전이를 제한할 수 있다- 상태 진입 시 ROS2 액션 goal을 보내고, 결과 콜백에서 다음 상태로 전이시킬 수 있다
- 액션 실패 시 별도 상태로 전이시키는 방법을 안다
- 왜 if-elif 사슬 대신 상태 머신을 쓰는 게 나은지 설명할 수 있다
- 상태 다이어그램을 이미지로 내보내 현재 상태를 확인할 수 있다
인증 스크린샷 첨부
상태 전이 결과나 ROS2 액션 goal/result 로그가 보이는 실행 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
ROS 2 응용 기초
실습 가이드토픽과 노드를 다뤘으니, 이제 서비스·액션·파라미터, 여러 노드를 한 번에 켜는 launch, 로그를 기록하는 rosbag2, 그리고 기본 디버깅 도구까지 훑습니다.
- 서비스와 액션을 CLI로 호출할 수 있다
- 파라미터와 launch 파일의 역할을 이해한다
- rosbag2로 데이터를 기록·재생할 수 있다
- rqt_graph로 노드 연결을 시각적으로 확인할 수 있다
서비스(Service)
$ ros2 service list $ ros2 service call /clear std_srvs/srv/Empty
액션(Action)
Level 2의 상태 머신 페이지에서 다룬 FollowJointTrajectory, NavigateToPose 같은 액션도 CLI로 먼저 감을 잡을 수 있습니다.
$ ros2 action list
$ ros2 action send_goal /turtle1/rotate_absolute \
turtlesim/action/RotateAbsolute "{theta: 1.57}"
파라미터(Parameter)
$ ros2 param list $ ros2 param get /turtlesim background_r $ ros2 param set /turtlesim background_r 150
launch 파일
노드를 하나씩 ros2 run으로 켜는 대신, 여러 노드를 한 번에 켜고 파라미터까지 넘길 수 있습니다.
# launch/my_robot.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='turtlesim', executable='turtlesim_node'),
Node(package='my_package', executable='simple_mover'),
])
$ ros2 launch my_package my_robot.launch.py
rosbag2로 로그 기록/재생
$ ros2 bag record /turtle1/pose /turtle1/cmd_vel $ ros2 bag play rosbag2_2026_.../
실제 로봇을 매번 다시 켜지 않고, 기록해둔 토픽 데이터를 재생해서 디버깅할 수 있습니다.
rqt와 기본 디버깅
$ rqt_graph # 노드-토픽 연결을 그래프로 확인 $ ros2 doctor # 환경/설정 문제를 점검 $ ros2 run my_package simple_mover --ros-args --log-level debug
자가 점검
- 서비스를
ros2 service call로 호출할 수 있다 - 액션을
ros2 action send_goal로 호출할 수 있다 - launch 파일로 여러 노드를 한 번에 실행할 수 있다
- rosbag2로 데이터를 기록·재생할 수 있다
- rqt_graph로 노드 연결 구조를 확인할 수 있다
인증 스크린샷 첨부
rqt_graph 화면이나 launch 파일 실행 결과를 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Level 2 미니 프로젝트
실습 가이드turtlesim을 상태 머신으로 조종하는 작은 미션을 만듭니다. 토픽, rclpy 노드, 상태 머신, launch, rosbag까지 이 레벨에서 배운 걸 전부 씁니다.
- 목표 지점까지 이동 → 도착하면 회전 → 종료 순서로 동작하는 상태 머신 노드
- 거북이 위치 토픽을 구독해서 도착 여부를 판단
- launch 파일로 turtlesim + 내 노드를 한 번에 실행
- 전체 과정을 rosbag2로 기록
1. 상태 설계
from statemachine import StateChart, State
class TurtleMission(StateChart):
idle = State(initial=True)
moving = State()
arrived = State(final=True)
start = idle.to(moving)
reach_goal = moving.to(arrived)
Level 2의 상태 머신 페이지에서 본 구조 그대로입니다 — 이번엔 ROS2 콜백이 reach_goal을 트리거합니다.
2. rclpy 노드에 연결
import math
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from turtlesim.msg import Pose
GOAL = (8.0, 8.0)
class MissionNode(Node):
def __init__(self):
super().__init__('turtle_mission')
self.mission = TurtleMission()
self.pub = self.create_publisher(Twist, '/turtle1/cmd_vel', 10)
self.create_subscription(Pose, '/turtle1/pose', self.on_pose, 10)
self.mission.send('start')
def on_pose(self, pose):
dx, dy = GOAL[0] - pose.x, GOAL[1] - pose.y
dist = math.hypot(dx, dy)
if self.mission.current_state.id == 'moving':
if dist < 0.3:
self.mission.send('reach_goal')
self.pub.publish(Twist()) # 정지
else:
msg = Twist()
msg.linear.x = min(dist, 1.5)
msg.angular.z = math.atan2(dy, dx) - pose.theta
self.pub.publish(msg)
def main():
rclpy.init()
node = MissionNode()
rclpy.spin(node)
if __name__ == '__main__':
main()
3. launch 파일로 묶기
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='turtlesim', executable='turtlesim_node'),
Node(package='my_package', executable='turtle_mission'),
])
$ colcon build --packages-select my_package $ source install/setup.bash $ ros2 launch my_package mission.launch.py
4. rosbag으로 기록
$ ros2 bag record /turtle1/pose /turtle1/cmd_vel -o mission_run
topic echo로 값이 실제로 나가는지 확인하고, 빌드 에러는 ROS 2 Python 페이지의 colcon 절차를, 상태 전이가 이상하면 상태 머신 페이지를 다시 보세요.
자가 점검
- 거북이가 목표 지점까지 이동한 뒤 정지한다
- launch 파일 하나로 전체가 실행된다
- rosbag으로 실행 과정을 기록했다
인증 스크린샷 첨부
거북이가 목표에 도달한 화면이나 rosbag 기록 결과를 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
URDF 기초
실습 가이드로봇의 생김새와 관절 구조를 코드로 표현하는 표준 포맷이 URDF(Unified Robot Description Format)입니다. OMY 같은 실제 로봇 팔도 URDF로 기술되어 있습니다.
- URDF가 XML로 link와 joint를 어떻게 표현하는지 안다
- origin·axis·limit이 각각 무엇을 뜻하는지 안다
- visual·collision·inertial의 역할 차이를 설명할 수 있다
먼저 읽을 것
DOCURDF는 XML이다
<robot name="simple_arm">
<link name="base_link"/>
<link name="link1"/>
<joint name="joint1" type="revolute">
<parent link="base_link"/>
<child link="link1"/>
<origin xyz="0 0 0.1" rpy="0 0 0"/>
<axis xyz="0 0 1"/>
<limit lower="-1.57" upper="1.57" effort="10" velocity="1.0"/>
</joint>
</robot>
link와 joint
- link — 강체(rigid body) 하나. 로봇 팔의 팔뚝, 손목 같은 물리적인 조각.
- joint — 두 link를 잇는 관절.
parent/child로 어느 link가 어느 link에 붙어있는지 트리 구조를 이룹니다.
origin, axis, limit
| 속성 | 의미 |
|---|---|
origin | parent 기준으로 child가 놓이는 위치·자세 오프셋 (xyz, rpy) |
axis | 관절이 회전(또는 이동)하는 축 방향 |
limit | 관절 각도(lower/upper), 최대 토크(effort), 최대 속도(velocity) 제한 |
visual, collision, inertial
- visual — 화면에 보이는 모양 (정밀한 3D 메시 사용 가능)
- collision — 충돌 계산용 형상 (계산량을 줄이려 단순한 박스/실린더로 대체하는 경우가 많음)
- inertial — 질량과 관성 텐서. 동역학 시뮬레이션(MuJoCo 등)에 필수
check_urdf my_robot.urdf로 문법과 트리 구조가 올바른지 확인하세요. 태그 하나만 잘못 닫아도 전체가 실패합니다.
자가 점검
- link와 joint의 관계(parent/child)를 설명할 수 있다
- origin/axis/limit의 역할을 구분할 수 있다
- visual/collision/inertial의 차이를 설명할 수 있다
- 최소 2-link URDF를 작성하고
check_urdf로 검증했다
인증 스크린샷 첨부
직접 작성한 URDF 파일이나 check_urdf 통과 결과 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
URDF와 OMY
실습 가이드URDF를 손으로 다 쓰는 건 관절이 많아지면 금방 지겨워집니다. Xacro로 반복을 줄이고, TF2/RViz2로 실제로 로봇 모델을 눈으로 확인하고, 우리 연구실의 OMY 로봇 팔 구조를 직접 읽어봅니다.
- Xacro로 URDF의 반복을 줄이는 이유를 안다
- robot_state_publisher/joint_state_publisher의 역할을 안다
- RViz2에서 로봇 모델과 TF 트리를 확인할 수 있다
- OMY의 URDF/xacro 파일 구조를 찾아 읽을 수 있다
Xacro로 반복 줄이기
관절 6개짜리 팔을 URDF로 그대로 쓰면 거의 같은 코드를 6번 반복하게 됩니다. Xacro는 매크로 문법으로 이를 줄여줍니다.
<xacro:macro name="arm_joint" params="name parent child">
<joint name="${name}" type="revolute">
<parent link="${parent}"/>
<child link="${child}"/>
</joint>
</xacro:macro>
<xacro:arm_joint name="joint1" parent="base_link" child="link1"/>
확장자는 .urdf.xacro이고, xacro my_robot.urdf.xacro > my_robot.urdf로 최종 URDF를 뽑아냅니다.
robot_state_publisher / joint_state_publisher
- robot_state_publisher — URDF와 현재 관절 각도를 받아서 각 link의 3D 위치·자세를 TF로 계산해 뿌리는 노드
- joint_state_publisher — 관절 각도값(
/joint_states)을 만들어 보내는 노드 (실습에서는 GUI 슬라이더로 직접 움직여볼 수 있음)
$ ros2 launch my_robot_description display.launch.py
TF2와 RViz2
TF는 로봇의 모든 좌표계 사이의 관계(트리)를 실시간으로 관리하는 시스템입니다.
$ ros2 run tf2_tools view_frames # 현재 TF 트리를 PDF로 저장 $ rviz2 # Fixed Frame을 base_link로, RobotModel/TF 디스플레이 추가
OMY 구조 읽기
OMY는 ROBOTIS의 6-DOF 로봇 팔로, open_manipulator 패키지가 ros2_control 기반으로 제어를 제공합니다. 실습으로 이 패키지 안의 xacro/URDF 파일을 직접 찾아 관절 구조를 읽어보세요.
$ ros2 pkg prefix open_manipulator_description $ find $(ros2 pkg prefix open_manipulator_description) -name "*.xacro"DOC
자가 점검
- Xacro 매크로로 반복되는 URDF 구조를 줄일 수 있다
- robot_state_publisher와 joint_state_publisher의 역할 차이를 설명할 수 있다
- RViz2에서 로봇 모델과 TF를 확인했다
- OMY의 xacro 파일에서 관절 개수와 이름을 확인했다
인증 스크린샷 첨부
RViz2에 로봇 모델이 표시된 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
기구학 기초
이론 강의URDF로 로봇의 구조를 표현했다면, 이제 "관절 각도가 주어지면 손끝이 어디에 있는가"를 계산하는 기구학을 다룹니다. MuJoCo 시뮬레이션과 실제 로봇 제어 모두 이 위에서 동작합니다.
좌표계와 자유도(DOF)
로봇을 다룰 때는 여러 좌표계가 동시에 존재합니다 — world(전체 공간 기준), base(로봇 베이스 기준), end-effector(손끝 기준). 자유도(DOF)는 로봇이 독립적으로 움직일 수 있는 방향의 개수입니다. OMY는 6-DOF로, 3차원 공간에서 위치(3) + 자세(3)를 모두 자유롭게 제어할 수 있습니다.
관절공간 vs 작업공간
| 관절공간 (Joint Space) | 작업공간 (Task/Cartesian Space) | |
|---|---|---|
| 표현하는 것 | 각 관절의 각도 (θ1, θ2, ...) | 엔드이펙터의 위치·자세 (x, y, z, roll, pitch, yaw) |
| 직관성 | 로봇 입장에서 자연스러움 | 사람 입장에서 자연스러움 ("여기로 가") |
2R 순기구학 (Forward Kinematics)
관절 길이가 각각 l1, l2인 평면 2관절 팔에서, 관절각(θ1, θ2)이 주어지면 손끝 위치는 다음과 같이 계산됩니다:
x = l1 * cos(θ1) + l2 * cos(θ1 + θ2) y = l1 * sin(θ1) + l2 * sin(θ1 + θ2)
Python 기초 3에서 배운 NumPy의 np.cos/np.sin이 바로 이런 계산에 쓰입니다. 순기구학은 관절공간 → 작업공간 방향의 계산이고, 관절이 몇 개든 입력이 정해지면 답은 항상 하나로 정해집니다.
역기구학 (Inverse Kinematics) 개념
역기구학은 순기구학의 반대 방향입니다: "손끝이 여기 있으려면 관절 각도는 얼마여야 하는가?" 순기구학과 달리 해가 여러 개이거나(팔꿈치를 위로 굽히거나 아래로 굽히거나 — elbow up/down) 아예 없을 수도 있습니다(닿을 수 없는 위치). 2R처럼 단순한 경우는 삼각함수로 직접 풀 수 있지만, 관절이 많아지면 보통 자코비안(Jacobian)을 이용해 수치적으로 반복 계산해서 근사해를 찾습니다. MoveIt 2 같은 도구가 이 계산을 대신해줍니다.
자코비안(Jacobian) — 순기구학을 미분한 것
자코비안은 별개의 새로운 개념이 아니라, 방금 본 순기구학 식 x = f(θ)을 각 관절각으로 미분한 결과입니다. "관절 각도가 아주 조금(dθ) 바뀌면, 손끝 위치는 얼마나(dx) 바뀌는가"를 나타내는 행렬입니다.
x = l1 * cos(θ1) + l2 * cos(θ1 + θ2) y = l1 * sin(θ1) + l2 * sin(θ1 + θ2) # 각 식을 θ1, θ2로 각각 편미분한다 ∂x/∂θ1 = -l1*sin(θ1) - l2*sin(θ1+θ2) ∂x/∂θ2 = -l2*sin(θ1+θ2) ∂y/∂θ1 = l1*cos(θ1) + l2*cos(θ1+θ2) ∂y/∂θ2 = l2*cos(θ1+θ2) # 이 네 개의 편미분을 행렬로 모으면 자코비안 J(θ) J(θ) = [[ ∂x/∂θ1, ∂x/∂θ2 ], [ ∂y/∂θ1, ∂y/∂θ2 ]]
즉 자코비안은 작업공간 속도와 관절공간 속도를 잇는 다리입니다:
ẋ = J(θ) · θ̇ # 관절이 이 속도로 움직이면, 손끝은 이 속도로 움직인다
자코비안을 어디에 쓰는가
- 속도 제어 — "손끝을 이 방향으로 이 속도로 움직여라"는 명령을 관절 속도로 바꿀 때:
θ̇ = J⁻¹ · ẋ - 수치적 역기구학 — 목표 위치와 현재 위치의 오차를 자코비안(의 역행렬)으로 관절각 보정량으로 바꿔서, 조금씩 목표에 다가가는 반복 계산에 씁니다.
- 특이점(Singularity) 진단 — 자코비안이 역행렬을 가질 수 없는(
det(J) = 0) 자세에서는, 특정 방향으로 아무리 관절을 움직여도 손끝이 그 방향으로 움직이지 못합니다. 2R 팔이 완전히 펴진 자세(θ2 = 0)가 대표적인 예입니다.
수치적 역기구학 — 자코비안을 반복 적용하기
관절이 많아 삼각함수로 직접 못 풀 때, 실제로는 이런 식으로 목표에 조금씩 다가갑니다:
θ = θ_initial
repeat:
x_current = forward_kinematics(θ)
error = x_target - x_current
if norm(error) < tolerance:
break
dtheta = inverse(J(θ)) @ error # 자코비안(역행렬)로 오차를 관절 보정량으로 변환
θ = θ + dtheta
한 번에 정답을 계산하는 게 아니라, "현재 위치와 목표의 차이 → 자코비안으로 관절 보정량 계산 → 조금 이동" 을 여러 번 반복해서 수렴시키는 방식입니다. MuJoCo 통합 실습 페이지에서 다룰 관절 제어도 이 감각 위에서 이해하면 훨씬 쉽습니다.
간단한 퀴즈
1. "관절 각도가 주어졌을 때 손끝 위치를 계산한다"에 해당하는 것은?
2. 역기구학이 순기구학보다 어려운 이유로 가장 알맞은 것은?
3. "관절 각도들의 조합"으로 로봇 자세를 표현하는 공간은?
4. 자코비안 J(θ)에 대한 설명으로 옳은 것은?
MuJoCo 기초
실습 가이드MuJoCo는 물리엔진 기반 로봇 시뮬레이터입니다. 실제 하드웨어 없이 URDF/기구학에서 배운 개념을 바로 눈으로 확인할 수 있습니다. 이 레벨의 실습은 전부 ROBOTIS의 실제 로봇 팔 OMY 모델로 진행합니다.
- OMY의 공식 MuJoCo 모델을 내려받고 뷰어로 띄울 수 있다
- MjModel과 MjData의 역할 차이를 설명할 수 있다
- 모델의 관절 목록을 코드로 확인할 수 있다
설치와 모델 내려받기
$ uv add mujoco $ git clone https://github.com/ROBOTIS-GIT/robotis_mujoco_menagerie.git
이 저장소 안에 OMY를 포함한 ROBOTIS 로봇들의 MuJoCo 모델(MJCF, .xml)이 들어있습니다. 아래 코드들은 전부 이 저장소를 프로젝트 폴더 기준 상대경로로 clone했다고 가정합니다.
모델 로드와 뷰어 띄우기
import mujoco import mujoco.viewer MODEL_PATH = "robotis_mujoco_menagerie/robotis_omy/scene.xml" model = mujoco.MjModel.from_xml_path(MODEL_PATH) data = mujoco.MjData(model) mujoco.viewer.launch(model, data)
실행하면 OMY 로봇 팔이 뜬 인터랙티브 창이 열립니다. 마우스로 돌려보고, 스페이스바로 시뮬레이션을 멈췄다 재생해볼 수 있습니다.
launch() vs launch_passive()
mujoco.viewer.launch(model, data)는 뷰어를 띄우고 그 자리에서 블로킹됩니다 — 지금처럼 모델을 눈으로 확인만 할 때 편합니다. 반대로 다음 페이지부터 쓸 launch_passive()는 뷰어를 띄운 뒤 바로 코드로 돌아와서, 내가 짠 반복문 안에서 mj_step과 viewer.sync()를 직접 호출하며 제어할 수 있게 해줍니다. "그냥 구경"이면 launch, "코드로 움직이면서 구경"이면 launch_passive입니다.
관절 목록 확인하기
model = mujoco.MjModel.from_xml_path(MODEL_PATH)
data = mujoco.MjData(model)
print("관절 개수:", model.njnt)
for i in range(model.njnt):
name = model.joint(i).name
print(i, name)
OMY는 팔 관절 6개 + 그리퍼 1개로 구성됩니다. 이후 실습에서 인덱스로 관절을 지정할 때, 지금 출력된 이름과 순서를 그대로 기준으로 삼습니다.
MjModel과 MjData
- MjModel — 시뮬레이션 도중 바뀌지 않는 구조 정보 (질량, 관절 정의, 충돌 형상 등). MJCF 파일에서 한 번 읽어 만듭니다.
- MjData — 매 스텝 바뀌는 상태 정보 (현재 위치, 속도, 가해지는 힘 등). 시뮬레이션이 진행되며 계속 갱신됩니다.
qpos, qvel, ctrl
| 변수 | 의미 |
|---|---|
data.qpos | 관절 위치 (기구학 페이지의 관절공간 값). OMY는 qpos[:6]이 팔 관절 |
data.qvel | 관절 속도 |
data.ctrl | 액추에이터에 보내는 제어 입력. 팔 6개 + 그리퍼 1개, 총 7개 |
print(data.qpos) # 현재 관절 각도 (rad) data.ctrl[0] = 0.5 # 첫 번째 관절(Joint1)에 명령
mj_step / mj_forward
mujoco.mj_step(model, data)는 시뮬레이션을 정확히 한 스텝 전진시킵니다 — 물리 법칙(관성, 중력)까지 반영됩니다. Level 2 초반의 제어주기 페이지에서 본 "센싱 → 계산 → 구동" 루프의 시뮬레이션 버전이 바로 이 mj_step을 반복 호출하는 while 루프입니다. 반면 mj_forward는 스텝을 진행하지 않고, 지금의 qpos 기준으로 모든 부품 위치만 다시 계산합니다 — 다음 페이지의 순기구학 계산에 씁니다.
자가 점검
- OMY MuJoCo 모델을 clone하고 뷰어로 띄웠다
launch()와launch_passive()의 차이를 설명할 수 있다- 코드로 관절 개수와 이름을 출력했다
- MjModel과 MjData의 차이를 설명할 수 있다
mj_step과mj_forward의 차이를 설명할 수 있다
인증 스크린샷 첨부
OMY가 뜬 MuJoCo 뷰어 화면이나 관절 목록 출력 결과를 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
MuJoCo 관절 제어
실습 가이드모델을 띄우고 관절 이름까지 확인했으니, 이제 실제로 움직여봅니다. 관절 하나부터 시작해서, 여러 관절을 동시에, 그리고 부드러운 궤적으로 이어갑니다.
- 관절 하나, 그리고 여러 관절을 동시에 원하는 각도로 보낼 수 있다
- 선형보간으로 급격한 움직임 없이 부드럽게 이동시킬 수 있다
- sin 함수로 반복적인 궤적을 만들고 실시간으로 확인할 수 있다
단일 관절 제어
import mujoco import numpy as np MODEL_PATH = "robotis_mujoco_menagerie/robotis_omy/scene.xml" model = mujoco.MjModel.from_xml_path(MODEL_PATH) data = mujoco.MjData(model) data.ctrl[0] = np.deg2rad(45) # Joint1 을 45도로 mujoco.mj_step(model, data) # 시뮬레이션 1스텝 진행 print("현재 관절 위치 (rad) : ", data.qpos[:6])
np.deg2rad로 각도를 라디안으로 바꾸는 이유는, MuJoCo 내부는 전부 라디안 단위로 동작하기 때문입니다. 사람은 도(degree)로 생각하는 게 편하니 입력만 변환해줍니다.
여러 관절 동시 제어
target = np.deg2rad([45, -30, 60, 0, 0, 0]).tolist() + [0]
data.ctrl[:] = target
mujoco.mj_step(model, data)
print("현재 관절 위치 (rad) : ", data.qpos[:6])
배열 길이가 7인 이유: 팔 관절 6개(각도, deg→rad 변환) + 그리퍼 1개(맨 뒤 0, 별도 단위). MuJoCo 기초 페이지에서 확인한 관절 순서와 정확히 일치해야 원하는 관절이 원하는 각도로 움직입니다.
부드러운 궤적 — 선형보간
관절을 목표각으로 한 번에 확 보내면 튀는 동작이 나옵니다. 시작값과 목표값 사이를 여러 단계로 나눠 조금씩 이동시키면 훨씬 자연스럽습니다.
start = np.zeros(7)
target = np.deg2rad([45, -30, 60, 0, 0, 0]).tolist() + [0]
target = np.array(target)
for t in np.linspace(0, 1, 50):
data.ctrl[:] = start + (target - start) * t
mujoco.mj_step(model, data)
t가 0→1로 갈수록 start에서 target으로 조금씩 섞입니다. 50단계로 나눴으니 스텝 하나당 이동량이 1/50로 작아집니다.
사인 궤적으로 실시간 확인
직선 이동 대신 sin으로 속도 프로파일을 주면, 시작과 끝에서 부드럽게 가속·감속하는 왕복 동작을 만들 수 있습니다. 이번엔 뷰어를 직접 돌리면서 확인합니다.
import math
start = np.zeros(7)
target = np.deg2rad([45, -30, 120, 0, 0, 0]).tolist() + [0]
target = np.array(target)
with mujoco.viewer.launch_passive(model, data) as viewer:
for t in np.linspace(0, 1, 5000):
data.ctrl[:] = start + (target - start) * math.sin(t * math.pi * 2)
mujoco.mj_step(model, data)
viewer.sync()
MuJoCo 기초 페이지에서 본 것처럼, 스크립트로 매 스텝 제어하려면 launch_passive + viewer.sync() 조합을 씁니다. sin(t·2π)는 0 → 최대 → 0 → 최소 → 0으로 한 바퀴 왕복하는 곡선이라, 관절이 목표각까지 갔다가 반대로도 움직입니다.
자가 점검
- 관절 하나를 원하는 각도로 움직일 수 있다
- 여러 관절을 동시에 제어하고, 배열 순서(6+그리퍼)를 설명할 수 있다
- 선형보간으로 부드러운 이동을 구현할 수 있다
- sin 궤적을 실시간 뷰어로 확인했다
인증 스크린샷 첨부
사인 궤적으로 OMY가 왕복 운동하는 뷰어 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
MuJoCo 순기구학과 자코비안 (CLIK)
실습 가이드지금까지는 관절 각도를 직접 지정했습니다. 이제 반대로 풀어봅니다 — "손끝이 이 위치에 있으려면?". 기구학 페이지에서 손으로 미분해 유도했던 자코비안을, MuJoCo는 함수 호출 한 줄로 계산해줍니다.
- 순기구학으로 엔드이펙터의 실제 XYZ 위치를 읽을 수 있다
mj_jacBody로 자코비안을 구할 수 있다- 자코비안 역행렬로 목표 위치까지 이동하는 CLIK을 구현할 수 있다
끝단(엔드이펙터) 위치 읽기 — 순기구학
import mujoco import numpy as np np.set_printoptions(precision=3, suppress=True) # 출력을 소수점 3자리로, 지수표기 없이 MODEL_PATH = "robotis_mujoco_menagerie/robotis_omy/scene.xml" model = mujoco.MjModel.from_xml_path(MODEL_PATH) data = mujoco.MjData(model) mujoco.mj_forward(model, data) # 끝단(link6)의 XYZ 위치 읽기 ee_pos = data.body("link6").xpos print("끝단 위치:", ee_pos)
이게 바로 기구학 페이지에서 손으로 유도했던 순기구학 x = f(θ)입니다. 다만 여기서는 삼각함수 식을 직접 쓰는 대신, mj_forward가 지금 qpos 기준으로 모든 부품(body)의 위치를 계산해주고, data.body("link6").xpos로 그 결과를 읽기만 하면 됩니다.
MuJoCo로 자코비안 구하기
jacp = np.zeros((3, model.nv)) # 위치 자코비안 (3 x 전체 자유도) jacr = np.zeros((3, model.nv)) # 회전 자코비안 body_id = model.body("link6").id mujoco.mj_jacBody(model, data, jacp, jacr, body_id) J = jacp[:, :6] # 팔 관절(Joint1~6)에 대한 부분만 사용 print("자코비안 J:\n", J)
기구학 페이지에서 2R 팔을 예로 직접 편미분해서 자코비안을 유도했던 것, 기억나시나요? mj_jacBody는 그 계산을 3차원 공간·6관절 로봇에 대해 자동으로 해주는 함수입니다. jacp는 위치 자코비안(3행 — x,y,z), jacr은 회전 자코비안입니다. 여기서는 위치만 다루므로 jacp만 씁니다.
CLIK — 자코비안 역행렬로 목표까지 이동
CLIK(Closed-Loop Inverse Kinematics)은 "오차를 자코비안으로 관절 보정량으로 바꿔서 조금씩 다가간다"를 매 스텝 반복하는, 기구학 페이지에서 본 수치적 역기구학의 실전 버전입니다.
EE_BODY = "link6" READY_POSE_DEG = [0, 0, 90, 0, 0, 0] # 특이 자세를 피한 시작 자세 GOAL_POSITION = np.array([0.335, -0.113, 0.619]) # 목표 끝단 위치 [X, Y, Z] (m) N_STEPS = 2000 body_id = model.body(EE_BODY).id jacp = np.zeros((3, model.nv)) jacr = np.zeros((3, model.nv)) data.qpos[:6] = np.deg2rad(READY_POSE_DEG) data.ctrl[:6] = data.qpos[:6] mujoco.mj_forward(model, data) start_pos = data.body(EE_BODY).xpos.copy() with mujoco.viewer.launch_passive(model, data) as viewer: for t in np.linspace(0, 1, N_STEPS): target = start_pos + (GOAL_POSITION - start_pos) * t # 작업공간에서 선형보간 mujoco.mj_forward(model, data) mujoco.mj_jacBody(model, data, jacp, jacr, body_id) J = jacp[:, :6] error = target - data.body(EE_BODY).xpos # 자코비안 역행렬로 오차를 관절 보정량으로 변환 dq = J.T @ np.linalg.inv(J @ J.T) @ error data.qpos[:6] += dq # 관절 위치를 직접 덮어씀 (제어가 아니라 기구학 확인용) data.ctrl[:6] = data.qpos[:6] # ctrl도 그 값으로 그대로 맞춰서 "이미 거기 있다"고 강제 mujoco.mj_step(model, data) viewer.sync() mujoco.mj_forward(model, data) final_pos = data.body(EE_BODY).xpos print("오차:", np.linalg.norm(GOAL_POSITION - final_pos), "m")
ctrl에 목표값만 넣고, 액추에이터가 관성·힘 같은 물리 법칙에 따라 그 목표를 향해 서서히 도달하는 과정을 거칩니다. 그런데 위 코드는 data.qpos[:6] += dq로 관절 위치(position)를 매 스텝 직접 덮어쓰고, 그 값을 그대로 ctrl에도 복사합니다 — 액추에이터가 움직여 오길 기다리는 게 아니라 "이미 거기 도달했다"고 강제로 우겨넣는 것입니다. 지금 목적은 자코비안·역기구학이 기하학적으로 맞게 계산되는지 확인하는 것이라, 힘·관성 같은 진짜 제어 문제는 일부러 배제하고 포지션을 바로 꽂아 넣었습니다. 목표까지 부드럽고 안정적으로 도달하게 만드는 것(진짜 제어)은 이후 PID 같은 컨트롤러를 씌우는 별개의 문제입니다.
J.T @ inv(J @ J.T)가 정확히 무엇인가
기구학 페이지에서는 θ̇ = J⁻¹ · ẋ로 배웠지만, 여기서 J는 정사각행렬이 아닙니다(3행 — XYZ, 6열 — 관절 6개). 정사각이 아니면 진짜 역행렬은 없고, 대신 오른쪽 유사역행렬(right pseudo-inverse) J⁺ = Jᵀ(JJᵀ)⁻¹을 씁니다. 관절이 작업공간 차원(3)보다 많은(6) 여유(redundant) 로봇이라 가능한 방식이고, 남는 자유도만큼 같은 손끝 위치를 여러 관절 조합으로 만들 수 있습니다.
det(J)가 0에 가까운 자세에서는 J @ J.T가 거의 특이행렬이 되어 역행렬 계산이 불안정해지고 dq가 폭발적으로 커질 수 있습니다. 코드의 READY_POSE_DEG가 완전히 펴지거나 완전히 접힌 자세가 아닌 이유가 이것입니다.
숙제 — 여러 목표점을 순서대로 (사각 경로)
위 코드를 확장하면, 목표 지점 4개를 순서대로 돌아 사각형 경로를 그릴 수 있습니다.
goals = [GOAL_POSITION_1, GOAL_POSITION_2, GOAL_POSITION_3, GOAL_POSITION_4]
for goal in goals:
for t in np.linspace(0, 1, N_STEPS):
target = start_pos + (goal - start_pos) * t
# ... 위와 동일한 CLIK 루프 ...
start_pos = goal # 다음 목표를 위해 시작 위치를 갱신
원본 실습에 있던 또 다른 숙제도 시도해보세요: 선형보간 대신 math.sin으로 위아래로 왕복하는 경로를 만들면 어떻게 될까요? (이전 페이지의 사인 궤적과 같은 아이디어를, 관절공간이 아니라 작업공간에 적용하는 것입니다.)
자주 나는 오류
| 증상 | 주로 원인 |
|---|---|
from_xml_path에서 파일을 못 찾음 | 상대 경로 문제 — robotis_mujoco_menagerie를 clone한 위치 기준으로 경로가 맞는지 확인 |
| CLIK이 목표에 안 가고 발산함 | 특이 자세 근처에서 시작했거나, 목표가 로봇의 도달 범위 밖에 있음 |
LinAlgError: Singular matrix | J @ J.T가 특이행렬 — 자세를 바꾸거나 np.linalg.pinv처럼 더 안정적인 유사역행렬 함수로 교체 |
| 관절이 목표각으로 안 감 | ctrl 인덱스가 실제 액추에이터 순서와 다름 — MuJoCo 기초 페이지에서 확인한 관절 이름과 순서를 재확인 |
결과 GitHub 업로드
GitHub 사용법 페이지에서 배운 흐름 그대로, 코드와 실행 로그를 저장소에 올려 마무리합니다.
$ git add clik.py $ git commit -m "feat: CLIK-based end-effector control for OMY in MuJoCo" $ git push origin main
자가 점검
mj_forward로 엔드이펙터의 실제 위치를 읽었다mj_jacBody로 자코비안을 구했다- CLIK 루프로 목표 위치까지 로봇을 이동시켰다
- 이 예제가
ctrl에 목표를 주고 기다리는 "제어"가 아니라,qpos를 직접 덮어써서 기구학만 확인하는 방식이라는 걸 설명할 수 있다 - 왜 특이 자세를 피해서 시작해야 하는지 설명할 수 있다
- 결과를 GitHub에 커밋·푸시했다
인증 스크린샷 첨부
CLIK으로 OMY가 목표 위치까지 이동한 화면이나, 최종 오차가 출력된 콘솔 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
Overleaf 시작하기
실습 가이드논문, 기술보고서, 졸업논문까지 연구실 문서 대부분은 LaTeX으로 씁니다. Overleaf는 설치 없이 브라우저에서 바로 쓸 수 있는 LaTeX 편집기라서, 온보딩 단계에서는 로컬 LaTeX 설치 없이 여기서 시작합니다.
- Overleaf 계정을 만들고 새 프로젝트를 생성할 수 있다
- 템플릿을 골라 컴파일하고 PDF 결과를 확인할 수 있다
- 공동 편집자를 초대할 수 있다
실습 과제
- overleaf.com에서 계정을 만든다
- New Project → 원하는 템플릿(IEEE, 논문 초안 등)을 고르거나 Blank Project로 시작한다
- 우측 상단 Recompile을 눌러 PDF가 정상적으로 렌더링되는지 확인한다
- Share 버튼으로 팀원을 초대한다 — Editor(편집 가능)와 Viewer(읽기 전용) 권한을 구분해서 부여할 수 있다
자가 점검
- Overleaf 계정을 만들고 새 프로젝트를 생성했다
- Recompile로 PDF 결과물을 확인했다
- Share로 협업자를 초대해봤다 (또는 초대 방법을 안다)
- Overleaf에 문서 히스토리(버전 관리) 기능이 있다는 것을 안다
인증 스크린샷 첨부
컴파일된 PDF 미리보기가 보이는 Overleaf 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
LaTeX 문서 구조 & 수식
실습 가이드Overleaf 안에서 실제로 LaTeX 문법을 몇 줄 써봅니다. 처음엔 코드처럼 보이지만, 몇 가지 패턴만 익히면 논문 형식 문서를 빠르게 만들 수 있습니다.
\documentclass,\begin{document}...\end{document}구조를 이해한다- 수식·그림·표·참고문헌(BibTeX)을 문서에 넣을 수 있다
기본 문서 구조
\documentclass{article}
\usepackage{amsmath, graphicx}
\title{My First Report}
\author{Your Name}
\begin{document}
\maketitle
\section{Introduction}
로봇의 상태는 다음과 같이 표현됩니다:
\begin{equation}
\dot{x} = f(x, u)
\end{equation}
\begin{figure}[h]
\centering
\includegraphics[width=0.6\linewidth]{robot.png}
\caption{예시 그림}
\end{figure}
\end{document}
참고문헌 (BibTeX)
.bib 파일에 참고문헌 정보를 저장해두고, 본문에서는 \cite{}로 인용만 하면 번호와 참고문헌 목록이 자동으로 생성됩니다.
% references.bib @article{levine2016end, title={End-to-end training of deep visuomotor policies}, author={Levine, Sergey and others}, journal={JMLR}, year={2016} } % 본문에서 관련 연구로 \cite{levine2016end}가 있다.
amsmath(수식), graphicx(그림), algorithm2e 또는 algorithmic(의사코드), 그리고 학회 제출용 IEEEtran 같은 템플릿 클래스를 자주 쓰게 됩니다. 지금 다 외울 필요는 없고, 필요할 때 검색해서 \usepackage{}로 추가하면 됩니다.
자가 점검
\section,\subsection으로 문서 구조를 만들 수 있다equation같은 수식 환경을 쓸 수 있다- 그림을 넣고 캡션을 달 수 있다
\cite와.bib파일로 참고문헌을 관리하는 흐름을 안다
인증 스크린샷 첨부
수식·그림이 정상적으로 렌더링된 컴파일 결과 화면을 캡처해 올려두세요. 인증서 발급 시 증빙자료로 자동 첨부됩니다.
인증서 발급
PDF 생성각 실습 페이지에서 업로드한 성공 화면 스크린샷이 아래 요약에 모두 반영됩니다. 이름을 입력하고 생성하면 1페이지 수료증 + 뒤에 증빙 스크린샷이 포함된 PDF가 다운로드됩니다.