3 . 변수, 표현식, 문장
3.1 값과 자료형
값(Value)은 문자와 숫자처럼 프로그램이 다루는 가장 기본이 되는 단위이다. 지금까지 살펴본 값은 1, 2 그리고 ’헬로 월드!’다.
상기 값은 다른 자료형(Type)에 속하는데, 2는 정수(integer), ‘헬로 월드!’ 는 문자열(String)에 속하는데, 문자(character)를 일련의 열(sequence)의 형태로 되어 있어서 문자열이라고 부른다. 인용부호에 감싸여 있어서, 여러분과 인터프리터는 문자열을 식별할 수 있다.
print
문은 정수에도 사용할 수 있다. R 명령어를 실행하여 인터프리터를 구동시키자.
print(7)
## [1] 7
값이 어떤 형인지 확신을 못한다면, 인터프리터가 알려준다.
typeof("헬로 월드!")
## [1] "character"
typeof(17)
## [1] "double"
다소 놀랄수도 있겠지만, 17
은 부동소수점 숫자 형식 더블(double)이고 헬로 월드!
는 문자(character)가 된다.
typeof(7L)
## [1] "integer"
정수형을 필히 표현하려면 7L
와 같이 정수 뒤에 L
을 붙이면 된다.
‘17’, ‘3.2’ 같은 값은 어떨가? 숫자처럼 보이지만 문자열처럼 인용부호에 감싸여 있다.
typeof('17L')
## [1] "character"
typeof('3.2')
## [1] "character"
‘17’, ‘3.2’ 은 문자열이다.
1,000,000 처럼 아주 큰 정수를 입력할 때, 사람이 인식하기 편한 형태로 세자리 숫자마다 콤마(,)를 사용하고 싶을 것이다. 하지만, R에서는 오류가 난다.
> 1,000,000
: unexpected ',' in "1," Error
파이썬의 경우 파이썬에서는 정상적으로 실행되나, 실행 결과는 우리가 기대했던 것이 아니다. 파이썬에서는 1,000,000 을 콤마(‘,’)로 구분된 정수로 인식한다. 따라서 사이 사이 공백을 넣어 출력했다. 이 사례가 여러분이 처음 경험하게 되는 의미론적 오류(semantic error)다. 코드가 에러 메세지 없이 실행이되지만, “올바른(right)” 작동을 하는 것은 아니다.
3.2 변수
프로그래밍 언어의 가장 강력한 기능 중의 하나는 변수를 다룰 수 있는 능력이다. 변수(Variable)는 값을 참조하는 이름이다. 대입문(Assignment statement)는 새로운 변수를 생성하고 값을 변수에 대입한다.
message <- "매월 세째주 수요일 R Meetup이 열립니다."
n <- 17L
pi <- 3.1415926535897931
상기 예제는 세가지 대입 사례를 보여준다.
첫 번째 대입 예제는 message
변수에 문자열을 대입한다.
두 번째 예제는 변수 n
에 정수 17을 대입한다.
세 번째 예제는 pi
변수에 근사값을 대입한다.
변수 값을 출력하기 위해서 print
문을 사용하도 되지만, 일반적으로 변수명을 콘솔에서 타이핑하면 된다.
print(message)
## [1] "매월 세째주 수요일 R Meetup이 열립니다."
n
## [1] 17
pi
## [1] 3.14
변수 자료형(type)은 변수가 참조하는 값의 자료형이다.
typeof(message)
## [1] "character"
typeof(n)
## [1] "integer"
typeof(pi)
## [1] "double"
3.3 변수명과 예약어
대체로 프로그래머는 의미있는 변수명(variable name)을 고른다. 프로그래머는 변수가 사용되는 것에 대해 문서화도 한다.
변수명은 임의로 길 수 있다. 변수명은 문자와 숫자를 포함할 수 있지만, 문자로 변수명을 시작해야 한다. 첫 변수명을 대문자로 사용해도 되지만 소문자로 변수명을 시작하는 것도 좋은 생각이다. (후에 왜 그런지 보게 될 것이다.)
변수명에 밑줄(underscore character, _)이 들어갈 수 있다.
종종 my_name
혹은 airspeed_of_unladen_swallow
처럼 밑줄은 여러 단어와 함께 사용된다.
변수명을 밑줄로 시작해서 작성할 수 있지만,
다른 사용자가 사용할 라이브러리를 작성하는 경우가 아니라면,
일반적으로 밑줄로 시작하는 변수명은 피한다.
한글을 변수명으로 사용하는 것도 가능하지만, 인코딩 등 여타 예기치 못한 문제가 생길 수도 있다는 점을 유념하고 사용한다.
R이 다른 언어와 다른 점은 <-
을 변수명에 값을 대입하는데 사용하는 점이다. 이유는 R이 한창 개발될 당시 가장 최신 이론에 바탕을 두고 있기 때문이다.
수학적으로 variable_name = 123L
와 같은 문장이 맞는지 곰곰히 생각해 보면 그 당시 <-
기호를 사용한 이유를 유추할 수 있다.
변수명을 적합하게 작성하지 못하다면, 구문 오류가 발생한다.
<- 'big parade'
76trombones @ <- 1000000
morerepeat <- 'Advanced Theoretical Zymurgy'
76trombones
변수명은 문자로 시작하지 않아서 적합하지 않다.
more@
은 특수 문자 (@)를 변수명에 포함해서 적합하지 않다.
하지만, repeat
변수명은 뭐가 잘못된 것일까?
구문 오류 이유는 repeat
이 R의 예약어 중의 하나라고 밝혀졌다.
인터프리터가 예약어를 사용하여 프로그램 구조를 파악하기 위해서 사용하지만, 변수명으로는 사용할 수 없다.
R 예약어 | 설명 |
---|---|
If, else, repeat, while, function, for, in, next, break | 조건, 함수, 반복문에 사용 |
TRUE, FALSE | 논리 상수(Logical constants) |
NULL | 정의되지 않는 값 혹은 값이 없음을 표현 |
Inf | 무한(Infinity) |
NaN | 숫자가 아님(Not a Number) |
NA | 결측값, 값이 없음 (Not Available) |
NA_integer_, NA_real_, NA_complex_, NA_character_ | 결측값 처리하는 상수 |
… | 함수가 다른 함수에 인자를 전달하도록 지원 |
상기 예약어 목록을 주머니에 넣고 잘 가지고 다니고 싶을 것이다. 만약 인터프리터가 변수명 중 하나에 대해 불평을 하지만 이유를 모르는 경우, 예약어 목록에 변수명이 있는지 확인해 보세요.
3.4 문장
문장(statement)은 R 인터프리터가 실행하는 코드 단위다.
지금까지 print
, 대입
(assignment, <-
) 두 종류의 문장을 살펴봤습니다.
인터랙트브 모드에서 문장을 입력하면, 인터프리터는 문장을 실행하고, 만약 출력할 것이 있다면 결과를 화면에 출력합니다. 스크립트는 보통 여러줄의 문장으로 구성됩니다. 하나 이상의 문장이 있다면, 문장이 순차적으로 실행되며서 결과가 한번에 하나씩 나타납니다.
예를 들어, 다음의 스크립트를 생각해 봅시다.
1
x <- 2
x
상기 스크립트는 다음 결과를 출력합니다.
## [1] 1
## [1] 2
대입 문장(x <- 2
)은 결과를 출력하지 않습니다.
3.5 연산자와 피연산자
연산자(Operators)는 덧셈, 곱셈 같은 계산(Computation)을 표현하는 특별한 기호입니다. 연산가자 적용되는 값을 피연산자(operands)라고 합니다.
다음의 예제에서 보듯이, +, -, *, /, ** 연산자는 덧셈, 뺄셈, 곱셈, 나눗셈, 지수승을 수행합니다.
20+32 hour-1 hour*60+minute minute/60 5**2 (5+9)*(15-7)
3.6 표현식
표현식 (expression)은 값, 변수, 연산자 조합이다. 값은 자체로 표현식이고, 변수도 동일하다. 따라서 다음 표현식은 모두 적합하다. (변수 x는 사전에 어떤 값이 대입되었다고 가정한다.)
17
x
x + 17
인터랙티브 모드에서 표현식을 입력하면, 인터프리터는 표현식을 평가(evaluate)하고 값을 표시한다.
1 + 1
## [1] 2
하지만, 스크립트에서는 표현식 자체로 어떠한 것도 수행하지는 않는다. 초심자에게 혼란스러운 점이다.
연습문제. R 인터프리터에 다음 문장을 입력하고 결과를 보세요.
5
x <- 5
x + 1
3.7 연산자 적용 우선순위
1개 이상의 연산자가 표현식에 등장할 때, 연산자 평가 순서는 우선순위 규칙(rules of precedence)에 따른다. 수학 연산자에 대해서 파이썬은 수학적 관례를 동일하게 따른다. 영어 두문어 PEMDAS는 기억하기 좋은 방식이다.
-
괄호(Parentheses)는 가장 높은 순위를 가지고 여러분이 원하는 순위에 맞춰 실행할 때 사용한다.
괄호내의 식이 먼저 실행되기 때문에
2 * (3-1)
은 4가 정답이고,(1+1)**(5-2)
는 8이다. 괄호를 사용하여 표현식을 좀더 읽기 쉽게 하려고 사용하기도 한다.(minute * 100) / 60
는 실행순서가 결과값에 영향을 주지 않지만 가독성이 상대적으로 더 좋다. -
지수승(Exponentiation)이 다음으로 높은 우선순위를 가진다. 그래서
2**1+1
는 4가 아니라 3이고,3*1**3
는 27이 아니고 3이다. -
곱셈(Multiplication)과 나눗셈(Division)은 동일한 우선순위를 가지지만, 덧셈(Addition), 뺄셈(Substraction)보다 높은 우선 순위를 가진다.
덧셈과 뺄샘은 같은 실행 우선순위를 갖는다.
2*3-1
는 4가 아니고 5이고,6+4/2
는 5가 아니라 8이다. - 같은 실행 순위를 갖는 연산자는 왼쪽에서부터 오른쪽으로 실행된다.
5-3-1
표현식은 3이 아니고 1이다. 왜냐하면5-3
이 먼저 실행되고 나서 2에서 1을 빼기 때문이다.
여러분이 의도한 순서대로 연산이 수행될 수 있도록, 좀 의심스러운 경우는 항상 괄호를 사용한다.
3.8 나머지 연산자
나머지 연산자(modulus operator)는 정수에 사용하며, 첫번째 피연산자를 두번째 피연산자가 나눌 때 나머지 값이 생성된다. 파이썬에서 나머지 연산자는 퍼센트 기호(%)다. 구문은 다른 연산자와 동일하다.
7을 3으로 나누면 몫이 2가 되고 나머지가 1이 된다.
나머지 연산자가 놀랍도록 유용다다. 예를 들어 한 숫자를 다른 숫자로 나눌 수 있는지 없는지를 확인할 수도 있다.
x %% y
값이 0 이라면, x를 y로 나눌 수 있다.
또한, 숫자에서 가장 오른쪽 숫자를 분리하는데도 사용된다.
예를 들어 x %% 10
은 x가 10진수인 경우 가장 오른쪽 숫자를 뽑아낼 수 있고, 동일한 방식으로 x %% 100
은 가장 오른쪽 2개 숫자를 뽑아낼 수도 있다.
나눗셈 연산자의 경우 minute
값은 59, 보통 59를 60으로 나누면 0 대신에 0.98333 입니다.
minute <- 59
minute / 60
## [1] 0.983
하지만, 몫을 minute %/% 60
와 같이 계산하여 0 얻고,
나머지를 minute %% 60
와 같이 계산하여 59을 얻게 된다.
3.9 문자열 연산자
+
연산자는 문자열는 동작하지 않는다.
대신에 문자열 끝과 끝을 붙이는 연결(concatenation) 작업을 수행할 때 paste()
함수를 사용한다. 예를 들어,
first <- 10
second <- 15
first + second
## [1] 25
first <- '100'
second <- '150'
paste(first, second, sep="")
## [1] "100150"
상기 프로그램 출력은 100150 이다.
3.10 입력값 받기
때때로 키보드를 통해서 사용자로부터 변수에 대한 값을 입력받고 싶을 때가 있다.
키보드로부터 입력값을 받는 readline()
이라는 내장(built-in) 함수를 R에서 제공한다.
입력 함수가 호출되면, R은 실행을 멈추고 사용자가 무언가 입력하기를 기다린다.
사용자가 Return (리턴) 혹은 Enter (엔터) 키를 누르게 되면 프로그램이 다시 실행되고,
readline()
은 사용자가 입력한 것을 문자열로 반환한다.
<- readline()
input
R은 사랑입니다.
input1] "R은 사랑입니다." [
사용자로부터 입력 받기 전에 프롬프트에서 사용자가 어떤 값을 입력해야 하는지 정보를 제공하는 것도 좋은 생각이다. 입력을 받기 위해 잠시 멈춰있을 때, 사용자에게 표시되도록 readline() 함수에 문자열을 전달할 수 있다.
<- readline(prompt="아무거나 입력하시요: ")
input
: R은 사랑입니다.
아무거나 입력하시요
input1] "R은 사랑입니다." [
경우에 따라서 프롬프트의 끝에 \n
을 넣는 경우도 있는데 개행(newline)을 의미한다.
개행은 줄을 바꾸게 하는 특수 문자다.
사용자 입력이 프롬프트 밑에 출력되도록 줄바꿈이 필요한 경우 사용한다.
만약 사용자가 정수를 입력하기를 바란다면, int()함수를 사용하여 반환되는 값을 정수(int)로 자료형을 변환한다.
<- '속도가 얼마나 됩니까? '
prompts <- readline(prompt=prompts)
speed 20
속도가 얼마나 됩니까? as.integer(speed) + 5
1] 25 [
하지만, 사용자가 숫자 문자열이 아닌 다른 것을 입력하게 되면 오류가 발생한다.
<- readline(prompt=prompts)
speed !!!
속도가 얼마나 됩니까? 뭐라고 하셨나요as.integer(speed) + 5
1] NA [
나중에 이런 종류의 오류를 어떻게 다루는지 배울 것이다.
3.11 주석
프로그램이 커지고 복잡해짐에 따라 가독성은 떨어진다. 형식 언어(formal language)는 촘촘하고 코드 일부분도 읽기 어렵고 무슨 역할을 왜 수행하는지 이해하기 어렵다.
이런 이유로 프로그램이 무엇을 하는지를 자연어로 프로그램에 노트를 달아두는 것은 좋은 생각이다.
이런 노트를 주석(Comments)이라고 하고 #
기호로 시작한다.
# 경과한 시간을 퍼센트로 계산
percentage <- (minute * 100) / 60
상기 사례의 경우, 주석 자체가 한줄이다. 주석을 프로그램의 맨 뒤에 놓을 수도 있다.
percentage <- (minute * 100) / 60 # 경과한 시간을 퍼센트로 계산
뒤의 모든 것은 무시되기 때문에 프로그램에는 아무런 영향이 없다.
명확하지 않은 코드의 기능을 문서화할 때 주석은 가장 유용하게 된다. 프로그램을 읽는 사람이 코드가 무엇을 하는지 이해한다고 가정하는 것은 일리가 있다. 왜 그런지를 이유를 설명하는 것은 더욱 유용하다.
다음의 주석은 코드와 중복되어 쓸모가 없다.
v <- 5 # 5를 v에 대입
다음의 주석은 코드에 없는 유용한 정보가 있다.
v <- 5 # 미터/초 단위로 측정된 속도
좋은 변수명은 주석을 할 필요를 없게 만들지만, 지나치게 긴 변수명은 읽기 어려운 복잡한 표현식이 될 수 있다. 그래서 상충관계(trade-off)가 존재한다.
3.12 연상되는 변수명
변수를 이름 짓는데 단순한 규칙을 지키고 예약어를 피하기만 하다면, 변수이름을 작명할 수 있는 무척이나 많은 경우의 수가 존재한다. 처음에 이렇게 넓은 선택폭이 오히려 프로그램을 읽는 사람이나 프로그램을 작성하는 사람 모두에게 혼란을 줄 수 있다. 예를 들어, 다음의 3개 프로그램은 각 프로그램이 달성하려하는 관점에서 동일하지만, 여러분이 읽고 이해하는데는 많은 차이점이 있다.
a <- 35.0
b <- 12.50
c <- a * b
print(c)
## [1] 438
hours <- 35.0
rate <- 12.50
pay <- hours * rate
print(pay)
## [1] 438
x1q3z9ahd <- 35.0
x1q3z9afd <- 12.50
x1q3p9afd <- x1q3z9ahd * x1q3z9afd
print(x1q3p9afd)
## [1] 438
R 인터프리터는 상기 3개 프로그램을 정확하게 동일하게 바라보지만, 사람은 이들 프로그램을 매우 다르게 보고 이해한다. 사람은 가장 빨리 두 번째 프로그램의 의도를 알아차린다. 왜냐하면 각 변수에 무슨 데이터가 저장될지에 관해서, 프로그래머의 의도를 반영하는 변수명을 사용했기 때문이다.
현명하게 선택된 변수명을 연상기호 변수명(“mnemonic variable name”)이라고 한다. 연상되기 좋은 영어 단어 (“mnemonic”)은 기억을 돕는다는 뜻이다. 왜 변수를 생성했는지 기억하기 좋게 하기 위해서 연상하기 좋은 변수명을 선택한다.
매우 훌륭하게 들리고, 연상하기 좋은 변수명을 만드는게 좋은 아이디어 같지만, 기억하기 좋은 변수명은 초보 프로그래머가 코드를 파싱(parsing)하고 이해하는데 걸림돌이 되기도 한다. 왜냐하면 얼마되지 않는 예약어도 기억하지 못하고, 변수명이 때때로 너무 서술적이라 마치 일반적으로 사용하는 언어처럼 보이고 잘 선택된 변수명처럼 보이지 않기 때문이다.
어떤 데이터를 반복하는 다음 파이썬 코드를 살펴보자. 곧 반복 루프를 살펴보겠지만, 다음 코드가 무엇을 의미하는지 알기 위해서 퍼즐을 풀어보자.
## [1] "봄"
## [1] "여름"
## [1] "가을"
## [1] "겨울"
무엇이 일어나고 있는 것일까? for, word, in 등등 어느 토큰이 예약어일까? 변수명은 무엇일까? 파이썬은 기본적으로 단어의 개념을 이해할까? 초보 프로그래머는 어느 부분 코드가 이 예제와 동일해야만 하는지 그리고, 어느 부분 코드가 프로그래머 선택에 의한 것인지 분간하는데 고생을 한다.
다음의 코드는 위의 코드와 동일하다.
초보 프로그래머가 이 코드를 보고 어떤 부분이 R 예약어이고 어느 부분이 프로그래머가 선택한 변수명인지 알기 쉽다. R이 피자와 피자조각에 대한 근본적인 이해가 없고, 피자는 하나 혹은 여러 조각으로 구성된다는 근본적인 사실을 알지 못한다는 것은 자명하다.
하지만, 작성한 프로그램이 데이터를 읽고 데이터에 있는 단어를 찾는다면 피자(pizza)와 피자조각(slice)은 연상하기 좋은 변수명이 아니다. 이것을 변수명으로 선핸하게 되면 프로그램의 의미를 왜곡시킬 수 있다.
좀 시간을 보낸 후에 가장 흔한 예약어에 대해서 알게 될 것이고, 이들 예약어가 어느 순간 여러분에게 눈에 띄게 될 것이다.
R에서 정의된 코드 일부분(for, in, print)은 예약어로 굵게 표시되어 있고, 프로그래머가 생성한 변수명(word, words)는 굵게 표시되어 있지 않다. 대다수 텍스트 편집기는 R 구문을 인지하고 있어서, R 예약어와 프로그래머가 작성한 변수를 구분하기 위해서 색깔을 다르게 표시한다. 잠시 후에 여러분은 R을 읽고 변수와 예약어를 빠르게 구분할 수 있을 것이다.
3.13 디버깅(Debugging)
이 지점에서 여러분이 저지르기 쉬운 구문 오류는 odd~job
, US$
같은 특수문자를 포함해서 잘못된 변수명을 생성하는 것과
repeat
, while
같은 예약어를 변수명으로 사용하는 것이다.
변수명에 공백을 넣는다면, 파이썬은 연산자 없는 두 개의 피연산자로 생각한다.
<- 5
bad name : unexpected symbol in "bad name" Error
구문 오류에 대해서, 오류 메세지는 그다지 도움이 되지 못한다.
가장 흔한 오류 메세지는 Error: unexpected symbol in "bad name"
인데 둘다 그다지 오류에 대한 많은 정보를 주지는 못한다.
여러분이 많이 범하는 실행 오류는 정의 전에 사용(“use before def”)하는 것으로 변수에 값을 대입하기 전에 변수를 사용할 경우 발생한다. 여러분이 변수명을 잘못 쓸 때도 발생할 수 있다.
<- 327.68
principal <- principle * rate
interest : object 'principle' not found Error
변수명은 대소문자를 구분한다. 그래서, LaTeX은
\(LaTeX\), latex
와 같지 않다.
이 지점에서 여러분이 범하기 쉬운 의미론적 오류는 연산자 우선 순위일 것이다. 예를 들어 \(frac{1}{2\pi}\)를 계산하기 위해서 다음과 같이 프로그램을 작성하게 되면 …
1.0 / 2.0 * pi
나눗셈이 먼저 일어나서 이 되는데 의도한 것과 같지 않다. R으로 하여금 여러분이 작성한 의도를 알게할 수는 없다. 그래서 이런 경우 오류 메세지는 없지만, 여러분은 잘못된 답을 얻게 된다.
3.14 용어 설명
- 대입(assignment): 변수에 값을 대입하는 문장
- 연결(concatenate): 두 개의 피연산자 끝과 끝을 합치는 것
- 주석(comment): 다른 프로그래머나 소스코드를 읽는 다른 사람을 위한 프로그램 정보로 프로그램의 실행에는 아무런 영향이 없다.
- 평가(evaluate): 하나의 값을 만들도록 연산을 실행함으로써 표현식을 간단히 하는 것
- 표현식(expression): 하나의 결과값을 만드는 변수, 연산자, 값의 조합
- 부동 소수점(floating-point): 소수점을 가진 숫자를 표현하는 자료형
- **버림 나눗셈(floor division)] 두 숫자를 나누어 소수점이하 부분을 절사하는 연산자
- 정수(integer): 완전수를 나타내는 자료형
- 예약어(keyword): 컴파일러가 프로그램을 파싱하는데 사용하기 위해서 이미 예약된 단어; if, def, while 같은 예약어를 변수명으로 사용할 수 없다.
- 연상기호(mnemonic): 기억 보조. 변수에 저장된 것을 기억하기데 도움이 되도록 변수에 연상되는 이름을 부여한다.
- 나머지 연산자(modulus operator): 퍼센트 기호 ()로 표시되고 정수를 가지고 한 숫자를 다른 숫자로 나누었을 때 나머지를 생성하는 연산자
- 피연산자(operand): 연산자가 연산을 수행하는 값중의 하나
- 연산자(operator): 덧셈, 곱셈, 문자열 결합 같은 간단한 연산을 표현하는 특별 기호
- 우선순위 규칙(rules of precedence): 다수의 연산자와 피연산자를 포함한 표현식이 평가되는 실행 순서를 규정한 규칙 집합
- 문장(statement): 명령이나 액션을 나타내는 코드 부문. 지금까지 assignment, print 문을 보았다.
- 문자열(string): 일련의 문자를 나타내는 형식
- 자료형(type): 값의 범주. 지금까지 여러분이 살펴본 자료형은 정수 (int), 부동 소수점수 (float), 문자열 (str) 이다.
- 값(value): 숫자나 문자 같은 프로그램이 다루는 데이터의 기본 단위중 하나
- 변수(variable): 값을 참조하는 이름
3.15 연습문제
-
readline()
을 사용하여 사용자의 이름을 입력받고 환영하는 프로그램을 작성하세요.
: 광춘
이름을 입력하시오 안녕하세요 광춘님
- 급여를 지불하기 위해서 사용자로부터 근로시간과 시간당 임금을 계산하는 프로그램을 작성하세요.
: 35.51
시간을 입력하시오: 7530
시급을 입력하시오: 263550 알바비
지금은 급여가 정확하게 소수점 두자리까지 표현되지 않아도 된다.
만약 원하다면, R 내장 round()
함수를 사용하여 소수점 아래 반올림하여 정수로 작성할 수도 있다.
- 다음 대입 문장을 실행한다고 가정합시다.
width <- 17
height <- 12.0
다음 표현식 각각에 대해서, 표현식의 값(value)과 (표현식 값의) 자료형(type)을 작성하세요.
width/2
width/2.0
height/3
1 + 2 * 5
정답을 확인하기 위해서 R 인터프리터를 사용하세요.
- 사용자로부터 섭씨 온도를 입력받아 화씨온도로 변환하고, 변환된 온도를 출력하는 프로그램을 작성하세요.