3 분 소요


파일 이름

구글에는 Javascript style guide가 존재한다.

회사 별로 사용하는 stylesheet는 모두 다르고 하기 때문에 코딩을 할 때 본인에게 맞는 스타일대로 일관성있게 코드를 짜는 것이 나쁘지 않다.

다만 그 style guide는 논리적인 이유를 토대로 정해진 것이기 때문에 그 룰을 왜 그렇게 사용하는지에 대한 이해가 뒷받침된다면 더욱 좋을 것이다.

  • 논리적인 이유에는 버그의 최소화도 있을 것이고 가독성도 있을 것이고 여러 이유가 있을 것이다.


이 컨벤션 중 파일 이름에 대해서 살펴 보자면 다음과 같은 규약이 있다.

  • 파일 이름은 반드시 소문자여야 하고 _-를 포함할 수 있다.
  • .js로 끝나야 한다.
  • UTF-8로 인코딩이 되어야 한다.
  • 등등…

이 중에서 소문자로만으로 파일 이름을 구성해야 하는 이유 중 하나는 require를 할 때 보통은 같은 모듈에 대해 여러번 require를 하더라도 한 번만 가져오게 되고 각각 가져온 객체는 같은 레퍼런스를 사용하게 되는데 require에 들어가는 인자의 문자열 중 하나 혹은 여러개를 대문자로 작성하면 다른 객체로 인식하게 된다.

그렇기 때문에 require가 되긴 하지만 각각을 다른 모듈로 생각하기 때문에 문제가 될 여지가 생기게 된다. 이를 막기 위해서 이러한 규약을 추가한 것이다.


node standard library가 제공하는 함수

fs 모듈을 가져와서 알아보도록 하겠다.

const fs = require('fs')

fs.readFile('src/main.js', 'utf-8', (err, result) => {
    if(err) {
        console.log(err)
    } else {
        console.log(result)
    }
})

위에서 보면 알 수 있듯이 standard library에서 제공하는 함수에서는 그 인자로 각각의 기능을 위해 필요한 인자를 쭉 받고 항상 마지막에는 콜백 함수를 받는다.

이 콜백 함수의 인자에는 첫 번째로는 err, 그 나머지는 결과를 받을 수 있도록 되어 있다.


fs에는 다음과 같은 기능도 제공한다.

const result = fs.readFileSync('src/main.js', 'utf-8')
console.log(result)

이 코드는 위의 코드와 결과가 정확히 같은 의미를 가지고 있다. 하지만 함수 이름에서 알 수 있듯이 synchonous(동기식)하게 동작한다.

그렇기 때문에 동작방식이 main.js 파일을 읽고 utf-8으로 인코딩을 마치고 이 결과를 result 변수에 넣을 때 까지는 다음 코드가 동작하지 않는 방식으로 진행되어 blocking 방식을 통해 엄격하게 순차적으로 코드가 진행되는 식이다. 이는 매우 간결한 형태의 코드이기 때문에 퍼포먼스 측면에서 별다른 차이를 느끼지 못하지만 규모가 큰 코드일 수록 이 차이가 매우 커질 것이다.


그럼 첫 번째 코드는 비동기식 진행을 의미하는데 이 코드의 진행 방식은 먼저 시스템 호출을 통해서 OS에게 파일을 읽어오라고 요청을 한 다음 응답이 올 때까지는 콜백함수가 실행되지 않고 있지만 다른 main.js의 스레드(아래의 코드)는 실행을 하고 있게끔 한다.

그 후 OS가 응답을 했을 때, 즉 파일이 다 읽어지고 났을 때서야 안에 있던 콜백 함수의 코드가 실행이 되는 식으로 동작한다.


이는 네트워크 관점에서 봤을 때 엄청난 차이를 가져올 수 있기 때문에 항상 비동기식 진행을 강조하게 된다.

  • 동기식 진행을 하면 아무 일도 하지 않는 idle time이 길어지기 때문에 굉장히 퍼포먼스가 낮은 것이다.


그럼에도 동기식 형태가 쓰이는 이유를 꼽자면 코드의 간결성과(콜백 함수가 없기 때문에) 동시성이 중요하지 않은 상황에서 퍼포먼스가 중요하지 않은 경우를 말할 수 있을 것이다.

이러한 배경지식에서 유념해야 할 점은 sync를 통해 코드를 실행할 경우 error를 catch 하기 위해선 try-catch 구문을 사용해야 한다는 것이다.(비동기식에서는 콜백 함수의 인자로 err르 받음으로써 처리한 것과는 다르게)


promise style

앞선 방식 들을 각각 callback-style과 sync-style이라고 할 수 있었다면 나머지 하나의 방식이 더 존재하는데 바로 promise style이다.

// promise-style
await fs.promises.readFile(FILENAME, 'utf-8')

이것이 앞선것들과 다른 점은 await을 해야한다는 점인데 이 await의 의미는 돌려주어야 한다는 의미이고 이 함수를 실행했을 때 돌려주는 것은 promises인데,

javascript 문법에 의해 top level에서는 await을 할 수 없으니까 다음과 같이 async function으로 감싸고 밖에서 해당 함수를 실행하는 식으로 코드를 작성해야 한다.

async function main() {
    await fs.promises.readFile(FILENAME, 'utf-8')
}
main()



이 코드를 작성하면서 나타날 수 있는 문제로 eslint가 node의 버전을 이해하지 못해서 node 구버전에서는 해당 기능을 사용할 수 없다는 에러가 발생하는 경우가 종종 있는데 이는 package.json에 engines 필드를 추가하여 노드의 버전을 명시하면 문제가 해결된다.

{
    "engines": {
        "node": "14.16.1"
    },
    ...
}

그래서 현재는 많은 경우에 async/await을 쓰면 훨씬 syntax가 간결해 지고 다양한 경우에도 유연하게 대응할 수 있기 때문에 이 promise-style을 쓰는 것이 강력하게 권고되어진다.

async function main() {
    const result = await fs.promises.readFile(FILENAME, 'utf-8')
    console.log(result)
}

main()


예외적으로 노드가 구버전이라 promises를 사용할 수 없는 경우에는 다음과 같이 util을 이용하여 작성할 수 있으니 노드 구버전을 사용해야 하는 환경에서는 이를 활용하면 좋을 것이다.

const util = require('util')

async function main() {
    const result = await util.promisify(fs.readFile)(FILENAME, 'utf-8')
    console.log(result)
}

main()


await을 할 때 error가 나는 것을 catch하기 위해서는 똑같이 try-catch로 잡아야 한다. (콜백 함수가 없기 때문)

async function main() {
    try{
        const result = await fs.promises.readFile(FILENAME, 'utf-8')
    console.log(result)
    } catch(error) {
        console.error(error)
    }
}

main()

댓글남기기