[초보자들] S69 5번째 – 판단연산자, 논리연산자

벌써 5번째 스터디가 진행되었습니다. 이제 다음이면 마지막이라니 시간이 참 빠르네요.
오늘 스터디는 지난번에 이어 흐름을 제어하는 방법에 대해서 계속 살펴보았는데요, 판단문 이외에도 흐름을 제어할 수 있는 판단 연산자(=3항연산자)와 논리 연산자(&&, ||)를 배움으로써 판단문 부분을 마무리했습니다. 스터디 끝부분에는 상태 프로그래밍과 다음 시간에 본격적으로 배우게 될 Loop(반복문)에 대해 맛보기로 배웠는데요, 오늘 후기는 전자를 중심적으로 살펴보고자 합니다.

제어의 또 다른 방법 : 흐름 지연

컴퓨터가 작동하는 순차적인 흐름을 흐름(Flow)이라고 합니다. 위에서 아래로, 왼쪽에서 오른쪽으로 코드가 하나도 빠지지 않고 차근차근 진행되는 것을 말하죠.

순차적인 흐름을 바꿀 때 제어문(control statement)을 쓰는데 제어문에는 지난 시간에 배운 if문과 switch문과 같은 판단문(conditional statement)이 있었습니다.

그런데 놀라운 사실! 판단문말고도 제어하는 다른 방법이 또 있습니다.

첫번째는 ‘함수’입니다. 함수는 특정한 기능을 수행하는 코드들을 묶어두고 필요하면 언제든지 불러서 쓸 수 있도록 만들어 놓은 것과 같습니다. 함수를 쓰려면 미리 만들어 놔야 하는데(선언), 먼저 함수를 선언해 둔 후 한참 코드를 진행하다가 함수를 쓰려고 부르면(호출) 위에 선언해둔 내용으로 함수의 내용이 실행됩니다. 아래로 흐르고 있었던 흐름이 점프해서 순차적인 흐름을 바뀌게 되었습니다.

흐름을 바꾸는 또 다른 방법은 연산자를 이용하는 방법입니다. 연산자 중에서도 지연을 일으키는 연산자가 있는데, 이 연산자를 이용하면 순차적으로 진행되던 흐름이 잠시 멈춥니다. 멈춘 사이에 일정한 판단을 한 후 특정한 곳으로 이동한다든지, 코드를 건너뛴다든지 등으로 흐름을 변경할 수 있습니다. 대표적인 예로는 우선순위를 강제하는 괄호 연산자()가 있습니다.

예로 살펴보겠습니다.

var result = 3 * (2 + 5);

원래의 순차 흐름이라면 왼쪽부터 차근차근 실행되므로 3 * 2가 먼저 실행되어야 합니다. 하지만 괄호 연산자로 인해 순차 흐름이 중단되고 괄호 안의 내용인 2 + 5 의 연산이 먼저 일어나게 됩니다. 그리고 다시 원래의 흐름으로 돌아와 3 * 7이 순차적으로 진행됩니다.

수학에서 쓰는 () 괄호와 생김새와 내용이 비슷해서 흐름과 상관없이 ‘먼저 계산하는 것’을 너무 당연하게 여겼지만, 컴퓨터 입장에서는 그렇지 않았네요^^

이처럼 지연을 일으켜서 흐름을 제어하는 연산자에는 괄호연산자 외에도 판단 연산자, 논리 연산자 등이 있습니다. 각각의 연산자를 좀 더 자세하게 살펴보겠습니다.

판단 연산자 ? :

판단 연산자는 3항 연산자라고도 합니다. (연산자를 기능적으로 구분했을 때에는 판단 연산자, 항의 개수로 분류할 때는 3항 연산자로 분류됩니다.)

형식은 다음과 같습니다.

식1 ? 식2 : 식3; 

위 식은 다음과 같은 뜻을 가지고 있습니다.

식1이 거짓이 아니면 식2, 아니면 식3

~이면 a, 아니면 b… 왠지 익숙한 것 같은데… 이전에 배웠던 if-else문이 생각나네요. if-else문과 같은 경우 식이 참이면 if 판단식 이후의 문이, 아니면 else 뒤에 해당하는 문이 실행되었습니다. 이처럼 판단 연산자로 쓴 문장은 if-else문으로 바꿀 수 있고, 그 반대도 가능합니다.

저는 처음에 판단 연산식을 접했을 때에는 형식이 너무 낯설어서 판단 연산식이 눈에 잘 안들어왔습니다. 그래서 처음에는 if문으로만 계속 썼던 기억이…^^;; 익숙해지는데 꽤 많은 시간이 걸렸네요.

그렇다면 (불편함을 제외하고) 어떤 때 판단 연산자를 쓰고 어떤 때 if-else문을 쓸까요?

판단 연산자 vs if-else문

우선 형식적으로 판단 연산자와 if-else문의 차이를 살펴보겠습니다.

//판단 연산자
식1 ? 식2 : 식3;

//if-else문
if (식) 문1
else 문2

판단 연산자는 각각의 자리에 ‘식’만 들어갑니다. 하지만 if-else문은 판단 부분에만 ‘식’이, 나머지에는 ‘문’이 들어갑니다. 따라서 if-else문의 결과는 문이 실행되므로 바로 할당이 불가능하지만, 판단 연산자는 연산의 결과로 값이 나오기 때문에 바로 할당이 가능합니다.

//기온에 따라 폭염주의보인지(33도 이상) 아닌지 판단
var alarm, temp;

//if-else문으로 표현
if(temp >= 33) {
  alarm = true;
}else{
  alarm = false;
}

위의 if-else문은 판단을 한 후 ‘alarm=’이란 문을 통해 변수에 true/false값을 할당 합니다. 판단 연산식을 이용하면 판단부터 할당까지 1줄로 할 수 있습니다.

//판단 연산식으로 표현 (판단부터 할당까지 1줄로 가능)
var alarm = temp >= 33 ?  true : false;

판단 연산식으로 쓰니 중복(‘alarm=’)부분도 많이 제거되고 코드도 한결 간결해진 것 같습니다.

위처럼 상황에 따라 다른 값을 할당해야한다면 if-else문 대신 판단 연산식을 쓰는 편이 좋다고 합니다. 하지만 가끔 판단 후 복잡한 연산을 처리한다든지, 여러가지 문을 실행해야할 때가 있습니다. 그 때에는 if-else문을 이용하는 것이 유리할 수도 있습니다. 이처럼 판단 연산자와 if-else문 중에 어떤 것이 절대적으로 좋으냐를 따질 순 없습니다. 다만 상황에 따라 중복이 적으며, 내가 계획했던 대로 결과가 나오고, 의도를 잘 드러낼 수 있는 방법을 잘 선택할 수 있는 것이 중요한 것 같습니다.

논리 연산자 &&, ||

연산 지연을 일으키는 다른 연산자로는 &&, || 연산자가 있습니다. &&, ||은 논리 연산자(Logical Operators)로 좌항과 우항을 논리 연산한 값을 반환해줍니다. 비교논리 연산자에도 여러가지 다양한 종류(>>여기서 확인 가능)가 있지만, 스터디에서는 &&연산자와 ||연산자에 대해 좀 더 깊이 살펴봤습니다.

형식

a && b // and 연산자
a || b // or연산자
a && b || c //여러개 연속해서 쓸 수도 있음. 이 경우 연산은 왼쪽에서 오른쪽으로 차근차근 진행

진짜 뜻

보통 &&연산자는 두 항이 모두 참이어야 참, || 연산자는 둘 중 하나면 참이라고들 설명합니다. 따라서 각 연산자마다 항의 참/거짓 여부에 따라 나올 수 있는 결과의 경우는 4가지씩 됩니다.

&&연산자 결과의 경우의 수는 다음과 같습니다.

자세히 보면 첫번째 항이 참인 경우에는 뒤의 두번째 항이 결과가 되고, 첫번째 항이 거짓이면 첫번째 항의 값이 됩니다. 즉, &&연산자의 결과는 첫번째 항이 거짓이면 첫번째 항, 아니면 두번째 항이라는 것을 알 수 있습니다.

이 원리를 이용하면 두 항을 모두 보지 않고 첫번째 항의 값만 보고도 결과를 판단할 수 있습니다. 예로 살펴보겠습니다.

0 && 'aaa'; //0

위 예시에서 첫번째 항인 0은 JavaScript에서 거짓으로 처리가 되어 거짓이 됩니다. 따라서 뒤의 항 ‘aaa’를 갈 필요도 없이 결과는 0이 됩니다.

'aaa' && 'bbb'; //"bbb"
'aaa' && 0; //0

첫번째 항이 참인 경우에는 두번째 항이 참이냐 거짓이냐는 결과에 영향을 미치지 않습니다. 따라서 ‘aaa’ && ‘bbb’ 처럼 두번째 항이 참인 경우도, ‘aaa’ && 0 처럼 두번째 항이 거짓인 경우도 첫번째 항인 ‘aaa’가 모두 참이므로 모두 결과는 두번째 항이 됩니다.

즉, &&연산자를 만났을 때 flow는 이렇게 된다고 볼 수 있을 것 같습니다.

 

한편, || 연산자의 결과는 다음과 같은 경우의 수를 갖습니다.

|| 연산자의 결과는  첫번째 항이 참이면 무조건 참이고 아니면 두번째 항이 됩니다.

'aaa' || 'bbb'; // "aaa"

위의 예에서 첫번째 항인  ‘aaa’는 참으로 취급되므로 || 연산자는 다음항인 ‘bbb’를 볼 필요도 없이 바로 ‘aaa’를 결과로 갖습니다.

하지만 첫번째 항이 거짓인 경우에는 두번째 항이 결과가 됩니다. 이 때 &&연산자와 마찬가지로 두번째 항이 참이냐 거짓이냐는 상관이 없습니다.

0 || 'aaa'; // 'aaa'
0 || false; // false

결론적으로, || 연산자를 만났을 때 flow는 이렇게 된다고 볼 수 있을 것 같습니다.

결론적으로 정리하자면, &&와 ||의 진짜 뜻은 다음과 같다고 이해할 수 있을 것 같습니다.

    a,b를 논리연산하면 둘 중 하나를 결과로 갖는데…

  • a && b : a가 거짓이면 a, 아니면 b
  • a || b : a가 참이면 a, 아니면 b

&& 연산자, || 연산자의 실제적인 사용

그렇다면 &&, || 연산자는 언제 쓸 수 있을까요?
우선 각종 식에서 참/거짓을 판단하는데 많이 쓰입니다.

var weather, temp;
// 비가 안오고 기온도 15이상 되야지 피크닉을 감.
if(weather !== 'rainy' && temp >= 15) {
  goPicnic();
}

// 비가 안오거나 기온이 15도 이상 둘중에 하나만 되면 축구하러 감.
if(weather !== 'rainy' || temp >= 15){
  playSoccer();
} 

특별히 || 연산자는 초기 환경에서 기본 값을 설정하는데 사용하기도 합니다.

예를 들어… 어떤 사람이 헬스장에 처음 가입했습니다. 그런데 처음 가입해서 어떤 운동을 해야할 지를 몰라서 근처에 있는 트레이너에게 물어봤습니다. 트레이너는 차근차근 잘 설명해줍니다. 그런데 이런 사람이 너무 많습니다! 그래서 트레이너는 모든 사람에게 운동 계획을 짜주는데, 새로 가입한 사람이나 기존에 모두 운동 스케줄을 가지고 있지 않은 사람에게는 ‘기본 계획(basic Plan)’을 기본으로 등록해주기로 했습니다.

이런 상황은 코드로 다음과 같이 나타 낼 수 있습니다

var myPlan; //나의 운동 계획
var basicPlan = '스트레칭, 러닝머신 15분, 근육운동 15분, 정리 스트레칭'; //기본적인 운동 계획

myPlan = myPlan || basicPlan; // '스트레칭, 러닝머신 15분, 근육운동 15분, 정리 스트레칭' 

||연산자는 첫번째 항이 거짓이면 두번째 항이 결과로 된다고 했습니다.
따라서 신입 회원인 경우에 myPlan이 없으므로 거짓으로 취급되어 basicPlan이 myPlan에 할당됩니다.

반면, 기존에 myPlan이 있었던 회원은 첫 항인 myPlan이 참이 되므로 원래 있었던 myPlan이 유지됩니다.

//기존에 운동 계획이 있었던 사람의 경우
myPlan = '스트레칭, 유산소 워밍업 10분, 상체 근육 운동 10분, 하체 근육 운동 10분, 복근운동 10분, 정리 스트레칭';
myPlan = myPlan || basicPlan; //  '스트레칭, 유산소 워밍업 10분, 상체 근육 운동 10분, 하체 근육 운동 10분, 복근운동 10분, 정리 스트레칭'

이는 위에서 배운 판단 연산식으로 바꿔 표현할 수도 있습니다.

myPlan = myPlan ? myPlan : basicPlan;

판단 연산식의 뜻은 식 1이 참이면 식2, 아니면 식 3이었습니다. 따라서 위의 코드는 myPlan이 참이면 myPlan의 값을 가지고, 아니면 basicPlan을 myPlan값에 할당한다는 뜻을 나타냅니다. 똑같은 코드이지만 위에 || 연산식을 이용했던 것에 비해 myPlan이 좀 더 많이 나오는 느낌이네요^^;;

또 if-else문을 이용해 다음과 같이 나타낼 수도 있습니다.

if(myPlan){
  myPlan = myPlan;
}else{
  myPlan = basicPlan;
}

그런데 위의 표현은 다르게 표현하자면  ‘myPlan이 거짓이면 myPlan에 basicPlan을 넣는다’라고 할 수 있습니다. 따라서 다음과 같이 바꿔서 표현 할 수도 있습니다.

if(!myPlan) myPlan= basicPlan;

위 코드는 myPlan이 참인 경우(존재하는 경우) myPlan을 또 myPlan에 할당했던 불필요한 과정이 없어지고 myPlan 진짜 필요할 때에만 할당이 이뤄집니다. 불필요한 부분이 사라졌으니까 좀 더 나은 표현이겠죠…?

이렇게 같은 기능을 구현했을지라도 if문, || 연산자, 판단 연산식 등을 이용해 다양하게, 그리고 다르게 표현할 수 있음을 알 수 있었습니다.

왜 이렇게 다양할까?

그렇다면 궁금해지는게… 왜 이렇게 다양한 표현들이 있는 것 일까요?
사실 처음에 저는 ‘왜?’라는 질문을 떠올릴 수 조차 없었습니다. 다양한 옵션을 배우기에도 급급했으니까요^^;;; 그런데 지금 생각해보니 좀 더 나은 프로그래밍을 할 수 있도록 도와주는 다양한 도구들을 준비해둔게 아닌가 싶습니다. 즉, 기획한 대로 오류가 없이 잘 작동하고 좀 더 쉽게 유지 보수를 할 수 있는 프로그램을 할 수 있도록 하는 것이죠.

그럼 이제 툴이 무엇이 있는지 알게 되었으니 툴을 잘 써야할텐데… 툴 쓰는 법은 어떻게 연습할까요?

다른 툴을 익히는 과정을 생각해봅니다. 예를 들어 망치로 못을 잘 박게 되는 법을 생각해보면 우선 망치가 어떤 구조로 이뤄졌고 어떤 원리로 작동하는지 공부를 할 것입니다. 그리고 못의 종류에 따라 다양한 것에 못질을 연습하겠죠. 물론 처음엔 못질보단 헛질이 많겠고^^;; 다치기도 하겠지만 계속 연습하다보면 못질을 잘 하게 되지 않을까요?

코드도 비슷할 것 같습니다. 다양한 툴(if문, switch 문 등..)을 사용해 제어문을 만들어보고 또 만들어본 제어문을 더 나은 방식으로 만들 순 없을지 연습해보면서 실력을 키워나가는 것이죠.

실제로 스터디 시간에는 그런 연습이 계속 되었습니다. 특히 교재 109페이지의 연습문제 4번의 문제를 풀고 개선했던 것이 인상적이었습니다.

문제 4번은 [문제 예시]를 switch문으로 수정하는 것이었습니다.

//[문제 예시]
//학년 변수를 생성합니다.
let level = 1;

//출력합니다.
if(level == 1){
  console.log('수강해야 하는 전공학점 : 12학점');
}else if(level == 2){
  console.log('수강해야 하는 전공학점 : 18학점');
}else if(level == 3){
  console.log('수강해야 하는 전공학점 : 10학점');
}else if(level == 4){
  console.log('수강해야 하는 전공학점 : 18학점');
}

//--------switch로 변경!---------
switch(level){
  case 1: console.log('수강해야 하는 전공학점 : 12학점'); break;
  case 2: console.log('수강해야 하는 전공학점 : 18학점');break;
  case 3: console.log('수강해야 하는 전공학점 : 10학점');break;
  case 4: console.log('수강해야 하는 전공학점 : 18학점');break;
}

교재에서는 문제를 switch문으로 바꾸라고 했으니까 저기까지만 하고 끝! 할 수도 있었습니다. 하지만 그냥 넘어가지 않고 좀 더 나은 코드로 고쳐보았습니다.

더 나은 코드로 고칠 수 있는 가장 쉬운(?) 방법은 중복을 없애는 것입니다.

위 코드를 보면 console.log( 부터 수강해야하는 전공학점 : , 학점)이 반복된다는 것을 알 수 있습니다. 그래서 반복되는 부분은 msg라는 변수에 넣어두고, 케이스에 따라 변하는 학점의 숫자 값을 msg에 합쳐 출력하도록 합니다.

let msg = '수강해야하는 전공학점 : ';
switch(level){
  case 1: msg += '12'; break;
  case 2: msg += '18'; break;
  case 3: msg += '10'; break;
  case 4: msg += '18'; break;
}
console.log(msg + '학점');

위 코드에서는 이제 메시지 부분을 변경하더라도 switch문의 case에 담겨있는 메시지를 하나하나 수정할 필요가 없고, msg 변수의 내용 만 바꿔주면 됩니다. 중복도 줄어들고 유지/보수가 좀 더 쉬워졌네요.

한편, 저 코드를 자세히 보면 level 2와 4의 학점값이 18로 동일하다는 것을 알 수 있습니다. 무슨 의미가 있는 걸까요..? 사실 교재에서는 따로 의미 부여를 하지 않았지만, level 2와 level 4가 특수반의 레벨을 의미하는 것이었다면 저 코드에서는 그 의도가 잘 나타나지 않습니다. 따로 메모를 해놓지 않고서는 잊어버릴 것이고, 유지 보수도 그것을 잊어버린 채 하게 될 것 입니다.

이 때, 주석을 이용하면 level 2와 4가 무슨 의미를 가지는지 나타낼 수 있습니다.

let score;
switch(level){
  case 1: score = 12; break;
  case 3: score = 10; break;
  //레벨 2, 4는 특별반의 레벨임 
  case 2: score = 18; break;
  case 4: score = 18; break;
}
console.log('수강해야하는 전공 학점 : ' + score + '학점');

이처럼 프로그램은 계속 개선될 수 있습니다..! 기존에 저는 한번 작성한 코드는 끝!이라며 다시 보지 않았는데, 이를 반성하며 기존의 코드를 더 개선해보고 다양한 표현으로 바꿔보는 연습을 많이 해봐야겠다고 생각했습니다.

Conditional Programming

지금까지 스터디를 해보며 그동안 쉽게 보았던 판단문이 굉장히 어렵다는 것을 깨달았습니다. 그렇다면 왜이렇게 어려운가!?!?!?

판단문은 상태를 판단해서 어떤 일들을 하는데, 그 상태를 추적하기가 어렵기 때문이죠.

여기서 ‘상태’는 판단문(Conditional statement)할 때의 condition으로 ‘메모리의 상태’를 말합니다. 판단문은 판단의 기준으로 무조건 ‘메모리에 있는 값, 상태’로 판단을 합니다. 그런데 메모리의 상태는 시시각각으로 변합니다. 즉, 코드 21번째 줄에서의 메모리 상태와 22번의 상태가 완전히 다를 수 있다는 것입니다.

이처럼 상태에는 ‘시점’이 함께 들어있습니다. 프로그램에서는 코드 한줄 한줄, 흐름의 한줄 한줄이 시점인데, 이 시점마다 메모리의 상태는 변화무쌍하게 바뀌고 우리는 이것을 추적하기가 힘들어서 판단문을 생각대로 쓰기가 어려운 것이죠.

흐름을 잘 읽고, 제어를 잘 해나가기 위해서는 위에서 말한 것 처럼 계속 써보면서 익숙해지는 수밖에 없는 것 같습니다. ^^

이렇게 보니 참 영어를 공부하던게 생각나네요. 처음에는 1형식, 2형식, 3형식 등을 예제에서 쉬운 문장으로 시작했다가 점점 복잡하고 긴 문장 읽기를 연습하다보면 처음엔 외계어로 보이던 문장도 영어로 보이고^^ 이해할 수 있던 기억들이요..! 프로그래밍도 비슷하지 않을까?하는 희망을 갖습니다.

결론

스터디 마지막에는 20분 정도는 반복문(Loop)에 대해 배웠습니다. 이는 다음 스터디에서도 이어서 다룰 예정이므로 다음 후기에 함께 다루도록 하겠습니다.

어쨌든… ! 드디어 제어문 중 판단 부분에 대한 내용이 마무리 되었습니다. 1+1=2와 같이 매우 간단하고 쉽다고 생각했지만, 매 스터디마다 그 내용의 깊이와 어려움에 놀랐던 판단문! 무엇보다도 쉽게 보았던 if, switch문이 사실은 흐름제어 즉, 프로그래밍의 끝판왕이었다는 사실을 되새기며…. 다시는 if님과 switch문님 등을 무시하지 않고 꾸준히 수련해나가야겠다고 다짐했습니다.

 


동영상 강의 보기


summer| bsidesoft 신입사원
디자인을 공부했던 섬머는 개발까지 해버리겠다는 욕심으로 개발자의 세계에 입문하게 되었습니다. 개발왕이 되어 멋진 제품을 만들어내는 꿈을 꾸고 있습니다. 코드, 디자인을 포함한 세상의 모든 아름다운 것들과 미드, 그리고 달리기를 좋아합니다.

%d 블로거가 이것을 좋아합니다: