\r\n(CRLF)와 \n\r의 차이
줄바꿈문자로 보통 윈도우는 \r\n을, POSIX류는 \n을 사용한다.
나는 운영체제 상관 없이 \n을 사용하지만.
\r\n과 \n은 무슨 뜻이고 차이는 뭘까? 그리고 왜 \n\r은 안 쓸까?
목차
각각의 뜻
\r은 캐리지 리턴(CR, Carrage Return)이고, \n이 진짜 줄바꿈문자(LF, Line Feed)이다.
재미도 감동도 없는 역사 얘기는 빼고, 유의미한 정보만을 전달하자면 대충 이 정도 되겠다.
\r(Carrage Return): 커서를 그 줄의 맨 앞으로 옮긴다\n(Line Feed): 커서를 아래로 한 칸 내린다
대충 이런 의미지만 보통은 그냥 줄바꿈문자로 쓰인다.
그런데 정말 그 뜻 그대로 쓰이는 곳이 있다. 바로 CLI 환경이다.
예시
예시로 뭐가 어떻게 다른지 확인해보자.
줄바꿈 없을 때
#include <stdio.h>
int main() {
return printf("Hello world!") != 12;
}
줄바꿈문자를 출력하지 않았을 때의 출력 결과는 아래 스크린샷과 같다. (sh 기준)

프로그램 실행이 끝나고 $ 가 출력된 것이다.1
CR
#include <stdio.h>
int main() {
return printf("Hello world!\r") != 13;
}
이 코드를 실행해보면 아래 스크린샷처럼, 커서가 그 줄의 맨 앞으로 이동된 것을 확인할 수 있다.

마찬가지로 프로그램 실행이 끝나고 $ 가 출력된 것인데,
커서가 맨 앞으로 이동돼 있었기 때문에 He부분이 가려져서 llo world!가 보이는 것이다.
LF
#include <stdio.h>
int main() {
return printf("Hello world!\n") != 13;
}

뭔가... 이상하다?
커서가 아래로 한 칸 내려가기만 한 게 아니라 \r을 붙인 것 같은 결과가 나오고 있다.
이는 터미널의 모드 때문이다. 기본적으로는 canonical mode로, 자동으로 이것저것 처리된다.
raw mode로 들어가면 자동으로 이것저것 처리되던 것들이 꺼진다.
일단 출력 후처리만 끄고 해 보자.
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
static void enable_raw_mode();
int main() {
enable_raw_mode();
return printf("Hello world!\n") != 13;
}
static struct termios original_termios;
static void disable_raw_mode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios) == -1)
exit(EXIT_FAILURE);
}
static void enable_raw_mode() {
if (tcgetattr(STDIN_FILENO, &original_termios) == -1)
exit(EXIT_FAILURE);
atexit(disable_raw_mode);
struct termios raw = original_termios;
raw.c_oflag &= ~OPOST;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
exit(EXIT_FAILURE);
}

기대대로 나오는 것을 확인할 수 있다!
CRLF, LFCR
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
static void enable_raw_mode();
int main() {
enable_raw_mode();
if (printf("Hello world!\r\n") != 14)
exit(EXIT_FAILURE);
if (printf("Hello world!\n\r") != 14)
exit(EXIT_FAILURE);
}
static struct termios original_termios;
static void disable_raw_mode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios) == -1)
exit(EXIT_FAILURE);
}
static void enable_raw_mode() {
if (tcgetattr(STDIN_FILENO, &original_termios) == -1)
exit(EXIT_FAILURE);
atexit(disable_raw_mode);
struct termios raw = original_termios;
raw.c_oflag &= ~OPOST;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
exit(EXIT_FAILURE);
}

CRLF나 LFCR이나 똑같아보인다. 그럼 왜 CRLF는 쓰고 LFCR은 안 쓰는 걸까?
그 이유는 바로 stdout의 버퍼링에 있다. 자세한 내용은 setvbuf 참조.
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
static void enable_raw_mode();
int main() {
if (setvbuf(stdout, NULL, _IOLBF, 4096))
exit(EXIT_FAILURE);
enable_raw_mode();
if (printf("Hello world!\r\n") != 14)
exit(EXIT_FAILURE);
sleep(10);
if (printf("Hello world!\n\r") != 14)
exit(EXIT_FAILURE);
sleep(10);
}
static struct termios original_termios;
static void disable_raw_mode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios) == -1)
exit(EXIT_FAILURE);
}
static void enable_raw_mode() {
if (tcgetattr(STDIN_FILENO, &original_termios) == -1)
exit(EXIT_FAILURE);
atexit(disable_raw_mode);
struct termios raw = original_termios;
raw.c_oflag &= ~OPOST;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
exit(EXIT_FAILURE);
}

일단 결과는 같다.

CRLF 이후에 sleep중인 모습. 예상과 같다.

LFCR 이후에 sleep중인 모습.
stdout이 줄 단위로 버퍼링되기 때문에 \r이 아직 출력되지 않은 상태.
결론
stdout의 라인 버퍼링 때문에 \n\r은 \r이 늦게 출력될 수 있으므로 \r\n을 쓴다.
Footnotes
-
쉘을
PS1='$ ' sh로 실행했음 ↩