avatar
Published on

자바스크립트 3항연산자에 대한 고찰

Author
  • avatar
    Name
    yceffort

Introduction

코딩 하는 시간을 100이라고 가정한다면, 요즘 80은 자바스크립트를, 나머지 10씩을 각각 러스트와 파이썬을 하는데 쓰고 있다. 자바스크립트에 삼항연산자가 있고, 물론 러스트와 파이썬에도 삼항연산자가 있다.

const protocol = isSecure ? 'https' : 'http'
protocol = 'https' if isSecure else 'http'
let protocol = if isSecure ? { 'https' } else { 'http' };

3항 연산자를 쓰면서 느끼는 건, 이게 언어마다 서순이 조금씩 미묘해서 헷갈린다는 점이다. 자바스크립트와 파이썬이 특히 그렇다. 자바스크립트에 젖어서 그런지 파이썬은 코딩을 할 때 마다 헷갈리는 것 같다.

아무튼 지간에, 우리가 대부분 3항연산자를 쓰는 이유는 간결한 코드의 작성을 위해서다. 하지만 우리는 간결함과 명확성을 선택해야할 때가 있고, 그 둘을 모두 잡을 수 없을 때도 존재한다. 그리고 이 두가지는 모두 서로의 주장이 팽팽하게 맞선다. 코드를 적게 씀으로써 내 코드의 크기를 줄이고, 버그의 위험성을 줄일 것이냐, 혹은 명확하고 읽기 쉽게 써서 유지보수와 수정이 더 편하게 할것인가?

이러한 논쟁의 한가운데에 있는것이 바로 3항연산자 인 것 같다. 3항 연산자를 자칫 잘못쓰게 되면 코드를 이해할 수 없는 난장판으로 만들어버리곤 한다. 그렇지만, 3항 연산자는 보통 if-else와 동일한 작업을 할 수 있지만 더 간결하다는 이유로 많이 쓰인다.

하지만 만약에 그 둘이 동일하지 않고 무언가 숨은 차이점이 있다면 어떨까? 둘은 완전히 동일한 것 같지만 그렇지는 않다. 여기에는 사람들이 자주 놓치는 중요한 차이점이 있다. 그리고 이 차이는 코드에 영향을 미칠 수 있다.

3항 연산자의 문제점

보기에 조금은 이상한 3항 연산자

3항 연산자는, 그 이름인 '3항' 에서 알 수 있듯 세가지 다른 표현으로 동작한다. 이 세가지 표현은 ? :으로 나뉘어져 있다.

(/* First expression*/) ? (/* Second expression */) : (/* Third expression */)
const protocol = isSecure ? 'https' : 'http'

첫번째 식이 참이면 두번째 식으로, 그렇지 않다면 세번째 식의 값이 나오게 된다. 그리고 이 연산자는 앞서 언급했듯 두가지 기호로 구분되어 있다. 3항 연산자외에 다른 연산자들은 이렇게 여러개의 기호로 구성되어 있지 않다. 이상한 것은 그것 뿐 만 아니다. 대부분의 바이너리 연산자는 일관적인 타입을 가지고 있다. 산술연산자는 숫자 타입으로만 동작하고, boolean 연산자는 boolean 에 대해서만 동작한다. 비트와이저는, 숫자타입에서만 동작한다. 그러나 삼항연산자는 어떠한 타입으로든 동작할 수 있다. 두번째와 세번째 식에서 어떤 타입이든 들어올 수 있다. 하지만 첫번째 식은 boolean으로 표현되어야 한다. 타입에 대해 이렇게 일관적이지 않다는 점은 확실히 이상하다.

초보자에게 별로 도움이 안되는 3항 연산자

3항 연산자는 특이하게 생겼다. 만약 나와 반대로 파이썬을 개발하다가 자바스크립트를 개발하러 온 사람이 있다면, 3항연산자는 항상 익숙하지 않은 존재일 것이다. 기억해야할 것이 많다. ?도 찾아야 하고 :도 찾아야 한다. if-else와 달리 삼항연산자를 수도 코드로 읽는 것은 어렵다.

if (someCondition) {
  takeAction()
} else {
  someOtherAction()
}

이 식을 읽는 것은 그리 어려운 일이 아니다. someCondition이 true면 takeAction을, 그렇지 않으면 someOtherAction을 실행할 것이다. 이는 크게 어려운 일이 아니다. 그러나 3항연산자는 기호와 구성되어 있기 때문에 위 코드처럼 쉽게 읽히지 않는다. 언어를 읽는 자연스러운 과정과 확실히 대비된다.

가독성이 떨어진다.

초보자가 아니더라도, 3항 연산자는 종종 읽기 어려운 것이 사실이다. 특히, 이런 가독성 문제는 삼항연산자가 길어질 때 더욱 문제를 만든다.

const ten = Ratio.fromPair(10, 1);
const maxYVal = Ratio.fromNumber(Math.max(...yValues));
const minYVal = Ratio.fromNumber(Math.min(...yValues));
const yAxisRange = (!maxYVal.minus(minYVal).isZero()) ? ten.pow(maxYVal.minus(minYVal).floorLog10()) : ten.pow(maxYVal.plus(maxYVal.isZero() ? Ratio.one : maxYVal).floorLog10());

저 3항연산자 내부에서 정확히 무슨일이 일어나고 있는지 한번에 이해할 수 있는 사람은 별로 없다. 각 표현식 내부의 코드도 복잡하고, 3항 연산자도 내부에 중첩이 되어서 더욱 어렵다.

물론, prettier를 사용하면 조금더 상황이 나아질 수는 있다.

const ten = Ratio.fromPair(10, 1)
const maxYVal = Ratio.fromNumber(Math.max(...yValues))
const minYVal = Ratio.fromNumber(Math.min(...yValues))
const yAxisRange = !maxYVal.minus(minYVal).isZero()
  ? ten.pow(maxYVal.minus(minYVal).floorLog10())
  : ten.pow(maxYVal.plus(maxYVal.isZero() ? Ratio.one : maxYVal).floorLog10())

그럼에도 여전히 읽기 어렵다는 사실엔 변함이 없다.

물론, 실제 이런식으로 3항연산자를 복잡하게 쓰는 사람은 별로 없을 것이다. (코드 리뷰어가 잔소리를 한두마디 하겠지) 하지만 우리가 기억해야할 포인트는 여전하다.

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler

우리는 컴퓨터가 아닌 사람이 읽기 좋은 코드를 짜야하는 책임을 가지고 있다. 그리고 이것이 3항 연산자가 가지는 가장 큰 문제라 생각한다. 일단 작성자가 코드를 우겨 넣는 것은 쉽다. 그러나 그렇게 코드를 우겨넣기 시작하면, 엉망진창이 될 가능성이 기하급수적으로 커진다. 특히 주니어 프로그래머라면, 3항연산자보다는 if-else를 쓰라고 이야기 하고 싶다.

if-else의 신뢰성 문제

그래, 3항연산자는 그렇다 치자. if-else는 완벽한가? 그럼에도 3항연산자를 쓰는 이유는 간결하거나 조금더 영리하게 코드를 작성하기 위함이다. 아래 예시를 보자.

let result;
if (someCondition) {
    result = calculationA();
} else {
    result = calculationB();
}`
const result = someCondition ? calculationA() : calculationB()

사람들은 일반적으로 이 두 예제가 동일하다고 생각한다. 두 예제 모두 result라는 변수안에 조건에 따라 두 함수의 리턴 값을 넣어준다. 하지만 다른 측면으로 보면, 조금은 다르다. 먼저 let을 살펴보면 알수 있다. 두 차이점은, if-else는 문(statement)이지만, 3항연산자는 표현식(expression) 이라는데에 있다. 그래서 그 차이점이 뭔데?

  • 표현식 (expression)은 항상 어떠한 값을 계산한다.
  • 문(statement)는 하나의 실행 단위로 존재한다.

이는 중요한 개념이다. 표현식은 값을 계산하지만, 문은 그렇지 않다. statement의 결과값을 어떤 변수에 할당할 수 없다. statement의 결과를 함수의 인수로 넘겨줄 수 없다. 그리고 if-else는 statement로, 어떠한 값을 resolve하고 있지 않다. 따라서 이를 사용할 수 있는 가장 좋은 케이스는 부수효과 (side-effect)을 일으키는 경우다.

부수효과(side-effect)는 무엇인가? 이는 값을 resolve하는 것 이외에 코드에서 일어나는 모든 일들이다. 예를 들어

  • 네트워크 요청
  • 파일 읽고 쓰기
  • 데이터베이스 쿼리
  • DOM 엘리먼트 조작
  • 전역변수 수정
  • 콘솔 로그 등

이 부수효과는 함수형 프로그래밍의 핵심 아이디어다. 우리는 이 부수효과를 잘 다룸으로써 코드에 대한 자신감을 얻게 된다. 최대한 가능한, 우리는 순수 함수로 작성해야할 의무가 있다. 우리는 순수함수에서는 값을 계산하고 리턴하는 것만 해야 한다는 것을 명심해야 한다.

근데 이게 여기서 무슨 상관일까? if-else를 항상 의심의 눈초리로 봐야 한다는 것을 의미한다.

if (someCondition) {
  takeAction()
} else {
  someOtherAction()
}

someCondition 의 결과가 어디로 이끌든 우리가 신경 쓸 문제가 아니다. 우리가 이 구문에서 주의해야할 것은 여기서 부수효과가 일어난다는 것이다. 여기서 부수효과는 takeAction someOtherAction 이다. 그리고 둘 모두 어떠한 값도 리턴하고 있지 않다. 그리고 이 두 함수가 수행하는 작업은 모두 이 블록 밖에서 이루어진다. 그리고 우리는 이 두함수가 어떤 부수효과를 이르키는지 알고 있어야 한다. 그것에 대해 대답할 수 없다면, 우리는 코드를 이해할 수 없다.

즉 if문에서 부수효과가 일어나고 있으며, 이는 더이상 순수하지 않다는 것을 의미한다.

3항연산자로 돌아와서

if-else를 의심스럽게 봐야 한다는 건 그렇다 치자. 그렇다면 3항연산자는 어떤가? 이러한 부수효과의 측면에서 더 나은가? 이전에 했던 모든 논란은 여기서도 여전히 유효하다.

우리가 표현식을 좋아하는 이유는 표현식은 다른 표현식과 합칠 수 있기 때문이다. 다양한 연산자와 함수를 사용하면 복잡한 표현식을 간결하게 아래 처럼 작성해 줄 수 잇다.

'<h1>' + page.title + '</h1>'

우리는 이 표현식을 함수의 인수로 넘겨줄 수 있다. 또는 다른 표현식과 연산자로 결합할 수도 있다. 표현식을 여러개 결합하는 것은 더욱 복잡한 연산을 가능하게 해준다. 표현식을 묶는 것은 코드를 작성하는 훌륭항 방법이다.

하지만, statements도 결합할 수 있는 건 마찬가지 아닌가? for 문과 if 문도 합칠 수 있잖아? 그런데 왜 표현식에서 더 빛을 발하는 것일까?

표현식의 장점은 바로 참조 투명성 (referential transparency)에 있다. 이는 우리가 표현식에 있는 값을 취해서 다시 표현식 자체에 사용할 수 있다는 것을 의미한다. 그리고 우리는 이것을 바탕으로 항상 같은 결과가 리턴된다는 것을 확신할 수 있다. 이 참조 투명성은 statements와 expression을 작성하는 것과 의 차이점을 설명해준다.

그러면 표현식인 3항연산자를 더 선호해야 한다는 것인가? 그렇지 않다. 다른 언어처럼, 자바스크립트도 표현식에서 부수효과로 부터 자유롭지는 않다. 아래 예제를 보자.

const result = someCondition ? dropDBTables() : mineDogecoin()

표현식인 3항연산자에서도, 부수효과가 일어날 수 있는 여지는 충분히 존재한다.

조건문을 쓸 때 가져야할 책임감

그래 3항연산자도 별로고, if-else도 별로면 다른 언어를 써야하는 건가? 여기서 하고 싶은 말은 항상 신중히 행동해야 한다는 것이다.

안전한 statements를 선택하자.

statements가 없다면 자바스크립트 코드를 제대로 작성하기 어렵다. 우리는 이 statements에서 벗어날수가 없다. 우리는 이 statements를 안전하게 작성해야 한다.

가장 위험한 경우는 블록이 존재하는 경우다. 여기에는 if, for, while, switch 등이 포함된다. 이 들은 부수효과를 일으키는 것들이기 때문이다. 무언가 블록밖을 벗어나서, 현재의 환경을 변경시킬 수 있다.

이보다 안전한 statements는 변수할당문과 리턴문이다. 변수 할당은 expression의 결과를 레이블에 바인딩 한다. 그리고 변수는 그자체로 expression이다. 우리가 원하는 만큼 많이, 자주 사용할 수 있다. 이 값이 할당 된 이후에 변하는 것을 피한다면 (immutable하게 만든다면) 변수할당문은 안전하다.

반환 문은 함수 호출을 값으로 확인하므로 유용하다. 함수 호출은 하나의 표현식이다. 변수할당과 마찬가지로 return은 표현식을 만드는데 도움이 된다.

이 두가지 전제조건을 바탕으로, 안전한 조건문을 쓸 수 있는지 생각해볼 수 있다.

안전한 if 문

안전한 if문을 작성하기 위해서는 다음의 간단한 규칙을 따르면 된다. 첫번째 분기 조건에서 return으로 끝나면 된다. 그렇게 하면, if-staement가 값을 resolve하지 않더라도, 외부 함수는 가능해질 것이다.

if (someCondition) {
  return resultOfMyCalculation()
}

이 규칙만 따른다면, else 블록을 작성할 필요가 없어진다. 만약 else가 들어간다면, 거기에 부수효과가 들어간다는 것을 의미한다. 물론 작고 별일 하지 않을 수도 있지만, 부수효과가 존재하는 것은 사실이다.

읽기 쉬운 3항 연산자

3항 연산자는 항상 작고 간결해야 한다. 만약 그 식이 길어진다면, 수직으로 나누어서 가독성을 높여야 한다. 그럼에도 길어진다면, 변수 할당을 통해서 더욱 간결하게 만들 필요가 있다.

중첩 3항 연산자는 어떤가? 항상 피해야할까? 그렇지 않다. 만약 수직으로 잘 정렬만 해둔다면, 좀 깊이가 되는 3항연산자라 할지라도 가독성을 해치지 않을 수 있다.

const xAxisScaleFactor =
    (xRangeInSecs <= 60)       ? 'seconds' :
    (xRangeInSecs <= 3600)     ? 'minutes' :
    (xRangeInSecs <= 86400)    ? 'hours'   :
    (xRangeInSecs <= 2592000)  ? 'days'    :
    (xRangeInSecs <= 31536000) ? 'months'  :
    /* otherwise */              'years';

위 코드에 대해서 prettier가 뭐라고 하겠지만, 잠시 이를 꺼둘 수도 있다. 물론 이 과정은 손이 간다. 그러나 3항 연산자와 if문을 좀더 책임감있게 작성하는 것이 가능해진다.

미래엔..?

우리가 이렇게 더 신경쓸수는 있지만, 할 수 있는 일은 제한적이다. 하지만 미래에 약간의 변화가 있을 수도 있다. TC39에 있는 do expression 이 바로 그것이다.

let x = do {
  if (foo()) {
    f()
  } else if (bar()) {
    g()
  } else {
    h()
  }
}

do 블록 안에 많은 statements를 넣었고, 여기에서 하나의 값을 완성해서 리턴했다. jsx에서 아마도 더 유용하게 사용될 수 있다.

return (
  <nav>
    <Home />
    {do {
      if (loggedIn) {
        ;<LogoutButton />
      } else {
        ;<LoginButton />
      }
    }}
  </nav>
)

아직 이게 언제 도입될지 알수는 없지만 서도, babel을 써서 미리 써볼 수는 있을 것이다.

참고