7. 드나듦(I/O) - 입력과 출력

  • 프로그래밍 언어를 새로 배울 때 제일 처음 등장하는 코드. 바로 Hello world!.
  • '드나듦'을 사용하면 화면에 출력하거나 키보드에서 입력을 받을 수 있다. ⇒ Hello world를 짜려면 드나듦 사용법을 알아야 한다!
 
TMI: 드나듦을 공부하는 법 - 알고 있는 것을 버려라!
⚠️
'평범한 한글'의 드나듦은 하스켈(Haskell) I/O 모나드(Monads)의 행동에 기초한다. 따라서 명령형 언어에 익숙하다면 여기서 설명하는 개념이 낯설 수 밖에 없다. 새롭게 개념을 적립할 수 있도록 이미 알고 있는 것을 잠시 내려놓기를 추천한다. 그렇다고 겁 먹을 필요는 없다. 천천히 하나씩 따라오면 어느새 여러분의 머리에 I/O 모나드의 개념이 세워져 있을 것이다. (적어도 모나드를 모르더라도 드나듦을 그냥 쓸 수는 있을 것이다.)
 
 

부작용 - 내부 세계 vs 외부 세계

  • 부작용(side effect): 함수를 호출했을 때 무언가의 상태가 변하는 것.
  • C를 비롯한 명령형(imperative) 언어는 상태를 변화시켜서 문제를 푼다. ⇒ 문제를 푸는 과정 속에 언제나 의도하지 않은 부작용이 생길 수 있다.
상태 변화로 문제를 푸는 방식의 맹점...
  • 변수나 객체의 상태는 끊임없이 변한다.
  • 프로그램이 커지면 흐름에 따라 상태가 어떻게 변하는지 추적하기가 어려워 진다.
  • 크던 작던 상태 변화에 의존하는 프로그램은 부작용 때문에 버그가 생길 위험이 있다.
  • 그럼에도 불구하고, 현실의 문제를 다루려면 상태 변화를 피할 수 없다. ⇒ 최대한 안전하게 상태 변화를 통제하는 것이 중요하다.
  • 이런 상태 변화의 범위를 통제하기 위해 모듈, 클래스, 범위(scope) 등의 기법을 사용한다.

 
  • '평범한 한글'에는 개념적으로 두 가지 세계가 존재한다.
    • 내부 세계: 순수하게 함수로 이루어진다. (pure function)
      • 상태 변화가 없다. ⇒ 부작용이 없다.
    • 외부 세계: 드나듦과 계획으로 이루어진다. (IO, action...)
      • 상태 변화가 있다. ⇒ 부작용이 있다.
  • 평범한 한글은 부작용이 있는 외부 세계를 내부 세계와 철저히 격리하여 상태 변화를 통제한다.
  • 입력과 출력은 상태를 변화시켜야 하는 작업이다. ⇒ 입력과 출력에는 상태 변화가 일어난다. (즉, 부작용이 있다.) ⇒ 드나듦은 상태 변화를 다루는 '평범한 한글'의 방법이다.
 

내부 세계 탐험

  • 다음 코드로부터 이야기를 시작하자.
ㄹ ㅂ ㄷ ㅎㄷ ㄹ ㅂ ㄷ ㅎㄷ ㄹ ㅂ ㄷ ㅎㄷ ㄹ ㅂ ㄷ ㅎㄷ ㄹ ㅂ ㄷ ㅎㄷ
▶️
8 8 8 8 8
  • 함수는 똑같은 인수를 넣어 호출하면 언제나 똑같은 값을 내놓는다. ⇒ 평범한 한글의 모든 함수는 똑같은 인수를 받으면 똑같은 값을 내놓는다. ⇒ 함수는 내부 세계에서 상태를 변화시킬 수 없다. ⇒ 내부 세게에서는 부작용이 없다!
  • 3과 5를 더한다고 해서 '3'이나 '5'가 변하는 것이 아니고 계산 결과로써 8을 내놓는 것이다.
  • 다른 명령형 언어에서도 변수를 쓰지 않으면 부작용 없는 함수를 만들 수 있다.
 

다른 언어 탐험 - 부작용 체험하기

  • Python에서 다음과 같은 코드를 만들었다.
name = input()
  • 화면에 커서가 깜빡이고 키보드로 무언가를 입력해주기를 기다린다.
  • 사용자가 Andrea 라고 입력한 다음 Enter 키를 누르면 name 변수에 'Andrea'가 대입된다.
  • 위의 코드는 name 변수의 상태를 바꾸었다. ⇒ 부작용이 일어났다!
  • 입력과 출력은 언제나 상태 변화가 따라오므로 부작용이 생길 수 밖에 없다.
 
  • 다음과 같은 Python 함수를 만들었다.
def inc(x): inc.counter = inc.counter + 1 return inc.counter inc.counter = 0
  • 이제 이 함수를 실행하면 다음과 같은 결과를 볼 수 있다.
inc(2) inc(2) inc(2) inc(2) inc(2)
▶️
2 4 6 8 10
  • inc() 함수를 실행할 때마다 함수가 내놓는 값이 달라진다. ⇒ 2를 인수로 넘겼음에도 다른 결과를 내놓는다.
  • 더구나 inc() 함수는 inc.counter 변수의 상태까지 바꾼다. 부작용이 생겼다.
  • 물론 다른 언어의 코드가 모두 부작용을 일으키지는 않는다.
3 + 5 3 + 5 3 + 5 3 + 5 3 + 5
▶️
8 8 8 8 8
  • 위와 같이 변수를 사용하지 않으면 상태 변화도 없고 부작용도 생기지 않는다.
  • 다른 언어에서는 값을 다루는 방법이 비슷한데도 부작용이 생길 여지가 있다.
 

외부 세계 탐험 - 키보드 입력 받기

  • '평범한 한글'은 부작용이 일어나는 모든 상황을 드나듦(I/O) 객체가 맡아서 처리한다.
  • : 표준 입력. 키보드에서 Enter 키를 입력할 때까지 모든 글자를 입력받아 문자열로 만들고, 이어서 이 문자열을 감싸는 드나듦을 만들어 내놓는다. ('읽다'의 로 기억하자.)
  • 다음과 같이 함수를 호출할 수 있다.
ㄹ ㅎㄱ
 
ㄹ ㅎㄱ 함수를 호출하여 키보드 입력을 기다리는 '평범한 한글' 실행 환경. ㄹ ㅎㄱ 함수를 호출하여 키보드 입력을 기다리는 '평범한 한글' 실행 환경.
ㄹ ㅎㄱ 함수를 호출하여 키보드 입력을 기다리는 '평범한 한글' 실행 환경.
  • 키보드에서 평범한 한글이라고 입력하고 Enter 를 누르면 실행 결과가 표시된다.
▶️
IO('평범한 한글')
  • 평범한 한글은 결과를 표시할 때 ' '로 둘러싸서 문자열을 출력한다. → 위의 결과는 '평범한 한글' 이라는 여섯 글자로 이루어진 문자열이다. 그런데...
  • 그냥 '평범한 한글'이 아니라 IO('평범한 한글')처럼 표시되었다.
 
  • 한 번 더 ㄹ ㅎㄱ 함수를 호출한 다음 3을 입력하고 Enter 키를 눌러보자.
ㄹ ㅎㄱ
▶️
 IO('3')
  • 이번에는 IO('3') 이 결과로 표시되었다.
  • 평범한 한글은 결과를 표시할 때 드나듦 객체를 IO( ...) 처럼 표시한다. () 안에는 드나듦이 품은 값을 표시한다.
  • ㄹ ㅎㄱ 처럼 똑같은 모양으로 함수를 호출했는데도 키보드의 입력에 따라서 어떨 때는 IO('평범한 한글')을 내놓고, 또 다른 때는 IO('3') 을 내놓는다.
  • 함수는 키보드의 입력에 따라서 드나듦의 상태를 변화시킨다. ⇒ 부작용이 생긴다. ⇒ 부작용이 생기는 것은 외부 세계의 특징이다.
 

문자열 다루기 - 길이 얻기, 변환하기

  • 문자열을 다루는 세 가지 함수를 알아보자.
  • ㅁㅈ: 실수를 문자열로 변환한다. 인수 없이 호출하면 빈 문자열('')을 내놓는다. ('문자'로 외우자.)
ㄷㄴㄱ ㅁㅈ ㅎㄴ
▶️
'10'
  • ㅈㄷ: 문자열을 세어서 길이를 내놓는다. (길이를 '재다'로 외우자.)
ㄷㄴㄱ ㅁㅈ ㅎㄴ ㅈㄷ ㅎㄴ
▶️
2
  • ㅅㅅ: 문자열을 실수로 변환한다. 두 개의 인수를 받는데, 첫 번째 인수는 변환할 문자열, 두 번째 인수는 사용할 진법을 실수로 넘겨 준다. 두 번째 인수를 생략하면 10진법으로 변환한다.
ㄷㄴㄱ ㅁㅈ ㅎㄴ ㅅㅅ ㅎㄴ ㄷㄴㄱ ㅁㅈ ㅎㄴ ㄷ ㅅㅅ ㅎㄷ
▶️
10 2
  • 첫 번째 줄은 ㄷㄴㄱ를 문자열로 바꾼 다음 다시 실수로 바꾸는 코드.
  • 두 번째 줄은 ㄷㄴㄱ를 문자열로 바꾼 다음 2진법을 사용하여 실수로 바꾸는 코드. '10'은 2진수로 2.

드나듦 = 객체 + 계획

  • 오~~래 기다리셨습니다! 😄 드디어 드나듦의 세계로!!! 💦
  • 함수는 키보드 입력을 받아 이를 문자열로 만들고 드나듦으로 감싼 다음 내놓는다.
  • 그래서 함수가 내놓은 객체를 그대로 사용하면 큰일이 난다. 다음처럼...
ㄹ ㅎㄱ ㅈㄷ ㅎㄴ
▶️
문제가 생겼습니다. TypeError: Arguments of type function ListV(value) { this.value = value },function StringV(value) { this.value = value } expected but received: [object Object]
  • 아예 입력창이 뜨지도 않고 에러를 출력한다. 메시지를 살펴보면 ㅈㄷ 함수는 문자열이나 목록(list)을 인수로 받을 수 있는데 뭔가 다른 값이 전달되었다는 뜻.
  • 함수는 드나듦 객체를 내놓는다. ⇒ 드나듦은 함수로 바로 전달할 수 없다. 왜? 외부 세계의 물건이니까.
 
  • 드나듦이란? 그 속에 "객체"와 그 객체로 무엇을 할 것인지에 대한 "계획"을 품고 있는 것.
    • 💬
      " 너는 다 계획이 있구나!"
ㄹ ㅎㄱ
▶️
IO('3') ※ 키보드에서 3Enter 를 입력.
  • 함수를 호출하면
      1. "키보드에서 무언가를 입력 받는다."는 계획(또는 행동, action)을 품은 드나듦이 만들어진다.
      1. 방금 만들어진 계획이 실행되면서 키보드에서 입력을 받는다.
      1. 방금 입력받은 것을 드나듦으로 감싸서 내놓는다.
🔑
함수가 내놓은 드나듦을 사용하려면 "어떻게 이 드나듦을 사용할지 계획"을 세워야 된다.
 
  • 우리가 하고 싶은 일을 정리하자.
      1. 키보드에서 값을 입력 받는다.
      1. 입력 받은 값의 길이를 구한다.
      1. 위에서 구한 길이를 드나듦으로 감싸서 내놓는다.
  • 1.ㄹ ㅎㄱ 을 호출하면 실행할 수 있다.
  • 2.3. 을 할 수 있는 계획을 짜면 된다.
    • 어떻게? ⇒ 👉👉👉👉👉 함수를 이용해서!!! 👈👈👈👈👈
  • 만약 1. 에서 입력 받은 문자열을 인수로 받을 수 있다면 다음과 같이 함수를 만들면 된다.
ㄱㅇㄱ ㅈㄷㅎㄴ [드나듦으로 감싸는 함수] ㅎㄴ ㅎ
  • ㄱㅇㄱ 는 함수가 전달 받은 인수이므로 ㅈㄷㅎㄴ 로 길이를 구할 수 있다. 여기까지가 2. 이다.
  • 2. 에서 구한 것을 드나듦으로 감싸는 함수를 호출하면 3. 을 달성할 수 있다.
 
  • ㄱㅅ: 감싸기. 인수 하나를 받아서 드나듦으로 감싼 다음 내놓는다.
ㄷㄴㄱ ㄱㅅ ㅎㄴ ㄷㄴㄱ ㅁㅈ ㅎㄴ ㄱㅅ ㅎㄴ
 
▶️
IO(10) IO('10')
 
  • 이제 우리가 세우려고 했던 계획을 완성하자.
ㄱㅇㄱ ㅈㄷㅎㄴ ㄱㅅㅎㄴ ㅎ
  • 이제 ㄹ ㅎㄱ 함수가 내놓은 드나듦에 우리의 계획을 묶어주기면 하면 된다.
 
  • ㄱㄹ: 묶기(bind). 하나 이상의 드나듦과 계획으로 쓰일 함수 하나를 묶어서 새로운 드나듦을 내놓는다. ('~기로 하다.'로 기억하자. 보기: 밥을 먹기로 하다, 공부를 하기로 하다, 놀기로 하다.)
 
ㄹ ㅎㄱ ㄱㅇㄱ ㅈㄷㅎㄴ ㄱㅅㅎㄴ ㅎ ㄱㄹㅎㄷ
▶️
IO(1) ※ 3 Enter IO(6) ※ 평범한 한글 Enter IO(13) ※ Hello, world! Enter IO(33) ※ 나 보기가 역겨워 가실 때에는 말 없이 고이 보내드리오리다. Enter
  • 🙌 우왓! 드디어 성공이다! 키보드에서 입력 받은 문자열의 길이를 구할 수 있다! 🎊🎉
 
  • 첫 번째 줄 ㄹ ㅎㄱ 은 키보드에서 입력을 받아 그 값을 드나듦으로 감싸 내놓는다.
  • 두 번째 줄 ㄱㅇㄱ ㅈㄷㅎㄴ ㄱㅅㅎㄴ ㅎ 은 입력 받은 값(ㄱㅇㄱ)의 길이를 구하고(ㅈㄷㅎㄴ), 이를 드나듦으로 감싸서 내놓는(ㄱㅅㅎㄴ) 함수()이다.
  • 세 번째 줄 ㄱㄹㅎㄷ 는 첫 번째 줄에서 구한 드나듦 객체와 두 번째 줄에서 만든 계획을 서로 묶어서(ㄱㄹㅎㄷ) 새로운 드나듦 객체를 만들고 이것을 실행시킨다.
 
🔑
드나듦을 다루기 위한 계획을 작성하는 함수는 항상 드나듦을 내놓아야 한다.
 
💡
드나듦은 부작용이 있는 외부 세계이기 때문에 내부 세계의 함수는 드나듦 속 객체에 접근할 수 없다. 하지만 드나듦에 묶이는 계획(함수)은 ㄱㅇㄱ 처럼 드나듦 속 객체를 인수로 받을 수 있다. 계획으로 쓰일 함수는 내부 세계가 아니라 외부 세계에에서 실행된다고 볼 수 있다.
 

들어가고 나가고 - 입출력 연습하기

  • 입력을 받을 때는 을 썼다면 출력할 때는 ㅈㄹ 함수를 쓴다.
  • ㅈㄹ: 표준 출력. 문자열 하나를 받아 출력하고 빈값(Nil)을 드나듦으로 감싸서 내놓는다. ('출력'으로 외우자.) 'ㅈㄹ'은 여러분이 익히 추측할 수 있는 그런 욕이 절대로 아닙니다! 😳😱🥶🤣
  • 이제 이것들을 가지고 이것 저것을 입출력해보자.
 

1. 키보드로 받은 것을 그대로 출력하기

  • 키보드로 입력 받은 값을 그대로 출력하려면 어떻게 해야 할까?
  • 무엇을 해야 할지 정리해보자.
      1. 키보드에서 문자를 입력받는다.
      1. 입력 받은 문자를 출력하는 계획을 세운다. (함수를 만든다)
      1. 앞서 세운 계획을 1. 에서 내놓은 드나듦에 묶는다.
ㄹ ㅎㄱ ㄱㅇㄱ ㅈㄹㅎㄴ ㅎ ㄱㄹㅎㄷ
▶️
IO(Nil) 키보드로 입력한 값을 그대로 출력한다.
  • 두 번째 줄에서 드나듦 객체로 감싸는 ㄱㅅㅎㄴ 는 필요 없다. 왜냐하면 ㅈㄹ 함수는 언제나 빈 값을 품고 있는 드나듦(IO(Nil))을 내놓기 때문이다.
 

2. 키보드에서 입력 받은 숫자에 1을 더해서 출력하기

  • 함수로 입력 받은 것은 문자열이므로 바로 더하기를 할 수 없다.
  • ㅅㅅ 함수를 이용해서 입력받은 문자열을 실수로 바꾼 다음 덧셈을 할 수 있다.
  • 할 일 정리:
      1. 키보드에서 값을 입력 받는다.
      1. 입력 받은 값에 대한 계획을 세운다.
        1. 입력 받은 값을 실수로 변환한다.
        2. 이 값에 1을 더한다.
        3. 더한 값을 문자열로 변환한다.
        4. 위에서 변환한 값을 화면에 출력한다.
      1. 드나듦에 계획을 묶는다.
       
ㄹㅎㄱ 1. ㄱㅇㄱ ㅅㅅㅎㄴ 2. ㄴ ㄷㅎㄷ 3. ㅁㅈㅎㄴ 4. ㅈㄹㅎㄴ ㅎ ㄱㄹㅎㄷ
  • '평범한 한글'은 한글이 아닌 모든 것을 무시하기 때문에 위와 같이 코드를 써도 실행되기는 한다.
  • 그렇지만 코드를 공부하는 목적이 아니라면 다음처럼 깔끔하게 쓰는 것을 권한다.
ㄹㅎㄱ ㄱㅇㄱ ㅅㅅㅎㄴ ㄴ ㄷㅎㄷ ㅁㅈㅎㄴ ㅈㄹㅎㄴ ㅎ ㄱㄹㅎㄷ
  • 다음 처럼 한 줄에 적는 방법도 있다.
(ㄹㅎㄱ) (ㄱㅇㄱ ㅅㅅㅎㄴ ㄴ ㄷㅎㄷ ㅁㅈㅎㄴ ㅈㄹㅎㄴ ㅎ) ㄱㄹㅎㄷ
  • 위와 같이 괄호를 이용하면 ㄱㄹㅎㄷ 가 어떤 인수를 받는지 명확하게 구분할 수 있다.
 

3. 키보드에서 숫자를 두 번 입력 받아서 곱한 것을 출력하기

  • 키보드에서 입력을 받아서 이를 실수로 바꾸는 작업을 잊지 않도록!
ㄹㅎㄱ ㄹㅎㄱ ㄱㅇㄱ ㅅㅅㅎㄴ ㄴㅇㄱ ㅅㅅㅎㄴ ㄱㅎㄷ ㅁㅈㅎㄴ ㅈㄹㅎㄴ ㅎ ㄱㄹㅎㄹ
  • 키보드로 입력을 두 번 받으면 당연히 드나듦 객체로 두 개가 된다.
  • 계획에서 첫 번째 드나듦은 ㄱㅇㄱ에 전달되고 두 번째 드나듦은 ㄴㅇㄱ에 전달된다.
  • 각각을 실수로 변환한 다음(ㅅㅅㅎㄴ) 곱하고(ㄱㅎㄷ), 문자열로 변환하여(ㅁㅈㅎㄴ) 화면에 출력(ㅈㄹㅎㄴ) 하는 계획()이 두 번째 줄이다.
  • 드나듦 두 개와 계획 하나, 모두 세 계를 묶어야 하므로 ㄱㄹ 함수에 는 세 개의 인수를 전달해야(ㅎㄹ) 한다.
 

4. 'Hello world!'를 출력하기

(ㅂ ㅂ ㅂㅎㄷ) ㄱㄴㅂㄷㅅㄱㄹㄹㅁㅂㅂㅈㅅㄱㄹㄴㄱㅁㅁㄹㅈㅅㄹㄹㄷㅅㄴㅅㅅㄱㄴㄹㄴㅁㄱ (ㄴ ㅂㄴㄱ ㄱㅇㄱㅎㄷ)ㅎㄴ (ㄱ ㄴ ㄱㅇㄱㅎㄷ)ㅎㄴ ㅎㅎㄴ ㄱㅅㅎㄴ ㄱㅇㄱ ㅈㄹㅎㄴ ㅎ ㄱㄹㅎㄷ
▶️
'Hello, world!' 출력
 
notion imagenotion image
 
  • (ㅂ ㅂ ㅂㅎㄷ) ㄱㄴㅂㄷㅅㄱㄹㄹㅁㅂㅂㅈㅅㄱㄹㄴㄱㅁㅁㄹㅈㅅㄹㄹㄷㅅㄴㅅㅅㄱㄴㄹㄴㅁㄱ (ㄴ ㅂㄴㄱ ㄱㅇㄱㅎㄷ)ㅎㄴ (ㄱ ㄴ ㄱㅇㄱㅎㄷ)ㅎㄴ ㅎㅎㄴHello, world! 를 내놓는 코드이다.
  • 위의 코드로 만들어진 Hello, world! 문자열을 드나듦으로 감싼다(ㄱㅅㅎㄷ).
  • 위의 드나듦(ㄱㅇㄱ)을 출력하는(ㅈㄹㅎㄴ) 계획()을 세운다.
  • 드나듦과 계획을 묶어서(ㄱㄹㅎㄷ) 실행한다. 드디어 '평범한 한글'로 Hello, world! 를 출력했다!
 
TMI: '평범한 한글'의 문자열 처리
  • 도대체 Hello, world! 문자열을 생성하려고 왜 (ㅂ ㅂ ㅂㅎㄷ) ㄱㄴㅂㄷㅅㄱㄹㄹㅁㅂㅂㅈㅅㄱㄹㄴㄱㅁㅁㄹㅈㅅㄹㄹㄷㅅㄴㅅㅅㄱㄴㄹㄴㅁㄱ (ㄴ ㅂㄴㄱ ㄱㅇㄱㅎㄷ)ㅎㄴ (ㄱ ㄴ ㄱㅇㄱㅎㄷ)ㅎㄴ ㅎㅎㄴ 코드를 실행해야 하는 걸까?
  • '평범한 한글'에서 문자열은 '바이트 열'(byte string)을 만들고 이를 다시 UTF-8로 인코딩을 해야 만들 수 있기 때문이다.
  • 그래서 문자열을 만들기 위해서는 바이트 열과 유니코드에 대한 이해가 필요하다. 문자열에 대한 내용은 추후 다룰 예정이다.
  • 하지만 문자열을 만들 때마다 위와 같은 코드를 계산해서 만드는 것은 굉장히 번잡하다. 난해해서 힘든 것과 번잡해서 짜증이 나는 것은 엄연히 다르다.
  • 다행이 '평범한 한글'에서는 사용자가 입력한 문자열을 '평범한 한글'에서 쓸 수 있는 문자열로 바꾸어 주는 모듈을 제공한다.
    • 조각글 문자만드는평범코드드립니다 ㅂㅎㄷ
  • 위의 코드를 실행하면 사용자의 입력을 기다리는데, 여기서 생성하고 싶은 문자열을 입력하고 Enter를 누르면 문자열을 생성할 수 있는 '평범한 한글' 코드가 만들어진다.
    • 🚧
      ※ 현재 웹 실행 환경에서는 파일 불러오기 기능이 제공되지 않으므로 Python이나 NodeJS 구현체를 사용해야 한다.
       
  • 드나듦은 순수 함수로 이루어진 내부 세계와 부작용이 있는 외부 세계를 이어주는 고마운 통로이다. 처음엔 낯설지만 자주 접해보면 익숙해질 수 있을 것이다. (사실 '평범한 한글'의 모든 게 다 그랬다.)
 
TMI: 하스켈의 IO Monad
하스켈(Haskell)에서는 순수 함수로 계산되는 것은 let 으로 묶고, 부작용이 일어나는 상황에서는 do 와 같은 방법을 사용한다. 외부 세계와 입출력을 할 때에는 다음과 같은 식으로 처리한다.
main = do putStrLn "What's your name?" name <- getLine putStrLn ("Hello, " ++ name ++ "!")
  • 사실 하스켈은 함수형 언어이기는 하지만 언어의 목표 자체가 '실생활의 문제를 풀 수 있는, 응용 프로그램을 작성할 수 있는 언어'이기 때문에 다분히 명령형 언어의 특징들을 많이 구현해 놓았다.
  • 그래서 하스켈에서는 굳이 모나드의 존재를 의식하지 않아도 되지만 '평범한 한글'은 순수 함수가 언어의 바탕을 이루고 있기 때문에 부작용과 모나드의 관계를 알고 있는 것이 도움이 된다. 어차피 '평범한 한글'은 난해한 언어니까! 🤣