C에서 사용자 입력을 읽는 함정: scanf와 Stdin에 관한 이야기

발행: (2025년 12월 12일 오전 06:40 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

C에서 사용자 입력을 읽을 때의 함정: scanfstdin에 관한 이야기

최근에 stdin에서 입력을 읽고, 개행 문자를 무시하며, 버퍼를 초과하는 부분은 버리고, 이를 반복하는 C 코드를 작성해야 했습니다.

최초 시도

#include <stdio.h>

void take_input(void)
{
    char buf[20];
    printf("> ");
    scanf("%19[^\n]", buf);
    printf("input=`%s`\n", buf);
}

int main(void)
{
    for (int i = 0; i < 5; ++i) {
        take_input();
    }
    return 0;
}
$ ./main
> hello world↵
input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
> input=`hello world`
$ █

문자열을 한 번만 소비하고 같은 값을 다섯 번 출력했습니다. 왜일까요?

스택 동작

char buf[20]은 지역 변수이지만, take_input()이 호출될 때마다 정확히 같은 스택 메모리 주소를 사용합니다.

/* ... */
printf("address=%p, input=`%s`\n", (void*)buf, buf);
/* ... */
$ ./main
> hello world
address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
> address=0x7ffcb96c49d0, input=`hello world`
...

버퍼가 초기화되지 않았고 이후 scanf 호출이 이를 덮어쓰지 못했기 때문에 이전 내용(“유령 데이터”)이 그대로 남아 있었습니다. 버퍼를 0으로 초기화하면 이 특정 문제는 해결됩니다.

char buf[20] = {0};

이제 버퍼는 매번 초기화되지만 scanf는 여전히 이상하게 동작합니다:

$ ./main
> hello world
address=0x7ffea5c2d840, input=`hello world`
> address=0x7ffea5c2d840, input=``
> address=0x7ffea5c2d840, input=``
...

실제 문제: %[^\n]은 개행을 소비하지 않는다

스캔셋 %19[^\n]은 개행이 아닌 최대 19자를 읽지만, 개행 문자는 입력 스트림에 남겨 둡니다.

GDB로 stdin을 살펴보면 개행이 아직 남아 있음을 확인할 수 있습니다:

$ gcc -g3 -O0 main.c -o main
$ gdb main
(gdb) break 8
Breakpoint 1 at 0x11d3: file main.c, line 8.
(gdb) run
...
> hello world↵
...
(gdb) p *stdin
$1 = {
    _flags = -72539512,
    _IO_read_ptr = 0x55555555972b "\n",
    _IO_read_end = 0x55555555972c "",
    _IO_read_base = 0x555555559720 "hello world\n",
    
}

stdin->_IO_read_ptr가 아직 개행을 가리키고 있습니다. 루프가 다시 실행될 때 scanf("%19[^\n]", buf)는 즉시 개행을 만나 0자를 매치하고 중단합니다. 버퍼는 비어 있거나(또는 0으로) 그대로이며, 루프는 같은 출력을 반복합니다.

작동 가능한 해결책

1. 추가 scanf 호출 사용

라인의 나머지와 개행을 강제로 소비합니다:

void take_input(void)
{
    char buf[20];
    printf("> ");
    scanf("%19[^\n]", buf);
    /* 19자를 초과하는 경우 라인의 나머지를 버림 */
    scanf("%*[^\n]");
    /* 개행을 버림 */
    scanf("%*c");
    printf("input=`%s`\n", buf);
}

코드는 다소 지저분해 보이지만, 공백, 짧은 입력, 길게 잘린 입력 모두에 대해 신뢰할 수 있게 동작합니다:

$ ./main
> hey
input=`hey`
> hello world
input=`hello world`
> a b c d e f g h i j k l m o p q r s t u v w x y z
input=`a b c d e f g h i j k l m o p q r s t u v w x y z`
>      /* 5개의 공백 */
input=`     `
> abcdefghijklmnpoqrstuvwxyz
input=`abcdefghijklmnpoqrs`

2. fgets – 표준적인 방법

보다 예측 가능하고 명시적인 방법은 fgets로 한 줄을 읽고 개행을 제거하는 것입니다:

#include <stdio.h>
#include <string.h>

void take_input_2(void)
{
    char buf[20];
    printf("> ");

    if (fgets(buf, sizeof(buf), stdin)) {
        char *newline_ptr = strchr(buf, '\n');
        if (newline_ptr) {
            /* \n을 \0으로 바꿔서 트림 */
            *newline_ptr = '\0';
        } else {
            /* 개행이 없으면 – 입력이 잘린 경우;
               라인의 나머지를 소비 */
            int c;
            while ((c = getchar()) != '\n' && c != EOF);
        }
    }
    printf("input=`%s`\n", buf);
}

3. getline – 힙을 이용한 방법

getline(POSIX, ISO C는 아님)은 전체 라인을 읽으며 힙에 버퍼를 할당(또는 재할당)합니다. 메모리는 직접 해제해야 합니다:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void take_input_3(void)
{
    char *line = NULL;      /* 포인터 초기화 */
    size_t cap = 0;          /* getline이 업데이트할 용량 */
    ssize_t n;

    printf("> ");

    n = getline(&line, &cap, stdin);
    if (n > 0) {
        /* 뒤쪽 개행 제거 */
        if (line[n-1] == '\n') {
            line[n-1] = '\0';
        }
        printf("input=`%s`\n", line);
    }

    free(line);  /* 힙 메모리는 항상 해제 */
}

정리

이 연습은 C에서 입력 처리가 얼마나 까다로울 수 있는지를 일깨워 주었습니다. scanf는 편리해 보이지만 공백과 개행에 관한 엣지 케이스에 부딪히면 “편리함”이 오히려 부담이 됩니다.

  • 잘라내기(truncation) 처리를 포함한 예측 가능하고 이식 가능한 라인 기반 입력을 원한다면 fgets가 거의 항상 더 좋은 선택입니다.
  • POSIX와 힙 할당이 가능한 환경이라면 getline이 유연성을 제공합니다.
  • 입력 형식이 정확히 알려진 경우에만 scanf를 사용하세요.
Back to Blog

관련 글

더 보기 »

그냥 코딩을 계속해

소개 우리는 모두 쉬운 변경이라고 생각하고 버그를 고치러 갔지만, 결국 하루의 대부분을 그 문제를 해결하려고 보내는 날이 있습니다. 만약...