코드도 회로처럼 정리해야 한다
LED 하나를 깜빡이는 코드는 짧습니다. 버튼 하나를 읽는 코드도 어렵지 않습니다. 하지만 LED가 세 개, 버튼이 두 개, 센서가 하나씩 늘어나면 스케치는 금방 복잡해집니다. 회로에서 점퍼선을 아무렇게나 꽂으면 나중에 어느 선이 어디로 가는지 알기 어렵듯이, 코드도 정리하지 않으면 작은 수정 하나가 전체 오류로 이어질 수 있습니다.
이번 장의 목표는 새로운 부품을 많이 배우는 것이 아닙니다. 이미 다룬 LED, 버튼, 가변저항, 시리얼 모니터를 가지고 코드를 더 잘 다루는 방법을 익힙니다. 핵심은 값에 이름을 붙여 보관하고, 조건에 따라 다른 일을 시키고, 반복되는 코드를 줄이고, 자주 쓰는 동작을 함수로 나누는 것입니다.
먼저 간단한 실습 회로를 기준으로 삼겠습니다.
LED 3개: 9번, 10번, 11번 핀에 각각 저항을 거쳐 연결
버튼 1개: 2번 핀과 GND 사이에 연결,
INPUT_PULLUP사용가변저항 1개: 양쪽 다리는 5V와 GND, 가운데 다리는 A0에 연결
LED에는 반드시 전류 제한 저항을 넣습니다. 버튼은 INPUT_PULLUP을 쓰므로 누르지 않았을 때 HIGH, 눌렀을 때 LOW로 읽힙니다. 가변저항은 A0에 0부터 기준 전압까지 변하는 값을 전달합니다.
AI 생성 이미지
변수와 자료형: 값에 맞는 그릇 고르기
변수는 값을 담는 이름표입니다. 예를 들어 9라는 숫자만 코드 곳곳에 쓰면 이 숫자가 LED 핀인지, 밝기인지, 반복 횟수인지 알기 어렵습니다. ledPin이라고 이름을 붙이면 코드의 의도가 바로 보입니다.
아두이노에서 자주 쓰는 자료형은 다음처럼 이해하면 됩니다.
int: 정수. 핀 번호, 센서 원값, 카운트에 많이 사용float: 소수점이 있는 값. 전압 계산처럼 2.51V 같은 값을 표현할 때 사용bool: 참 또는 거짓. LED가 켜졌는지, 버튼이 눌렸는지 같은 상태에 사용char: 문자 한 글자. 시리얼 모니터에서a,b,1같은 명령을 받을 때 사용
가장 작은 예제로 시작해 보겠습니다.
const int ledPin = 9;
const int sensorPin = A0;
int sensorValue = 0;
float voltage = 0.0;
bool isBright = false;
char command = '0';const int는 값이 바뀌지 않는 정수라는 뜻입니다. 핀 번호처럼 실행 중 바뀔 일이 없는 값에 붙이면 실수로 값을 바꾸는 일을 줄일 수 있습니다. 반대로 sensorValue는 가변저항을 돌릴 때마다 달라지므로 const를 붙이지 않습니다.
자료형을 고를 때는 "이 값이 어떤 종류인가?"를 먼저 생각하면 됩니다. A0에서 읽은 원값은 보통 정수이므로 int가 어울립니다. 그 값을 전압으로 바꾸면 2.50처럼 소수점이 필요하므로 float가 어울립니다. 어두운지 밝은지처럼 둘 중 하나만 필요한 판단 결과는 bool이 좋습니다.
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT);
}
void loop() {
sensorValue = analogRead(sensorPin);
voltage = sensorValue * (5.0 / 1023.0);
isBright = sensorValue > 600;
Serial.print("sensor: ");
Serial.print(sensorValue);
Serial.print(" voltage: ");
Serial.print(voltage);
Serial.print(" bright: ");
Serial.println(isBright);
delay(300);
}여기서 5.0과 1023.0처럼 소수점이 붙은 숫자를 사용한 이유는 계산 결과를 소수로 얻기 위해서입니다. 만약 정수끼리만 계산하면 중간 결과가 기대와 다르게 잘릴 수 있습니다. 아두이노 우노 계열에서 analogRead() 값은 보통 0부터 1023까지 나오므로, 위 식은 센서 원값을 대략적인 전압으로 바꿔 보는 계산입니다.
점검 기준은 간단합니다. 가변저항을 돌렸을 때 시리얼 모니터의 sensor 값이 변해야 합니다. 값이 계속 0이면 가운데 다리가 A0에 연결되었는지 확인합니다. 값이 계속 1023 근처라면 A0가 5V 쪽에 고정되어 있거나 GND 연결이 빠졌을 수 있습니다.
조건문: 상황에 따라 다른 행동 시키기
조건문은 갈림길입니다. 센서값이 크면 LED를 켜고, 작으면 끕니다. 버튼이 눌렸으면 모드를 바꾸고, 눌리지 않았으면 현재 상태를 유지합니다.
가장 기본은 if와 else입니다.
if (sensorValue > 600) {
digitalWrite(ledPin, HIGH);
} else {
digitalWrite(ledPin, LOW);
}이 코드는 "센서값이 600보다 크면 LED를 켜고, 그렇지 않으면 끈다"는 뜻입니다. 조건식 안의 결과는 결국 참 또는 거짓입니다. 그래서 bool isBright = sensorValue > 600;처럼 판단 결과를 변수에 담을 수도 있습니다.
센서 범위를 여러 단계로 나누고 싶다면 else if를 사용합니다.
if (sensorValue < 300) {
Serial.println("dark");
} else if (sensorValue < 700) {
Serial.println("middle");
} else {
Serial.println("bright");
}순서가 중요합니다. 위 코드는 먼저 300보다 작은지 확인하고, 그다음 700보다 작은지 확인합니다. 만약 조건 순서를 잘못 잡으면 어떤 구간은 실행되지 않을 수 있습니다.
명령 종류가 정해져 있을 때는 switch가 읽기 쉽습니다. 예를 들어 시리얼 모니터에서 문자 하나를 받아 LED 동작을 바꾸는 코드를 생각해 보겠습니다.
if (Serial.available() > 0) {
command = Serial.read();
switch (command) {
case '1':
digitalWrite(ledPin, HIGH);
break;
case '0':
digitalWrite(ledPin, LOW);
break;
case 'b':
Serial.println("blink mode selected");
break;
default:
Serial.println("unknown command");
break;
}
}break는 해당 경우의 실행을 끝내고 switch 밖으로 나가게 합니다. break를 빠뜨리면 아래쪽 case까지 이어서 실행될 수 있으므로 주의해야 합니다.
반복문: 같은 일을 여러 번 시키기
아두이노의 loop() 자체도 반복입니다. 하지만 loop() 안에서도 같은 작업을 여러 번 해야 할 때가 있습니다. 예를 들어 LED 3개를 차례대로 켜려면 비슷한 코드가 반복됩니다.
먼저 정리되지 않은 예를 보겠습니다.
digitalWrite(9, HIGH);
delay(200);
digitalWrite(9, LOW);
digitalWrite(10, HIGH);
delay(200);
digitalWrite(10, LOW);
digitalWrite(11, HIGH);
delay(200);
digitalWrite(11, LOW);LED가 세 개일 때는 참을 만합니다. 하지만 여덟 개가 되면 코드가 길어지고, 지연 시간을 바꾸려면 여러 줄을 고쳐야 합니다. 이럴 때 for 반복문을 사용합니다.
for (int pin = 9; pin <= 11; pin++) {
digitalWrite(pin, HIGH);
delay(200);
digitalWrite(pin, LOW);
}for 문은 반복 횟수가 비교적 분명할 때 좋습니다. int pin = 9로 시작하고, pin <= 11인 동안 반복하며, 한 번 반복할 때마다 pin++으로 1씩 증가합니다.
while 문은 조건이 참인 동안 계속 반복합니다.
while (digitalRead(2) == LOW) {
digitalWrite(ledPin, HIGH);
}
digitalWrite(ledPin, LOW);이 코드는 버튼을 누르고 있는 동안 LED를 켭니다. 하지만 while을 사용할 때는 조심해야 합니다. 조건이 계속 참이면 프로그램이 그 안에 갇힙니다. 그러면 센서 읽기, 시리얼 출력, 다른 버튼 확인이 멈춘 것처럼 보일 수 있습니다. 입문 단계에서는 반복 횟수가 정해진 작업에는 for, 계속 상태를 확인해야 하는 작업에는 loop() 구조를 우선 사용하는 편이 안전합니다.
함수: 자주 쓰는 동작에 이름 붙이기
함수는 여러 줄의 코드를 하나의 동작으로 묶는 방법입니다. 회로에서 센서 모듈을 하나의 부품처럼 다루듯이, 코드에서도 "LED를 깜빡인다", "센서를 읽는다", "상태를 출력한다" 같은 동작을 함수로 만들 수 있습니다.
예를 들어 LED를 한 번 깜빡이는 코드를 함수로 분리해 보겠습니다.
void blinkLed(int pin, int timeMs) {
digitalWrite(pin, HIGH);
delay(timeMs);
digitalWrite(pin, LOW);
delay(timeMs);
}void는 이 함수가 돌려주는 값이 없다는 뜻입니다. int pin, int timeMs는 함수에 전달하는 값입니다. 이렇게 만들면 loop()에서는 다음처럼 짧게 사용할 수 있습니다.
void loop() {
blinkLed(9, 200);
blinkLed(10, 200);
blinkLed(11, 200);
}센서 읽기도 함수로 만들 수 있습니다.
int readSensor() {
int value = analogRead(A0);
return value;
}return은 함수가 계산한 값을 밖으로 돌려줍니다. 이제 loop()에서는 int value = readSensor();처럼 사용할 수 있습니다.
함수를 나누면 좋은 점은 세 가지입니다. 첫째, loop()가 짧아져서 전체 흐름이 보입니다. 둘째, 같은 코드를 반복해서 쓰지 않아도 됩니다. 셋째, 문제가 생겼을 때 함수 하나씩 따로 확인할 수 있습니다.
다만 함수 이름은 동작을 드러내야 합니다. doIt()보다 readSensor(), turnOffAllLeds(), printStatus()가 훨씬 낫습니다. 이름이 설명을 대신할 수 있으면 나중에 코드를 읽는 시간이 줄어듭니다.
배열: 여러 값을 한 줄로 관리하기
LED 핀 세 개를 각각 변수로 만들 수도 있습니다.
const int led1 = 9;
const int led2 = 10;
const int led3 = 11;하지만 LED가 많아지면 이 방식은 불편합니다. 배열은 같은 종류의 값을 순서대로 담는 상자입니다.
const int ledPins[] = {9, 10, 11};
const int ledCount = 3;배열의 첫 번째 값은 ledPins[0]입니다. 두 번째는 ledPins[1], 세 번째는 ledPins[2]입니다. 사람은 보통 1부터 세지만, 배열의 번호는 0부터 시작합니다. 이 차이를 헷갈리면 엉뚱한 값을 읽게 됩니다.
배열과 for 문을 함께 쓰면 여러 LED를 한꺼번에 설정할 수 있습니다.
void setup() {
for (int i = 0; i < ledCount; i++) {
pinMode(ledPins[i], OUTPUT);
}
}LED를 모두 끄는 함수도 간단해집니다.
void turnOffAllLeds() {
for (int i = 0; i < ledCount; i++) {
digitalWrite(ledPins[i], LOW);
}
}배열은 센서값을 여러 개 저장할 때도 유용합니다.
int sensorValues[3] = {0, 0, 0};지금은 A0 하나만 사용하지만, 나중에 조도 센서, 가변저항, 온도 센서처럼 여러 값을 함께 다룰 때 배열을 쓰면 코드 구조가 단순해집니다.
실습: 버튼으로 LED 모드 바꾸기
이제 변수, 조건문, 반복문, 함수, 배열을 한 번에 묶어 보겠습니다. 버튼을 누를 때마다 모드가 바뀌고, 모드에 따라 LED 동작이 달라지는 예제입니다.
const int buttonPin = 2;
const int ledPins[] = {9, 10, 11};
const int ledCount = 3;
int mode = 0;
bool lastButtonState = HIGH;
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
for (int i = 0; i < ledCount; i++) {
pinMode(ledPins[i], OUTPUT);
}
Serial.begin(9600);
}
void loop() {
bool buttonState = digitalRead(buttonPin);
if (lastButtonState == HIGH && buttonState == LOW) {
delay(30); // 간단한 디바운스
if (digitalRead(buttonPin) == LOW) {
mode++;
if (mode > 2) {
mode = 0;
}
Serial.print("mode: ");
Serial.println(mode);
}
}
lastButtonState = buttonState;
runMode(mode);
}
void runMode(int currentMode) {
switch (currentMode) {
case 0:
turnOffAllLeds();
break;
case 1:
turnOnAllLeds();
break;
case 2:
chaseLeds();
break;
}
}
void turnOffAllLeds() {
for (int i = 0; i < ledCount; i++) {
digitalWrite(ledPins[i], LOW);
}
}
void turnOnAllLeds() {
for (int i = 0; i < ledCount; i++) {
digitalWrite(ledPins[i], HIGH);
}
}
void chaseLeds() {
for (int i = 0; i < ledCount; i++) {
turnOffAllLeds();
digitalWrite(ledPins[i], HIGH);
delay(150);
}
}이 예제에서 mode는 현재 동작 상태를 나타내는 정수입니다. 0은 모두 끄기, 1은 모두 켜기, 2는 차례대로 켜기입니다. 버튼은 INPUT_PULLUP이므로 눌리는 순간 HIGH에서 LOW로 바뀝니다. 그래서 lastButtonState == HIGH && buttonState == LOW 조건으로 "방금 눌린 순간"만 잡습니다.
버튼은 손으로 한 번 눌러도 내부 금속 접점이 아주 짧은 시간 동안 여러 번 붙었다 떨어질 수 있습니다. 이 때문에 코드에서는 버튼을 한 번 눌렀는데 여러 번 누른 것처럼 보일 때가 있습니다. 이런 현상을 줄이는 처리를 디바운스라고 합니다. 위 예제의 delay(30)은 30밀리초 정도 기다렸다가 버튼 상태를 다시 확인하는 아주 간단한 디바운스입니다.
runMode() 함수는 현재 모드에 맞는 함수를 호출합니다. LED 핀은 배열에 들어 있으므로 LED 개수가 늘어나도 ledPins와 ledCount만 고치면 대부분의 코드는 그대로 사용할 수 있습니다.
단, 이 코드에는 아직 한계가 있습니다. chaseLeds() 안에서 delay(150)을 사용하므로 그동안 버튼 반응이 늦어질 수 있습니다. 디바운스도 delay()로 단순하게 처리했기 때문에 아주 빠른 입력을 정교하게 다루기에는 부족합니다. 지금은 문법을 익히기 위한 예제이므로 단순하게 작성했습니다. 다음 단계에서는 delay()에 오래 멈추지 않고 여러 일을 번갈아 처리하는 구조를 배웁니다.
오류가 났을 때 보는 기준
문법을 배우는 단계에서는 회로보다 코드에서 막히는 일이 많습니다. 오류 메시지를 모두 외울 필요는 없지만, 자주 생기는 실수는 구분할 수 있어야 합니다.
세미콜론
;이 빠지면 해당 줄이나 다음 줄에서 컴파일 오류가 납니다.중괄호
{}짝이 맞지 않으면 함수나 조건문의 범위가 꼬입니다.=와==를 혼동하면 조건문이 의도와 다르게 동작합니다. 비교할 때는==를 씁니다.배열은 0부터 시작합니다. LED 3개의 인덱스는
0,1,2입니다.INPUT_PULLUP버튼은 누르면LOW입니다. LED나 모드 변경이 반대로 동작한다면 이 기준을 먼저 확인합니다.함수 이름을 호출할 때 철자가 하나라도 다르면 다른 이름으로 인식됩니다.
스케치가 커질수록 중요한 태도는 한 번에 많이 바꾸지 않는 것입니다. LED 핀 배열을 추가했다면 먼저 LED가 켜지는지만 확인합니다. 그다음 버튼 조건을 붙이고, 그다음 모드 함수를 붙입니다. 회로를 한 선씩 점검하듯이 코드도 한 단계씩 키우면 오류를 찾기 쉬워집니다.
이번 장에서 배운 문법은 단순한 규칙 목록이 아닙니다. 프로젝트를 오래 유지하기 위한 정리 도구입니다. 변수는 값에 이름을 붙이고, 조건문은 판단을 표현하고, 반복문은 중복을 줄이고, 함수는 동작을 나누고, 배열은 여러 부품을 한꺼번에 관리합니다. 이 다섯 가지를 사용할 수 있으면 예제 코드를 그대로 따라 하는 단계에서 벗어나, 자신의 회로에 맞게 스케치를 바꾸기 시작할 수 있습니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.