들어가며

이 글은 미디움에 있는 9 Tricks for Kickass JavaScript Developers in 2019 라는 포스팅을 원작자의 허락을 받아 번역한 글입니다.

처음하는 번역이라 많은 의역과 오역이 있을 수 있습니다.
혹시 틀린 부분이 있으면 댓글로 피드백을 해주시면 정말 감사하겠습니다.


새해가 시작되었고, JavaScript는 여전히 사용되고 있다. 이 글은 당신이 깔끔하고 효율적으로 규모있는 코드를 작성할 수 있게 도울 수 있는 팁들이 적혀있다.
아래의 9가지 실용적인 팁들은 당신을 더 나은 개발자로 만들어 줄 것이다.

1. async / await

만약 당신이 아직도 콜백지옥(callback hell)에 빠져있다면, 2014년도로 돌아가길 원하는 것과 같다. 절대적으로 필요한 상황이 아니면 더 이상 callback을 사용하지 마라. (Ex. 라이브러리가 필요로 하거나, 성능적인 이유로 인한 것)

Promise도 괜찮지만 당신의 코드 크기가 커지면 커질수록 사용하는 것이 약간 곤란해진다.

오늘 날 내가 찾은 해답은 async / await 다. 이것은 나의 코드가 읽기 쉽고 깔끔할 수 있게 개선시켜준다.
사실, 당신은 JavaScript의 모든 Promise에서 await 할 수 있다. 당신이 사용하고 있는 Promise를 반환하는 라이브러리도 간단히 await 할 수 있다. 사실, async / await는 promise 에 대한 문법설탕이다. (문법 설탕이란 문법을 더 쉽게 사용해주는 방법을 말합니다.)

당신의 코드에서 await 가 동작하게 만들기 위해 당신은 당신의 함수 앞에 async 키워드를 추가해야한다.

  async function getData() {
      const result = await axios.get('https://dube.io/service/ping');
      const data = result.data 

      console.log('data', data);

      return data
  }

  getData();

기억해라. top 레벨에서는 await를 사용할 수 없다. 당신은 오직 async 함수에서만 await를 사용할 수 있다.

async / await는 ES2017에서 소개되었다. 사용하기 전에 당신의 환경을 확인하기 바란다.

2. async control flow

종종, 모든 비동기 호출들이 값을 반환한 후에 반환된 여러 데이터들을 가져와 각각의 데이터셋에 대해 어떤 처리를 하거나 작업할 때가 있다.

for ... of

우리는 우리의 웹페이지에서 한쌍의 포켓몬에 대한 정보들을 가져와야한다고 가정하자. 우리는 포켓몬에 대한 모든 호출이 끝날때 까지 기다리기를 원하지 않는다. 특히, 우리가 얼마나 많은 정보를 호출해야할 지 모를 때는.

하지만 우리는 우리가 요청한 데이터들을 받자마자 갱신하길 원한다. 이럴 때 async 함수 내부에서 호출을 반복하기 위해 for ... of 를 사용할 수 있다. 이것을 사용하면 각각의 호출이 성공할 때 까지 중단과 실행을 반복할 것이다.
여기서 기억해야할 건 이렇게 코드를 사용하면 당신의 코드에 병목 현상이 발생할 수 있다는 것을 알아야한다. 하지만 이것은 당신의 toolset을 유지하는데 유용할 것이다.

    import axios from 'axios'

    let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}];

    async function fetchData(dataSet) {
        for (entry of dataSet) {
            const result = await axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`);
            const newData = result.data;
            updateData(newData);

            console.log(myData);

            }

    }

    function updateData(newData) {
            myData = myData.map(el => {
                    if(el.id === newData.id) return newData;
                    return el 
            })
    }

    fetchData(myData);

이 예제들은 실제로 동작한다. 코드 샌드박스에서 자유롭게 사용해봐라.

Promise.all

만약 당신이 동시에 모든 포켓몬에 대한 정보를 가져와야 한다면 어떻해야 할까?
이럴때 Promise.all 을 사용하면 모든 요청이 올 때까지 await 할 수 있다.

import axios from 'axios'

    let myData = [{id: 0}, {id: 1}, {id: 2}, {id: 3}]

    async function fetchData(dataSet) {
            const pokemonPromises = dataSet.map( entry => {
                return axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`);
            });

            const results = await Promise.all(pokemonPromises)

            results.forEach( result => {
                updateData(result.data);
            });

            console.log(myData);

    }

    function updateData(newData) {
            myData = myData.map(el => {
                    if(el.id === newData.id) return newData;
                    return el;
            });
    }

    fetchData(myData);

for ... ofPromise.all 은 ES6+ 에서 소개되었다. 만약 당신의 코드에서 사용하고 싶으면 당신의 코드 버전을 확인해라.

3. Destructuring & default value

위의 예제로 돌아가보자.

const result = axios.get(`https://ironhack-pokeapi.herokuapp.com/pokemon/${entry.id}`)

const data = result.data

우리는 한 개의 배열이나 객체에서 하나 또는 여러 개의 값을 가져오기 위해 destructuring을 사용할 수 있다.
아래와 같이 쉽게 사용하자.

const { data } = await axios.get(...)

우리는 코드를 한 줄로 바꾸었다. 변수의 이름 또한 바꿀 수 있다.

const { data: newData } = await axios.get(...)

destructuring을 할 때 기본 값을 주기 위한 또 다른 좋은 트릭이 있다.

이것은 당신이 destructuring을 할 때 undefined 검사를 할 필요가 없게 만든다.

const { id = 5 } = {...}

console.log(id) // 5

또한 이 트릭들은 함수 파라미터를 가진 채로 사용될 수 있다.
예를 들자면 아래와 같다.

function calculate({operands = [1,2], type='addition'} = {}) {
        return operands.reduce((acc, val) => {
                switch(type) {
                        case 'addition':
                                return acc + val;
                        case 'subtraction':
                                return acc - val;
                        case 'multiplication':
                                return acc * val;
                        case 'division':
                                return acc / val;
                }
        }, ['addition', 'subtraction'].includes(type) ? 0 : 1)
}

console.log(calculate()); // 3
console.log(calculate({type: 'division'}));  // 0.5
console.log(calculate({operands: [2,3,4], type: 'multiplication'})) // 24

위 예제는 처음에 약간 혼란스러워 보일 수 도 있지만, 시간을 써서 사용해 봐라. 우리가 함수의 매개인자로써 어떤 값도 통과시키지 않을 때, 기본 값(default values)이 사용되어 진다. 또 우리가 값을 통과시키면 존재하지 않는 매개인자에 대해 기본 값들이 사용된다.

Destructuring 과 default values 들은 ES6 에서 소개되어졌다. 당신의 코드에서 동작하는지 확인해라.

4.Truthy & Falsy values

default values 를 사용하면, 값의 유효 검사들은 과거의 것들이 될 것이다. 그러나, truthy 와 falsy 값을 아는 것은 당신에게 매우 도움이 된다. 이것은 당신의 코드를 향상시킬 것이다.

나는 보통 사람들이 이런식의 코드를 짜는 것을 보았다.

if(myBool === true) {
        console.log(...)
    }

    // OR 

    if(myString.length > 0) {
        console.log(...)
    }

    // OR 

    if(isNaN(myNumber)) {
        console.log(...)
    }

하지만 이 코드들은 더 짧게 될 수 있다:

if(myBool) {
    console.log(...)
}

// OR 

if(myString) {
    console.log(...)
}

// OR 

if(!myNumber) {
    console.log(...)
}

이 조건문들의 이점을 가지고 가기 위해서는, 당신은 truthy와 falsy 값들이 무엇이 있는지를 이해해야 한다.

Falsy

  • string with the length of 0 (길이 값이 0인 문자열: '', "")
  • the number 0(숫자 0)
  • `false``
  • undefined
  • null
  • NaN

Truthy

  • empty arrays(빈 배열: [])
  • empty objects(빈 객체: {})
  • Everythig else: (falsy 하지 않은 모든 값)

기억해라. truthy 값과 falsy 값을 검사하는 것은, 이중 동등 연산자 (==)로 검사하는 것과 같다.
일반적으로 ===에서는 같은 방법으로 동작하지만 어떤 상황에 대해서는 버그로 끝날 수 있다. 나의 경우에는 주로 숫자 0 에서 많이 발생 하였다.

5. Logical and ternary operators

여기 당신의 코드에서 더 짧게 사용 될 수 있는게 하나 더 있다. 당신의 이전 코드를 저장해라.
때때로 이 팁은 당신의 코드가 깨끗하게 유지될 수 있게 돕는 좋은 도구가 될 수 있지만 연속적으로 사용될 때 약간의 혼란을 유발 할 수 있다.

Logical operators

논리 명령어들은 기본적으로 두개의 표현식이 결합된다. 그리고 truefalse 둘 중 하나가 반환될 것이다. 또는 "and"를 의미하는 && 과 "or"을 의미하는 ||에 의해 일치되는 값이 반환 될 수 있다.

어떤 말인지 예제를 보자:

console.log( true && true ) // true
console.log(false && true) // false
console.log(true && false) // false
console.log(false && false) // false
console.log(true || true) // true
console.log(true || false) // true
console.log(false || true) // true
console.log(false || false) // false 

우리는 지난 팁으로 본 truthy와 falsy 값의 지식을 논리 명령어에 대한 지식과 결합시킬 수 있다.
논리 명령어들을 사용할 때, 다음과 같은 규칙이 따라온다.

  • &&: 처음의 falsy 값을 반환 받는다. 만약 없다면, 마지막 truthy 값이 반환되어진다.
  • ||: 처음의 truthy 값을 반환 받는다. 만약 없다면, 마지막 falsy 값이 반환되어진다.
console.log(0 && {a: 1}) // 0
console.log(false && 'a') // false
console.log('2' && 5) // 5
console.log([] || false) // []
console.log(NaN || null) // null
console.log(true || 'a') // true

Ternary operator (삼항 연산자)

3항 연산자는 논리 연산자와 매우 비슷하다. 하지만 3가지 다른 부분을 가진다.

  1. 비교는 falsy 거나 truthy 중 하나이다.
  2. 비교가 truthy 일 경우 처음 값이 반환된다.
  3. 비교가 falsy 일 경우 2번째 값이 반환된다.

여기 예제가 있다:

const lang = 'German'
console.log(lang === 'German' ? 'Hallo' : 'Hello') // Hallo
console.log(lang ? 'Ja' : 'Yes') // Ja
console.log(lang === 'French' ? 'Bon soir' : 'Good evening') // Good eveing

6.Optional chaining

당신은 객체나 하위 프로퍼티 중 하나가 존재하는지 모르는 상태에서 중첩된 오브젝트 속성에 접근하는 문제가 있었던 적이 있는가? 당신은 아마도 다음과 같은 것을 하게 될 것이다.

let data;
if(myObj && myObj.firstProp && myObj.firstProp.secondProp && myObj.firstProp.secondProp.actualData) data = myObj.firstProp.secondProp.actualData;

이 방법은 별로 좋지 않다. 이것보다 더 나은 방법이 있다. 이것을 optional chaining이라고 부른다.

const data = myObj?.firstProp?.secondProp?.actualData;

나는 중복된 속성들을 검사하기 위해 괜찮은 방법이라고 생각한다. 이것은 코드를 좀 더 깨끗하게 만들어 줄것이다.

현재, optional chaining은 공식 스펙이 아니다. experimental feature로써 stage-1에 있다. 당신은 당신의 babelrc 안에서 이것을 사용하기 위해 @babel/plugin-proposal-optional-chaining를 추가해야한다.

7. Class properties & binding

JavaScript에서 바인딩된 함수들은 공통된 작업이다. ES6 스펙에서 소개된 arrow function과 같이, 우리는 지금 매우 유용하고 JavaScript 개발자들 사이에서 흔하게 사용될 수 있는 자동으로 함수를 bind 하고 context를 명시 할 수 있는 방법을 가진다.

클래스가 처음 소개되었을 때, 당신은 arrow function을 더 이상 사용할 수 없었다. 왜냐하면 클래스 메서드들은 구체적인 방법으로 명시되어야 했다.

우리는 함수들을 다른 곳에서 결합했어야만 했다. 예를 들자면 생성자 안에서 결합했어야 했다.(Ex. React.js의 best practice).

하지만 우리는 이제 클래스 안에서 arrow function을 사용할 수 있다.

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: 0 }
    }

    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this._increaseCount}>Increase Count</button>
            </div>
        )
    }

    _increaseCount = () => {
        this.setState({ count: this.state.count + 1});    
    }
}

현재 클래스 프로퍼티들은 official spec이 아니다. 그러나 experimental feature로써 stage-3에 있다. 당신은 당신의 babelrc에 @babel/plugin-proposal-class-properties을 추가함으로 써 사용할 수 있다.

8. Use parcel

당신이 frontend 개발자라면 틀림없이 이미 bundling과 transpiling 코드를 맞닥들였을 것이다. 이것을 위한 사실상의 표준은 오랜시간 동안 웹팩 이었다.

나는 처음에 webpack 버전 1을 사용했었다. 그건 고통스러웠다. 모든 다른 설정 옵션들을 만지작 거리면서, 나는 bundling 되고 동작을 얻기 위해서 끊임없는 시간들을 소비했다. 만약 내가 다시 그래야한다면 나는 절대 그것을 건들이지 않을 것이다. 몇달 전에 나는 parcel로 건너왔다. 이건 극적인 효과가 있었다. 이것은 너에게 발군의 효과를 줄 것이다. 이것은 필요할 때 마다 너에게 바꿀 기회를 줄것이다. 또한 webpack 또는 babel과 유사한 plugin 시스템을 지원하며 믿을 수 없을 정도로 빠르다. 만약 당신이 파셀을 알지 못한다면, 나는 당신이 꼭 한번 검토해 볼 것을 제안한다.

9. Write more code yourself

이것은 좋은 주제이지만 이것에 관한 다른 토론들은 나에게 진절머리가 난다. CSS를 이야기하자면 사람들은 bootstrap 같은 컴포넌트 라이브러리를 사용하려는 경향이 있으며, JavaScript를 예로 들면, 나는 여전히 사람들이 JQuery를 사용하거나 벨리데이션이나 슬라이더, 기타 등등을 위한 작은 라이브러리를 사용하는 것을 본다. npm 패키지로 설치되는 보이지 않는 라이브러리 사용법을 이해 할 동안에, 나는 너 스스로 코드를 만들어 쓰는 것을 강력히 추천한다. 큰 팀들이 만든 큰 라이브러리나 (또는 프레임워크) moment.js나 react-datepicker과 같은 것들에 대해 너 스스로 만들어 쓰라는 것이 아니다. 그러나 당신은 당신 가능한 스스로 사용할 수 있는 코드는 작성할 수 있다. 이것은 당신에게 3가지 이점을 줄 것이다.

  1. 당신은 당신의 코드가 어떻게 동작하는지 정확히 이해할 수 있을 것이다.
  2. 어떤 지점에서, 당신은 정말로 프로그래밍과 기반 지식이 어떻게 동작하는지 이해하기 시작할 것이다.
  3. 당신은 당신의 코드베이스가 날아갈 수 있는 것을 예방한다.

단지 npm package를 사용해서 시작하는 것은 쉬울 것이다. 몇몇 기능을 당신 스스로 구현하는 것은 분명 시간이 걸린다. 그러나 패키지가 정말 기대한대로 동작하지 않는 다면 당신은 그것을 또 다른 패키지로 교체 해야한다. 또 교체를 위해 API를 어떻게 설정하는지 읽는 것으로 더 많은 시간을 보낼 것이다.

당신 스스로 기능들을 구현 할 때, 당신은 당신의 유스케이스에 백퍼센트로 동작을 맞출 수 있다.