아들과 함께 만들어보는 인공지능(LLM) 로봇 만들기 프로젝트 — EP 2. AI 임베디드 코딩 첫 실험, 컨텍스트 전략이 코드 품질을 갈랐다
아들과 함께 만들어보는 인공지능(LLM) 로봇 만들기 프로젝트 — EP 2. AI 임베디드 코딩 첫 실험, 컨텍스트 전략이 코드 품질을 갈랐다
아들이 처음으로 AI에게 직접 코딩 명령을 내린 날이었다.
노트북 앞에 앉아서 잠시 뭘 써야 하나 생각하더니, 이렇게 입력했다.
"앞에 뭔가 있으면 멈추게 해줘"
Claude Code가 코드를 내놨다. 아들이 쭉 훑어봤다. 뭔지 잘 모르겠다는 표정이었다. "올려봐도 돼?" 해서 Arduino IDE에서 업로드했다. 로봇 앞에 손을 뻗었다.
안 멈췄다.
"왜 안 돼?"
그때 코드를 보니 Claude가 HC-SR04 Trig 핀을 2번으로 잡고 있었다. 우리 배선은 4번이다. CLAUDE.md에 명시해뒀던 핀맵이 그 대화에서 로드되지 않은 상태였다. 아들이 새 Claude Code 세션을 열었고, 그 세션에는 CLAUDE.md 컨텍스트가 없었다.
이 에피소드 하나가 이번 편에서 하고 싶은 얘기 전부다.
컨텍스트 없이 AI에게 임베디드 코드를 짜달라고 하면
HC-SR04는 꽤 유명한 초음파 거리 센서라 Claude가 기본 동작 원리는 안다. pulseIn()으로 에코 시간 재고, 340m/s로 나누고, 2로 나누면 거리가 나온다는 것도 안다. 그래서 이렇게만 물어봐도 어느 정도 작동하는 코드가 나온다.
"HC-SR04로 거리 재는 아두이노 코드 짜줘"
나온 코드:
#define TRIG_PIN 2
#define ECHO_PIN 3
void setup() {
Serial.begin(9600);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
void loop() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH);
float distance = duration * 0.034 / 2;
Serial.print("Distance: ");
Serial.println(distance);
delay(500);
}
작동한다. 문제는 이게 우리 로봇에는 안 쓸 수 있다는 것이다.
Trig가 2번이고 Echo가 3번이다. 우리 배선은 4번, 5번이다. delay(500)이 들어있다. EP 1에서 설명했듯이 루프 안에 delay가 들어가면 나중에 모터 제어와 함께 쓸 때 전체 루프가 블로킹된다. NewPing 라이브러리도 안 쓰고 있다.
이 코드를 그대로 올리면 지금은 작동하지만 다음 단계에서 문제가 생긴다.
RAG를 잠깐 고려했다가 포기한 이유
AI에게 도메인 지식을 주입하는 방법이 여러 가지 있다. 그중 RAG(Retrieval-Augmented Generation)를 잠깐 생각해봤다.
RAG는 문서를 벡터 DB에 저장해두고, 질문이 들어오면 관련 문서를 검색해서 컨텍스트로 끼워 넣는 방식이다. HC-SR04 데이터시트, L298N 핀맵, ROS2 문서를 넣어두면 Claude가 질문할 때마다 관련 부분을 검색해서 가져온다.
기술적으로는 완전히 가능하다. 문서 분량이 많아지면 오히려 CLAUDE.md보다 유리하다.
그런데 이 프로젝트에 RAG가 필요한가? 냉정하게 생각해봤다.
지금 하네스에 들어가는 도메인 지식의 양이 많지 않다. 핀맵 테이블 하나, 라이브러리 선택 이유 몇 줄, ROS2 토픽 규칙, 동작 제약 조건. 합쳐도 120줄이다. 이걸 매번 컨텍스트로 통째로 넣어도 Claude의 컨텍스트 윈도우 입장에서는 새 발의 피다.
RAG가 빛을 발하는 건 문서가 수백 페이지이거나, 대화마다 다른 부분을 참조해야 할 때다. 120줄짜리 CLAUDE.md에 벡터 DB를 붙이는 건 오버엔지니어링이다. 나중에 실제로 문서가 많아지면 그때 고려한다. 지금은 아니다.
컨텍스트 폭을 넓히는 실험
RAG 대신 시도한 게 "데이터시트를 통째로 붙여넣기"였다.
HC-SR04 데이터시트는 4페이지짜리 PDF다. 이걸 텍스트로 추출해서 프롬프트 앞에 붙이면 어떻게 되는지 테스트했다.
[HC-SR04 ULTRASONIC MODULE DATASHEET]
Working Voltage: 5V
Working Current: 15mA
Working Frequency: 40Hz
Max Range: 4m
Min Range: 2cm
...
(4페이지 분량 전문)
...
이 센서로 거리 재는 아두이노 코드 짜줘.
결과는 나쁘지 않았다. 데이터시트에 나온 타이밍 다이어그램을 읽고 pulseIn 타이밍도 더 정확하게 잡았다.
근데 두 가지 문제가 있었다.
하나는 핀 번호가 여전히 틀렸다. 데이터시트에 핀 번호가 있긴 한데, 그건 모듈 자체 핀 번호지 내 아두이노 배선 핀 번호가 아니다. 데이터시트를 아무리 읽어도 내가 Trig를 4번에 꽂았다는 건 알 수 없다.
다른 하나는 잡음이 많아진다. 데이터시트에는 Claude가 이미 알고 있는 정보, 이 프로젝트와 무관한 정보가 섞여 있다. 4페이지를 통째로 먹이니까 응답이 조금 더 장황해지고, 코드 설명이 길어졌다. 실제로 필요한 것 — 내 핀맵, 내 라이브러리 선택, 내 제약 조건 — 은 여기에 없다.
이 실험이 확인시켜준 건 하나다. 도메인 지식을 전달할 때는 원본 데이터를 통째로 던지는 게 아니라 내 프로젝트 맥락에 맞게 정제한 정보를 줘야 한다는 것. 데이터시트 4페이지보다 내가 직접 뽑은 핀맵 테이블 10줄이 더 효과적이다.
CLAUDE.md 방식과 비교
세 접근법을 같은 요청으로 테스트했다.
요청: "HC-SR04로 20cm 이하면 멈추는 아두이노 코드 짜줘"
컨텍스트 없음:
#define TRIG_PIN 2 // 틀림
#define ECHO_PIN 3 // 틀림
// ...
delay(100); // 금지 패턴
데이터시트 통째로:
const int trigPin = 9; // 여전히 틀림 — 예제 코드의 핀을 그대로 씀
const int echoPin = 10; // 여전히 틀림
// ...
delay(50); // 금지 패턴
CLAUDE.md 로드 후:
#include <NewPing.h>
#define TRIG_PIN 4 // 핀맵 정확
#define ECHO_PIN 5 // 핀맵 정확
#define MAX_DISTANCE 400
#define STOP_DISTANCE 20
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
// 모터 핀 (L298N)
#define IN1 8
#define IN2 9
#define ENA 10
#define IN3 11
#define IN4 12
#define ENB 13
unsigned long lastPingTime = 0;
const unsigned long PING_INTERVAL = 100;
void stopMotors() {
analogWrite(ENA, 0);
analogWrite(ENB, 0);
}
void loop() {
unsigned long now = millis();
if (now - lastPingTime >= PING_INTERVAL) {
lastPingTime = now;
unsigned int distance = sonar.ping_cm();
if (distance > 0 && distance <= STOP_DISTANCE) {
stopMotors();
}
}
}
NewPing 썼고, delay 없고, 핀 번호 정확하고, millis() 기반 비동기 처리까지 했다. CLAUDE.md에 다 적어둔 내용들이다.
차이가 선명하다. 같은 모델에게 같은 요청을 했는데, 뭘 알고 있느냐에 따라 코드 품질이 완전히 달라진다.
아들의 첫 시도 — 안 된 장면
다시 그날 저녁 이야기로 돌아간다.
아들이 새 세션을 열었던 건 나도 나중에 알았다. 아들은 그냥 "새로운 대화 시작"을 누른 거였다. Claude Code에서 새 세션을 열면 CLAUDE.md가 다시 로드된다는 건 알고 있었는데, 아들은 그걸 몰랐다. 세션을 새로 열어도 이전 대화가 기억된다고 생각한 것 같았다.
그래서 "앞에 뭔가 있으면 멈추게 해줘"라고 입력했을 때, CLAUDE.md 없는 상태에서 Claude가 가장 일반적인 코드를 짰다. Trig 2번, Echo 3번.
아들이 "왜 안 돼?"라고 물었을 때 설명을 해줬다.
"지금 세션에서 로봇 설명서가 안 불려있어. 새로 열면 자동으로 불려오는 게 맞는데, 지금 상태를 한번 확인해봐."
아들이 .claude/ 폴더를 열어봤다. CLAUDE.md가 거기 있었다. "그럼 왜 안 불려왔어?" 이걸 설명하는 게 좀 어려웠다. 세션 시작 시점에 로드된다는 것, 그 이후에 파일을 수정하거나 세션이 불안정하면 로드가 안 될 수도 있다는 것. 아들이 이해했는지는 모르겠다.
대신 이렇게 가르쳤다. "새 대화를 시작하고 나서, 코드를 짜달라기 전에 이렇게 한 번 물어봐."
"지금 이 프로젝트에서 HC-SR04 Trig 핀이 몇 번이야?"
아들이 그대로 입력했다. Claude가 답했다: "Trig 핀은 4번, Echo 핀은 5번입니다."
"이제 됐어?"
됐다. CLAUDE.md가 로드된 상태라는 걸 확인한 것이다. 이후에 "앞에 뭔가 있으면 멈추게 해줘"를 다시 입력하니 이번엔 핀 번호가 맞게 나왔다.
그 이후로 아들이 코딩 세션을 시작할 때마다 이걸 먼저 한다. 컨텍스트 확인 질문. 12살이 스스로 만든 루틴이다.
첫 실험에서 실제로 작동한 코드
그날 저녁 최종적으로 올린 코드가 이거다. HC-SR04로 20cm 이하 장애물 감지하면 모터 정지.
#include <NewPing.h>
#define TRIG_PIN 4
#define ECHO_PIN 5
#define MAX_DISTANCE 400
#define IN1 8
#define IN2 9
#define ENA 10
#define IN3 11
#define IN4 12
#define ENB 13
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
unsigned long lastCheck = 0;
void motorForward(int speed) {
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(ENA, speed);
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
analogWrite(ENB, speed);
}
void motorStop() {
analogWrite(ENA, 0);
analogWrite(ENB, 0);
}
void setup() {
Serial.begin(9600);
pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT);
pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); pinMode(ENB, OUTPUT);
motorStop();
}
void loop() {
if (millis() - lastCheck >= 100) {
lastCheck = millis();
unsigned int dist = sonar.ping_cm();
Serial.print("dist: "); Serial.println(dist);
if (dist > 0 && dist <= 20) {
motorStop();
} else {
motorForward(150);
}
}
}
아들이 앞에 손을 뻗었다. 20cm 전에 멈췄다. 다시 손을 뺐다. 앞으로 갔다.
"됐다!"
그게 이 시리즈에서 처음으로 AI가 짠 코드가 실제 로봇에서 작동한 순간이었다. 첫 시도가 아니라 세 번째 시도였고, 중간에 컨텍스트 문제로 한 번 막혔고, 핀 번호 때문에 한 번 더 틀렸다. 그래도 됐다.
아직 안 된 것
두 가지가 남아있다.
하나는 한쪽 바퀴가 여전히 다른 쪽보다 약간 느리다. 두 모터가 같은 PWM 값(150)을 받는데도 결과가 다르다. 모터 개체 차이인지, 배선 저항 차이인지, 드라이버 채널 특성 차이인지 아직 모른다. 다음에 캘리브레이션 코드를 짜봐야 한다.
다른 하나는 초음파 센서 값이 튄다. 가끔 0cm가 나오거나 400cm가 나온다. NewPing에 필터링 옵션이 있는데 아직 안 써봤다. 노이즈가 심할 땐 로봇이 멀쩡히 가다가 갑자기 멈춘다. 아들이 보면서 "왜 갑자기 멈춰?" 한다. 나도 아직 완전히 잡지 못했다.
이걸 고치는 게 다음 단계다. AI가 코드를 짰다고 해서 끝이 아니다. 실제 하드웨어에서 테스트하면 새 문제가 계속 나온다.
댓글
댓글 쓰기