이 편에서 다룰 내용은

WoW API : 시간 포맷 관련. 오라 지속과 시전시간 정보
Lua : 조건문, 로컬 변수와 블럭
입니다.

구상하면서는 '내용이 너무 많을 것 같은데'했는데 만들어놓고 보니 내용이 되게 부실하네요.


예제 6-2-1 : 도트 리필 알림

조드 자원바가 월식 상태를 벗어나서 일식 상태로 넘어가려는 도중에 달섬 도트 지속 시간이 짧으면 리필하라는 텍스트를 알려줍니다.

구상을 해봅시다.
월식 피크 찍고 일식쪽으로 이동하는 것은 간단하게 달섬 즉시데미지 강화 발동 버프인 '달의 정점'을 추적하면 됩니다. 이게 5초 지속이거든요.
'달의 정점 오라가 있는데 달섬 지속 시간이 15초 정도보다 짧으면' 텍스트를 표시하게 하겠습니다.
사실 조건 두 개 만들고 and로 묶으면 되지만 흠흠...



텍스트 표시기를 만들고 달의 정점이 있을 때 표시되게 합니다.
디스플레이 탭에서는 글자 크기 적당히 키우고 %c, 매 프레임

편집창에는 다음과 같이

function()
local _, _, _, _, _, _, expires = UnitDebuff("target", "달빛섬광", nil, "PLAYER")
if not expires then
return "뿌엉!?"
end
if expires - GetTime() < 15 then
return "뿌엉!?"
end
end

지난 번의 예제에서 보셨다시피 UnitDebuff는 15개 이상의 반환값을 줍니다.
name, rank, icon, count, dispelType, duration, expires, caster, isStealable, shouldConsolidate, spellID, canApplyAura, isBossDebuff, value1, value2, value3 = UnitDebuff("unit", index [, "filter"]) or UnitDebuff("unit", "name" [, "rank" [, "filter"]])

그 중 저는 7번째, expires만 원합니다. 앞서서는 select(7, UnitDebuff( 이런 형태를 사용했는데 이번에는 조금 다른 형태를 써 봅시다.
언더스코어 _ 는 그 자체로 변수명으로 사용됩니다. 즉, 위의 표현은
_ = name값
_ = rank값
_ = icon값
_ = count값
_ = dispellType값
_ = duration값
expires = expires값
을 한 줄에 적어둔 것입니다. ( 실제로는 나눠서 하면 _ 에는 마지막 지정된 duration값이 남겠지만 코드에서처럼 한 번에 여러 번 쓰면 맨 앞 것만 지정되고 나머지는 무시됩니다.

select를 안쓰고 7번째 값을 얻어내려면 앞쪽 1~6번째에 해당하는 변수도 다 지정해야 되는데 어짜피 안쓸거면 괜히 하기 귀찮잖아요?
따라서 자릿수만 맞추기 위해 _를 사용한 것입니다. 이런 것을 dummy 파라미터라고 하고 대단히 흔하게 사용하는 트릭입니다.


6, 7번째 리턴인 duration과 expires가 지속시간에 관련된 값인데요, 허수아비에다 도트 거시고 채팅 창에
/dump UnitDebuff("target", "달빛섬광") 이렇게 해보시면 6, 7번째로 각각 40과 이상하게 큰 숫자가 나옵니다.
40은 도트의 전체 지속 시간이고요, 6번째의 숫자는 '도트가 종료되는 시점'입니다.

6번 반환값은 GetTime() 형식입니다.
와우에서 쓰는 초단위 시간이 두 가지가 있습니다.
GetTime()
time()
각각 /dump 해보시면 하나는 소수점 아래 세 자리까지의 ~수만 단위 숫자가 나올 것이고 아래 것은 14억 얼마짜리 정수입니다.

GetTime()은 부팅한 후 지난 시간을 초 단위로 표시한 것이고 이것이 와우 인터페이스의 표준 시간 단위입니다.
time()은 1970년 1월 1일 자정에서부터 지난 시간을 초 단위로 쓴 것입니다.
마스터 플랜에서 고소득 임무가 언제 떴는지를 time() 포맷으로 저장해둡니다. 재부팅해도 기억해야 되니까요.


자 아무튼, 버프, 디버프 등의 지속시간 정보는 '남은 시간'을 말하지 않고 '전체시간, 종료 시각' 형태로 표현합니다.
남은 시간은 시간이 지나면서 계속 변하는 값이기 때문에 계속 값을 바꾸어가며 반환해야 합니다.
전체 시간과 종료 시각이라면 처음 오라가 생성될 때 데이터를 만들면 바뀌지 않는 것이지요.

쿨다운, 시전시간 등등의 정보도 남은 시간이 아니라 '시작한 시각, 전체 시간'으로 표시됩니다.

if not expires then

if 뒤의 구문은 boolean 검사를 수행하므로 (not expires) 는 논리값이 됩니다.
expires가 nil이거나 false이면 expires의 논리값이 false, 따라서 not expires는 true가 되는데요(글 뒤쪽의 Lua 강좌 참고) 이걸 쓰는 것은 도트가 없어서 expires가 nil로 나오는 상황을 가려내기 위함입니다.
if not expires라는 구문은 '도트가 없다면' 에 해당하며 이렇다면 then 이하를 수행합니다.

return "뿌엉!?"
도트 리필하라고 글자를 출력합니다.

end 는 if문을 완료하기 위해 들어있는데 만약 도트가 없다면 return을 수행했기 때문에 이 함수는 여기서 끝납니다.
따라서 뒷부분은 '도트가 있을 때'에만 수행되고요.

if expires - GetTime() < 15 then
도트가 끝나는 시각 - 현재 시각  =  도트의 남은 시간

남은 시간이 15초 미만이면 역시 '뿌엉!?'




오른쪽의 도트 타이머는 ForteXorcist입니다.
일월식바 + 별쇄충전 + 별섬 강화 회수 표시기는 WA로 만든겁니다.



예제 6-2-2 : 필러스킬을 쓸까 말까

많은 딜러들이
1. 도트 혹은 버프 유지
2. 6~12초내외의 쿨이 있는 주력스킬 최대한 밀리지 않게 사용
3. 남는 시간동안 필러filler 스킬 사용
의 스킬 로테이션을 공유합니다.

사격냥꾼은 키메라 사격이 2번에 해당하는데요, 만약 글쿨이 끝나거나 이전 스킬 시전이 끝나는 시점에 키메라 쿨이 0.5초 정도가 남으면 기다릴까요, 하나 더 쓸까요?
판단은 각자가 하고

'지금 시점이 아니라, 글쿨이 끝나거나 지금 시전이 끝나는 시점 기준으로 키메라 쿨이 얼마나 남게될까?'
를 표시하는 기능을 구현하겠습니다.



조준 사격을 시전중이고 지금 키메라 쿨은 1초가 조금 넘게 남았습니다(스킬 단축창의 2는 아마 1~2초를 뜻할겁니다. 0.x초일 때 0으로 안띄우려고 올림할거거든요)
하지만 조사 시전이 끝나는 시각 기준으로는 키메라 쿨이 0.52초 남는군요.
이러면? 0.52면 좀 기니까 글레이브 투척을 씁니다. 0.1초 뭐 이러면 글투 스킵하고 키메라 썼을거고요.




쿨다운 표시 아이콘을 만들고 Always를 체크해서 아이콘이 쿨다운을 표시하되 계속 떠 있게 만듭니다.




지금 보니 '반대로'에도 체크했어야 좋습니다.
아무튼, 코드는 다음처럼 입력합니다.


function()
local GCDend
local castEnd = select(6, UnitCastingInfo('player'))

if castEnd then
GCDend = castEnd / 1000
else
local s, d = GetSpellCooldown(56641)
if s > 0 then
GCDend = s + d
else
GCDend = GetTime()
end
end

local S, D = GetSpellCooldown(53209)
local CDend = S + D

if CDend > GCDend then
return string.format("%.2f", CDend - GCDend)
end
end

코드 시작하자마자 local GCDend로 값 없는 변수를 생성합니다.
로컬 변수 얘기부터 하겠습니다.

변수를 그냥 지정하면 글로벌, 지금처럼 앞에 local을 붙이면 로컬 변수가 됩니다.
글로벌 변수는 Lua시스템이 종료될 때까지 계속해서, WA애드온 뿐 아니라 어떤 코드에서든 접근할 수 있습니다.
로컬 변수는 생성된 블럭block 안에서만 유효하며 블럭 밖에는 영향을 주지 않습니다.
밖에 같은 이름의 변수가 있어도 블럭 안에서만 이 변수가 호출되고 밖에서 같은 이름 변수를 부르면 원래의 변수값이 반환됩니다.
이 경우에는 function()   end 사이의 구문이 GCDend가 선언된 블럭입니다.

WA에 우리가 쓰는 코드는 함수 안에 적고 매 프레임으로 체크했으니 이 함수가 매 프레임마다 반복적으로 수행됩니다. 그런데 굳이 이번 수행할 때 생성한 정보를 다음 수행할 때도 쓰거나, 개인추가 조건에서 생성한 정보를 텍스트 코드에서도 알아야 할 필요가 없다면 로컬 변수를 쓰는 것이 원칙입니다.

그래서 여기에서도 로컬 변수를 쓸건데 실제 정보값을 변수에 기록하는 일은 if then end 안에서 할겁니다.
그렇다고 해서 선언까지도 구문 안에서 하면 if구조 자체도 블럭 안의 블럭이기 때문에 if문이 끝나면 변수값에 접근할 수가 없습니다.
그렇기 때문에 값을 넣지 않더라도 일단 선언은 if문 밖에서 하고 값은 if문 안에서 저장하는 것입니다.


local castEnd = select(6, UnitCastingInfo('player'))

name, subText, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo("unit")

주문 시전에 관한 정보입니다. 각각,
주문 이름, 주문 레벨(현재는 없고요), 시전바에 쓸 텍스트, 아이콘경로, 시작시각, 완료시각, 전문기술여부, 시전의 고유id(같은 주문을 연달아 시전할 때 구분할 목적입니다), 차단 불가능여부

여기에도 시간정보가 있는데 시전, 채널링의 시간 포맷은 오라, 쿨다운 등과 다릅니다.
시간 자체는 같은 GetTime()인데 단위가 초가 아니라 밀리초ms = 1/1000초 입니다.
따라서 저 시각은 GetTime() * 1000 과 1:1로 비교하게 됩니다.

어짜피 GetTime도 밀리초까지 표시돼서 굳이 이럴 필요가 있나 싶은데, 뭐 사정이 있겠지요.
아무튼 시전바는 버프보다 더 정교한 시간단위로 표시되며 GetTime과 비교하려면 1000으로 나누어야 합니다.

if castEnd then
시전중이 아니면 nil이라서 건너뜁니다. 이 부분은 '시전중이라면'입니다.
냥꾼 시전기술의 시전 시간은 고정 사격이라도 글쿨의 두 배(2초)라서 시전을 시작했다면 대체로 글쿨보다 시전 완료가 더 늦습니다. 그러니까 '글쿨이나 시전이 종료되는 시점'을 체크하려면 더 긴 쪽인 이걸 먼저 봅니다.

GCDend = castEnd/1000
GCDend가 '다음 시전이 가능해지는 시점'을 나타내는 변수입니다. 단위가 다르므로 1000으로 나누어서 GetTime()과 포맷을 맞춥니다.

else ~ end
앞서 있던 'if 조건에 해당하지 않으면' 이쪽이 수행됩니다.

시전중이 아니면 글쿨이 있나 봐야죠.
글쿨을 확인하려면 보통 '자체 쿨이 없지만 글쿨 영향은 받으며 차단되지 않는 스킬'의 쿨다운을 체크합니다.
즉시시전기를 쓰면 단축바의 모든 스킬이 쿨다운이 도는 것을 볼 수 있는데 이게 글쿨이고 고정 사격도 글쿨의 영향은 받아서 고정 사격 쿨다운을 추적하면 글쿨이 도는지 확인할 수 있습니다.

local s, d = GetSpellCooldown(56641)

start, duration, enable = GetSpellCooldown(index, "bookType") or GetSpellCooldown("name") or GetSpellCooldown(id)

쿨다운을 추적하는 함수는 '시작 시각, 지속 시간'을 반환합니다.
쿨다운 종료 시점은 시작 + 지속시간 이 되겠네요.

만약 쿨이 안돌고 지금 시전 가능하다면 0, 0이 나옵니다.
if s > 0 then
GCDend = s + d
else
GCDend = GetTime()
end

쿨이 도는 중이면 시작 + 지속시간이 종료 시각
아니라면 지금 바로 시전 가능한 상태니까 현재 시각 GetTime

local S, D = GetSpellCooldown(53209)
local CDend = S + D
요건 키메라 쿨 체크용


if CDend > GCDend then
return string.format("%.2f", CDend - GCDend)
end
'키메라 쿨다운 종료 시점이 글쿨 종료 시점보다 늦다면'
그 차이 시간을 반환합니다.

string.format은 ("형식", 값) 으로 씁니다.
입력한 값을 형식에 맞게 반환하는데요, %.2f 는 소수점 두 자리까지 표시하는 숫자 입니다.
만약 값 두 개를 이어서 쓰려면
string.format("%d%s", 38, "분")
이렇게 하면 앞의 %d는 정수 형태로 38, 뒤의 %s 자리에는 문자열 형태로 "분"이 들어가서 "38분"이라는 문자열이 됩니다.

스트링 라이브러리를 오늘 다뤄야 하는데 이거 준비하는데 시간이 걸릴거라 다음 기회에 다루고, 링크로 대체합니다.



==========================================================================

정리:

WoW API

현재 시각은 GetTIme(), time() 두 가지 포맷이 있습니다. 앞쪽 것이 보통 사용됩니다.
버프의 지속시간 정보는 '지속시간, 만료시각'
주문 시전의 지속시간 정보는 '시작시각, 종료시각'  다만 밀리초 단위이므로 1000으로 나누어야 GetTime()과 덧뺄셈 가능
쿨다운의 지속시간 정보는 '시작시각, 지속시간'
형태입니다.


Lua

로컬 변수는 그 변수가 선언된 블럭에서만 유효합니다.


밖에서 글로벌 변수를 선언하고
함수가 시작하면 function foo() ~ 맨 끝 end까지가 블럭을 이룹니다. 밖에서 선언한 변수는 안에서도 유효합니다.

do 구문 안에서 새로운 로컬 변수를 2로 지정하면 이것은 지정된 블럭 안에서만 유효합니다.

그 안의 if then 블럭 안에서 local a = 3 지정한 것은 또 새로운 로컬 변수이며 2로 지정된 변수에는 영향이 없습니다.
그래서 else문은 then이하와 같은 레벨의 독립된 블럭이므로 then 뒤에서 지정한 3은 영향이 없고 2로 지정한 값이 유효합니다. 이걸 4로 바꾸면 밖에서 선언한 값이 바뀌어 있습니다.

do-end 구문이 끝나면 2로 지정한 변수는 무효, 밖의 글로벌 변수값인 1입니다.

a = 5를 지정하면 글로벌 변수값을 바꿉니다.

다시 if문 안에서 로컬 지정하면 구문 끝나면 여전히 5이고요, 밖에 나와서 봐도 글로벌 값이 5로 변경된 상태입니다.


이런 구문 구조를 편히 보시기 위해 노트패드++을 추천합니다.

언어를 Lua로 선택하시면 Lua 문법에 맞게 function() - end, if then - else - end, while do - end 등의 구조가 어디부터 어디까지인지 표시해줍니다.

강좌 시작합니다.
오늘 내용은 논리연산, 블럭과 로컬 변수, 조건구조 입니다.
조건구조에 사용되기 때문에 논리연산 부분이 좀 내용이 깁니다.
(사실 좀 성실할 때 미리 써둔건데 지금은 게을러졌습니다)


Lua 3. 식 s

수의 계산, 논리 연산, 문자열 조작 등을 다룰 것입니다.


3.1 대수 연산자Arithmethic Operators

등호=가 이상하게 쓰이지만 이 외에는 매우 평범합니다. + - * / 의 가감승제 연산자가 모두 같고 부호를 지정하는 -도 흔히 쓰는 수학 기호와 같습니다.
거듭제곱을 뜻하는 ^도 쓸 수 있으나 제곱을 하시려면 x^2 대신 x*x를 권합니다.

3.2 관계 연산자Relational Operators

부등호와 등호입니다.

대소를 비교하는 부등호와
< > <= >=

같음, 다름을 비교하는 등호들이 있습니다. =는 지정할 때 쓰이고 같다는 의미로는 ==를 씁니다.
== ~=
뒤의 것은 ‘같지 않다’.

관계 연산자는 앞뒤로 값이 있고 그 사이에 들어가서 조건에 따라 true 또는 false를 반환합니다.

이렇게 됩니다.



> print(2>3)
false
> print(25 == 5*5)
true
> print(25 ~= 5*5)
false

부등호는 숫자 타입만 가능하나 ==와 ~=는 다른 값에도 쓸 수 있습니다.

> a = “2”
> print(a==3-1)
false
> print(a==”2”)
true

테이블은 조금 다릅니다.

> b = {3}
> c = b
> print(b==c, b=={3})
true false

테이블 쪽에서 다루었는데 테이블은 내용이 같다해서 같은 개체가 아닙니다. 모양새가 완전히 같은 서로 다른 개체지요.


3.3 논리 연산자Logical Operators

and or not
관계 연산자가 두 값 사이에 들어가는 것처럼 얘들도 두 논리값 사이에 들어가는데 boolean 타입이 아닌 값들이 논리값으로 쓰이면 반환이 좀 특이합니다.

자, 앞에서 boolean 타입이 아닌 값들도 boolean 연산 결과가 있고, 값이 nil과 false이면 그 결과는 false / 그 외 모든 값은 true라고 했습니다.



> print(true and true)
true
> print(true and false)
false
> print(true or false)
true

여기까지는 논리학을 체계적으로 배우기 전에 이해하는 연산법과 정확히 같아 자연스럽습니다.
and는 둘 다 참이어야 참, or는 적어도 하나가 참이면 참.

그런데 Lua에서 다루는 and, or의 Bool 연산은 앞 뒤 구분이 생길 수 있는 것이며 여기에 더해 반환이 단순히 참, 거짓이 아닙니다.

> print(4 and 5)
5
> print(4 and true)
true
> print(X and 5)
nil

A and B 연산은 앞쪽의 논리값이 true이면 뒤쪽 값을, true가 아니면 앞쪽 값을 내어놓습니다.
X는 지정한 바 없으니 nil이고요.

> print(4 or 5)
4
> print(false or 5)
5
> print(X or 5)
5

반대로 A or B 연산은 앞쪽의 논리값이 true이면 앞쪽 값을, true가 아니면 뒤쪽 값을 내어놓고요.

처음 true false로 한 연산들도 다시 보면 이 규칙을 정확히 따르고 있습니다.
다시 정리하면
'A and B' : A이면 - B(bool타입이든 아니든 B의 값) // A가 아니면 - A(false 또는 nil)
 'A or B' : A이면 - A(bool타입이든 아니든 A의 값) // A가 아니면 - B


not은 뻔하죠.
> print(not true, not false)
false true

A and B or C라는, 자주 써먹는 연산법이 있습니다.
C언어에서 쓰는 A ? B : C와 같습니다.
의미는 “A면 B, 아니면 C” 즉, “A가 참이면 B를 반환, 아니라면 C를 반환”
A and B에서 A가 false이면 A가 나오는 데에 반해 여기에서는 C로 대체됩니다.

> print(4 and 5 or 6)
5
> print(X and 5 or 6)
6



3.4 문자열 축약Concatenation

마침표 두 개 .. 연산자는 앞뒤의 문자열을 하나로 잇습니다.



> a, b, c = “L”, “ove”, “ive”
> print(a..b)
Love
> print(a .. c)
Live
> print(a..b..a..c)
LoveLive

문자열이 숫자로만 이루어지면 즉시 대수 연산에 사용될 수 있는 것처럼( “11” - 1 == 10) 숫자값 또한 문자열처럼 축약해서 문자열을 만듭니다.
다만 소수점과 구분하기 위해 숫자 뒤에는 ..을 붙여쓰면 안됩니다.

> print(11 ..a)
11L


3.5 우선순위Precedence

사칙연산이 
곱셈 = 나눗셈 / 덧셈 = 뺄셈 의 순서가 있듯이 Lua의 연산자들은 다음 우선순위가 있습니다.
^
not -(부호로 쓰일 때)
* /
+ -
..
< > <= >= ~= ==
and
or

and와 or는 우선순위가 있습니다.

물론 괄호로 묶으면 그 안에서부터 연산합니다.


Lua 4. 구문 Statement

4.1 지정Assignment

앞서 다루었습니다.


4.2 로컬 변수와 블럭Local Variables and Blocks

로컬 변수local variables를 이해하려면 우선 블럭block에 대해 알아야 합니다.
블럭은 제어문control structure, 함수, chunk 등등의 내부 구역을 뜻하며 로컬 변수가 그 안에서 생성되면 그 변수는 자신이 속한 블럭 안에서만 유효합니다.
로컬 변수는 local a와 같이 선언하거나 한 번에 local a = 10 등으로 지정하며 생성할 수 있습니다.



> local a = 10
> print(a)
nil

로컬 변수를 지정했습니다. 그런데 Lua standalone interpreter의 맨 바깥쪽 구역(어떤 블럭에도 속하지 않는 처음 시작하는 곳)에서 로컬 변수를 지정하며 생성하면 그 자체가 하나의 chunk를 이루며 해당 변수는 그 줄에서만 의미가 있습니다.
맨 밖에서 하시려면
local a
a = 10
으로 선언한 뒤 지정하셔야 합니다.

> do
>> local a = 10
>> print(a)
>> end
10
> print(a)
nil

do - end는 그냥 사이에 있는 코드를 수행하라는 구조이고 그냥 코드를 쓴 것과 기능적으로 다른 점은 end를 입력하는 순간 한 번에 실행된다는 의미 밖에 없습니다.
그러나 do - end는 블럭을 구성하기 때문에 안에서 로컬 변수를 지정할 수 있고 밖에서는 해당 변수가 존재하지 않습니다.
안에서 생성한 로컬 변수가 블럭 안의 print로는 10을 출력하나 밖에서는 존재하지 않음을 볼 수 있습니다.

> a = 10
> do
>> local a = 20
>> print(a)
>> end
20
> print(a)
10

로컬 변수가 블럭 밖에 영향을 주지 않는다는 것은 블럭 밖에서 같은 변수명을 가진 다른 변수가 생성되어 있을 때도 마찬가지입니다. 밖에서 a에 10을 지정하고 블럭 안에서 로컬 변수 a에 20을 지정해도 밖의 a는 여전히 10입니다.
다만 같은 이름을 사용했다면 블럭 안에서는 밖의 변수에 접근할 방법이 없습니다.
반대로 블럭 밖에서 지정된 변수는 블럭 안에서도 유효한데, 만약 블럭 안에서 로컬 변수 지정 전에 print(a)를 했다면10이 출력되었을 것입니다.


4.3 제어 구조Control Structures

매 줄마다 위에서 아래로 진행되던 코드의 흐름을 필요에 따라 뛰어넘거나 분기시키거나 반복하는 구조를 제어문이라고 합니다.
제어구조는 각각이 블럭을 이루며
if 조건 then  -  (else)  -  (elseif)  -  end : 조건 판단
while 조건 do  -  end  : 반복
repeat  -  until  :  반복
for 조건 do  -  end  : 반복
등이 있습니다.

앞의 내용에 있듯 조건 판단 구문에서는 false와 nil이 아닌 모든 값을 true로 간주합니다.

4.3.1 if then else
if 가 뒤에 오는 조건을 판단하고 조건에 해당하면 then 구문을, 아니면 else 구문을 실행합니다.
else 구문이 없이 then 뒤에 end로 끝날 수도 있습니다. 이 때는 조건이 false라면 아무 일도 하지 않습니다.




> function Abs(x)
>> if x < 0 then
>> x = - x
>> end
>> return x
>> end
> print(Abs(3.14))
3.14
> print(Abs(-273.15))
273.15
> function kilomega(x)
>> if x < 1000 then
>> return x
>> elseif x < 1000000 then
>> return math.floor(x/1000)..”k”
>> else
>> return math.floor(x/1000000)..”m”
>> end
>> end
> print(kilomega(365))
365
> print(kilomega(86400))
86k
> print(kilomega(299792458))
299m


k, m 단위로 숫자를 바꾸는 함수입니다.
1,000보다 작으면 그대로
(1,000보다 작지 않은 것 중에) 1,000,000보다 작으면 k단위로
그외에는(1,000,000이상이면) m 단위로
출력합니다.

if then 구문 뒤에 else가 붙으면 if 테스트에서 걸러진 나머지에 대해서만 수행됩니다.
특히 elseif는 그 상태에서 다시 if 테스트를 수행하여 if문을 새로 연 다음 end를 한 번 더 쓰는 수고를 줄여줍니다.