[C언어 50강] 23강. 문자열 입출력: fgets/puts, 개행 처리, 안전한 입력
문자열 입출력은 C에서 가장 자주 쓰이지만, 동시에 가장 자주 사고가 나는 영역입니다. 특히 scanf("%s")에 익숙해진 상태에서 프로젝트를 시작하면 공백 입력 누락, 버퍼 초과, 개행 잔여 문제를 연달아 만나게 됩니다. 이번 강의의 목표는 함수 이름을 외우는 것이 아니라, 왜 fgets 중심으로 입력을 설계해야 하는지를 확실히 이해하는 것입니다.
핵심 개념
- 문자열 입력의 핵심은 “무엇을 읽을지”보다 **얼마나 읽을지(경계)**를 먼저 정하는 것이다.
fgets는 버퍼 크기를 알고 동작하므로, 경계 초과를 막는 기본 도구다.fgets는 조건에 따라 개행 문자(\n)를 버퍼에 포함하므로, 후처리(개행 제거) 규칙이 필요하다.puts는 문자열 출력 뒤 자동으로 줄바꿈을 붙인다.printf와 출력 습관이 다르다.- 안전한 문자열 입출력은 함수 1개 선택 문제가 아니라, 입력 → 정제 → 검증 → 사용 흐름 전체의 설계 문제다.
개념 먼저 이해하기
많은 초보자가 문자열 입력 문제를 “어떤 함수가 더 편하냐” 관점으로 시작합니다. 하지만 실무 관점에서는 질문이 달라야 합니다. “내 버퍼가 몇 바이트인지 함수가 알고 있나?”, “입력이 너무 길 때 내 프로그램은 예측 가능한 방식으로 동작하나?”, “이 입력이 다음 로직으로 넘어가기 전에 정제되었나?”가 먼저입니다. C는 런타임이 자동으로 안전망을 깔아주지 않기 때문에, 프로그래머가 직접 경계와 상태를 설계해야 합니다.
fgets가 중요한 이유는 단순합니다. 함수 시그니처에 버퍼 크기가 들어가 있기 때문입니다. 즉 fgets(buf, sizeof(buf), stdin)처럼 호출하면, 함수는 최대 sizeof(buf)-1개 문자만 읽고 마지막에 \0을 붙이는 규칙으로 동작합니다. 여기서 중요한 건 “항상 전체 한 줄을 읽는다”가 아니라, 버퍼 크기 범위 안에서만 읽는다는 점입니다. 입력이 짧으면 줄바꿈(\n)까지 들어올 수 있고, 입력이 너무 길면 줄 일부만 읽고 나머지는 입력 스트림에 남습니다. 이 성질을 이해하지 못하면 “왜 다음 입력이 건너뛰어졌지?” 같은 현상을 자주 겪게 됩니다.
개행 처리는 단순한 미관 문제가 아닙니다. 예를 들어 사용자 이름을 fgets로 받았는데 개행을 제거하지 않으면, 파일 저장 시 의도치 않은 줄바꿈이 들어가거나 문자열 비교(strcmp)에서 실패합니다. 눈으로 보기에는 같은 이름인데, 실제 바이트는 "Toby\n\0"와 "Toby\0"로 다르기 때문입니다. 그래서 문자열 입력에서는 “받자마자 정제한다”가 기본 원칙입니다. 가장 흔한 패턴은 strcspn으로 \n 위치를 찾아 \0으로 바꾸는 방식입니다.
puts도 개념적으로 분리해 이해해야 합니다. puts(s)는 printf("%s\n", s)에 가까운 동작을 합니다. 즉 문자열 뒤에 자동 줄바꿈이 붙습니다. 간단한 로그 출력에서는 매우 편리하지만, 프롬프트 출력처럼 줄바꿈 없이 이어서 입력을 받아야 할 때는 printf가 더 적합합니다. 함수 선택은 취향이 아니라, 인터페이스 요구사항과 사용자 경험의 문제입니다.
결국 문자열 입출력의 안정성은 한 줄로 정리할 수 있습니다. 입력 함수가 버퍼 경계를 존중하게 만들고, 읽은 데이터에서 개행/잔여 입력을 처리하고, 그 뒤에야 비즈니스 로직으로 넘겨라. 이 흐름이 익숙해지면 C에서 “입력 때문에 망가지는 프로그램” 비율을 크게 줄일 수 있습니다.
기본 사용
예제 1) fgets + 개행 제거의 표준 패턴
#include <stdio.h>
#include <string.h>
int main(void) {
char name[32];
printf("이름을 입력하세요: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
fprintf(stderr, "입력을 읽지 못했습니다.\n");
return 1;
}
// 줄바꿈이 있으면 제거
name[strcspn(name, "\n")] = '\0';
printf("안녕하세요, %s님!\n", name);
return 0;
}
설명:
fgets는 최대 크기를 알기 때문에 버퍼 오버런 위험을 크게 줄입니다.strcspn(name, "\n")은 개행 위치(또는 문자열 끝)를 찾아줍니다.- 개행 제거 후 비교/저장/출력 로직이 예측 가능해집니다.
예제 2) 입력이 너무 길 때 잔여 버퍼 비우기
#include <stdio.h>
#include <string.h>
static void clear_stdin_until_newline(void) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
// 남은 문자를 버림
}
}
int main(void) {
char line[8]; // 매우 작은 버퍼(실습용)
printf("최대 7글자 입력: ");
if (fgets(line, sizeof(line), stdin) == NULL) {
return 1;
}
if (strchr(line, '\n') == NULL) {
// 개행이 없다면 입력이 잘린 것. 스트림에 남은 나머지를 비운다.
clear_stdin_until_newline();
printf("입력이 너무 길어 잘렸습니다.\n");
} else {
line[strcspn(line, "\n")] = '\0';
}
printf("처리된 입력: [%s]\n", line);
return 0;
}
설명:
fgets가 안전하더라도, 긴 입력의 “나머지”가 남는 문제를 처리해야 다음 입력이 정상 동작합니다.- 실무에서는 입력 유틸 함수로 이 패턴을 캡슐화해 재사용하는 편이 좋습니다.
예제 3) puts와 printf의 역할 구분
#include <stdio.h>
int main(void) {
char msg[] = "DevLab C 강의";
puts(msg); // 자동 줄바꿈 O
printf("%s", msg); // 자동 줄바꿈 X
printf(" <- 같은 줄에 이어서 출력\n");
return 0;
}
설명:
puts는 간단한 한 줄 출력용으로 빠르고 명확합니다.- 프롬프트처럼 출력 제어가 필요하면
printf를 사용해야 합니다.
자주 하는 실수
실수 1) scanf("%s", buf)를 기본 입력으로 고정하는 경우
- 문제: 공백을 기준으로 입력이 끊기고, 길이 제한을 빼먹으면 버퍼 초과 위험이 큽니다.
- 개선: 문자열 라인 입력은
fgets를 기본으로 하고, 필요한 경우 파싱을 분리합니다.
실수 2) fgets 후 개행을 제거하지 않는 경우
- 문제: 비교 실패, 파일 포맷 깨짐, UI 출력 이상(의도치 않은 줄바꿈).
- 개선: 입력 직후 즉시
line[strcspn(line, "\n")] = '\0';패턴을 적용합니다.
실수 3) 긴 입력이 들어왔을 때 잔여 데이터를 방치하는 경우
- 문제: 다음 입력이 건너뛰어지거나 즉시 종료된 것처럼 보입니다.
- 개선: 개행 포함 여부를 검사하고, 잘린 경우 남은 입력을 비우는 루틴을 둡니다.
실수 4) puts를 프롬프트 출력에 사용해 UX가 어색해지는 경우
- 문제: 입력 커서가 항상 다음 줄로 내려가서 인터랙션이 불편해집니다.
- 개선: 프롬프트는
printf("입력: ");처럼 줄바꿈 없이 출력합니다.
실무 패턴
- 입력 래퍼 함수 만들기:
int read_line(char *buf, size_t n)같은 유틸을 만들어 개행 제거/잔여 입력 처리/실패 코드를 표준화합니다. - 입력과 파싱 분리: 먼저 줄 단위로 안전하게 받고(
fgets), 이후sscanf/strtol로 의미 파싱을 수행합니다. - 오류 상태를 명시적으로 전달: 읽기 실패, 잘림 발생, 빈 입력 등을 enum 또는 반환값으로 명확히 구분합니다.
- 테스트를 경계 중심으로 설계: 빈 줄, 최대 길이 정확히 맞는 입력, 최대 길이+1 입력, 공백 포함 입력을 자동 테스트 케이스로 둡니다.
- 출력 정책 통일: 로그는
puts, 포맷 출력은printf, 사용자 프롬프트는printf(줄바꿈 없음)로 팀 컨벤션을 정하면 가독성과 유지보수가 좋아집니다.
오늘의 결론
문자열 입출력의 핵심은 함수 암기가 아니라 입력 경계의 통제와 상태 정제입니다. fgets로 안전하게 받고, 개행과 잔여 입력을 정리한 뒤, 검증된 문자열만 다음 로직에 넘기세요. 이 기본기만 지켜도 C 프로그램의 안정성이 눈에 띄게 올라갑니다.
연습문제
read_line함수를 직접 구현해 보세요.- 인자:
char *buf, size_t n - 기능:
fgets입력, 개행 제거, 잘림 여부 감지, 반환 코드(성공/실패/잘림)
- 인자:
- 사용자로부터 제목(
title)과 작성자(author)를 각각 입력받아puts로 출력하는 프로그램을 작성해 보세요.- 단, 프롬프트는 줄바꿈 없이 출력하고 입력은
fgets기반으로 처리하세요.
- 단, 프롬프트는 줄바꿈 없이 출력하고 입력은
- 버퍼 크기가 12바이트일 때,
- 입력이 5글자, 11글자, 20글자인 경우 각각 버퍼 내부 상태(
\n,\0포함)가 어떻게 달라지는지 설명해 보세요.
- 입력이 5글자, 11글자, 20글자인 경우 각각 버퍼 내부 상태(
이전 강의 정답
지난 22강 연습문제 핵심은 “문자열은 \0으로 끝나는 char 배열”이라는 사실을 코드/설명으로 증명하는 것이었습니다.
정답 1) char id[10] 안전 입력
#include <stdio.h>
#include <string.h>
int main(void) {
char id[10]; // 최대 9글자 + '\0'
printf("ID 입력(최대 9글자): ");
if (fgets(id, sizeof(id), stdin) == NULL) {
return 1;
}
id[strcspn(id, "\n")] = '\0';
printf("저장된 ID: %s\n", id);
return 0;
}
핵심 포인트:
- 버퍼 크기 10이면 문자열 최대 길이는 9입니다.
fgets+ 개행 제거로 기본 안전성을 확보합니다.
정답 2) char *a = "devlab"; vs char b[] = "devlab";
char *a = "devlab";- 문자열 리터럴을 가리키는 포인터입니다.
- 수정 시도(
a[0] = 'D')는 정의되지 않은 동작입니다. - 주로 읽기 전용 문자열 참조에 적합합니다.
char b[] = "devlab";- 배열에 문자열이 복사되어 저장됩니다.
b[0] = 'D';처럼 수정 가능합니다.- 크기는
{'d','e','v','l','a','b','\0'}를 담을 만큼 할당됩니다.
정답 3) 두 문자열 사전순 비교
#include <stdio.h>
int cmp_lex(const char *a, const char *b) {
int i = 0;
while (a[i] != '\0' && b[i] != '\0') {
if (a[i] < b[i]) return -1;
if (a[i] > b[i]) return 1;
i++;
}
if (a[i] == '\0' && b[i] == '\0') return 0;
return (a[i] == '\0') ? -1 : 1;
}
int main(void) {
char s1[16], s2[16];
printf("문자열1: ");
if (!fgets(s1, sizeof(s1), stdin)) return 1;
printf("문자열2: ");
if (!fgets(s2, sizeof(s2), stdin)) return 1;
for (int i = 0; s1[i] != '\0'; i++) {
if (s1[i] == '\n') { s1[i] = '\0'; break; }
}
for (int i = 0; s2[i] != '\0'; i++) {
if (s2[i] == '\n') { s2[i] = '\0'; break; }
}
int r = cmp_lex(s1, s2);
if (r < 0) printf("앞선 문자열: %s\n", s1);
else if (r > 0) printf("앞선 문자열: %s\n", s2);
else printf("두 문자열은 같습니다.\n");
return 0;
}
핵심 포인트:
\0종료를 기준으로 한 글자씩 비교하면strcmp없이도 사전순 비교 구현이 가능합니다.- 길이가 짧은 문자열이 공통 접두사 이후 먼저 끝나면 사전순으로 앞섭니다.
실습 환경/재현 정보
- OS: macOS (Apple Silicon), Ubuntu 22.04
- 컴파일러: clang 17+ 또는 gcc 13+
- 컴파일 옵션:
-std=c11 -Wall -Wextra -O2 - 권장 실행 절차:
- 예제별로 파일 분리 후 컴파일
- 짧은 입력/긴 입력/공백 포함 입력으로 동작 비교
- 긴 입력 후 다음 입력이 정상인지(잔여 버퍼 처리 여부) 확인
- 품질 체크:
- 코드 블록 3개 이상
개념 먼저 이해하기500자 이상- 문서 전체 4500자 이상