노드를 제대로 쓰기 위해 꼭 알아야 할 자바스크립트 개념 10가지
컨텐츠 정보
- 조회 479
본문
새로운 런타임인 디노(Deno), 번(Bun)과의 경쟁에 직면했다 해도, 서버에서 자바스크립트의 대표적인 플랫폼은 여전히 노드js(Node.js)다. 노드는 익스프레스(Express)와 같은 서버 측 노드 프레임워크, 웹팩(Webpack)과 같은 빌드 체인 툴, 그리고 개발자 친화적인 풍부한 유틸리티에 힘입어 백엔드에서 자바스크립트의 강력함과 표현력을 활용하는 데 있어 큰 인기를 끄는 플랫폼이다.
물론 노드가 가진 인기의 상당부분은 다양한 프로그래밍 스타일을 지원하는 다중 패러다임 언어인 자바스크립트에 기인한다. 자바스크립트는 원래 HTML의 빈틈을 메우기 위한 동적 스크립팅 언어로 개발됐지만 현대의 자바스크립트는 함수형 프로그래밍과 반응형 프로그래밍, 객체 지향 프로그래밍에 사용된다. 개발자는 한 가지 스타일에 집중할 수도 있고 다양한 사용례의 요구사항에 따라 유연하게 여러 스타일을 바꿔가며 사용할 수도 있다.
그러나 자바스크립트의 다중 패러다임 특성은 양날의 검이다. 자바스크립트에서 다양한 프로그래밍 패러다임을 사용하는 방법을 제대로 이해해야 한다. 다재다능한 언어인 만큼 하나의 스타일만 딱 집어서 항상 최고라고 믿는 것은 그다지 합리적이지 않다. 또한 언어가 유연한 만큼 코드를 체계화하고 유지보수 편의성을 유지해야 할 개발자의 부담은 더 커진다. 노드를 사용할 때는 단일 스레드 플랫폼임을 계속 염두에 두어야 한다는 부가적인 어려움도 따른다. 비동기 코드를 작성한다 해도 진정한 의미의 동시성은 갖지 않는다.
자바스크립트는 신중하게 사용하면 약이 되지만 아무렇게나 사용하면 독이 될 수도 있다. 노드js로 확장성 높은 코드를 작성하는 데 필요한 10가지 자바스크립트 개념을 소개한다.
Promise와 async/await
자바스크립트와 노드에서는 다수의 작업을 “논블로킹” 방식으로 동시에 수행하는 것이 가능하다. 즉, 다른 작업이 완료될 때까지 기다리지 않고 플랫폼에 여러가지 일을 동시에 하도록 지시할 수 있다. 자바스크립트의 비동기 프로그래밍에 사용되는 주된 메커니즘은 Promise 객체와 async/await 키워드다(이벤트 루프에 대해서는 다음 세션에서 다룸).
Promise는 처음에는 난해할 수 있지만, 일단 이해하면 이를 활용해 아주 간편하게 비동기 작업을 제공하고 소비할 수 있게 된다. 노드에서 대부분의 경우 우리는 소비하는 측에 해당한다.
const fileReadPromise = fs.readFile(filePath, 'utf-8');fileReadPromise.then(fileData => { console.log('Success… file content:'); console.log(fileData);});.then() 구문은 Promise가 완료된 후 발생하는 상황을 처리하는 데 사용된다. 또한 오류에 대처하기 위한 .catch() 함수도 포함돼 있다. 이 두 가지 구문 요소는 Promise가 실행된 후 발생하는 상황을 설명하기 위한 간편한 방법을 제공한다.
async/await의 경우 동기(또는 선형) 구문을 사용해서 같은 종류의 뭔가를 기술한다(참고로 이 코드에서는 오류 처리는 무시함).
async function readFileAsync() { const fileData = await fs.readFile(filePath, 'utf-8'); console.log('Success... file content:'); console.log(fileData);}이 코드가 하는 일은 Promise를 사용하는 버전과 동일하다. readFileAsync 함수에 async 키워드를 붙여 인터프리터에게 비동기 작업을 수행할 것임을 알린다. 그런 다음 실제 작업(fs.readfile)을 호출해야 할 때 await를 사용한다.
여기서 자바스크립트의 Promise와 async/await에 대해 자세히 알아볼 수 있다.
이벤트 루프
노드 작업에서 Promise 객체와 async/await를 효과적으로 사용하려면 노드 이벤트 루프에 대해서도 이해해야 한다. 노드가 단일 스레드로 분류되는 이유는 이벤트 루프에 있다. 개념이 생소하더라도 걱정할 필요 없다. 노드(그 외의 유사 플랫폼도 마찬가지) 내에서 코드는 완전히 멀티태스킹되지 않는다는 점만 기억하면 된다. 이 부분이 자바 또는 고(Go)와 같이 플랫폼이 운영체제 스레드를 직접 사용할 수 있는 동시성 플랫폼과의 차이점이다.
이건 규모가 커지기 전까지는 애초에 고민해야 할 제약은 아니다. 노드를 이해하는 데 있어 비동기 작업이 어떻게 작동하는지 아는 것이 중요하다.
예를 들어 다음과 같이 fetch() 호출을 통해 비동기 작업을 시작한다고 가정해 보자.
const response = await fetch(“https://anapioficeandfire.com/api/characters/232”)작업이 제출되면 노드는 운영체제의 네트워킹 서비스에 요청을 스케줄링한다. 이 시점에서 이벤트 루프는 병렬로 진행 가능한 다른 네트워크 요청을 시작하는 등의 다른 작업을 수행할 수 있다. 진행 중인 요청 중 하나가 완료되면 이벤트 루프는 알림을 받는다. 이벤트 루프는 시간이 생기면 사용자가 정의한 핸들러를 사용해 작업을 진행한다. 예를 들어 다음을 보자.
const characterData = response.json().then( /* do stuff */ );작업이 완료되면 비동기 작업은 끝난다. 이것이 노드에서 모든 비동기 프로그래밍의 일반적인 흐름이다.
스트림
스트림은 노드가 네트워킹과 파일시스템, 기타 채널에서 데이터 흐름을 모델링하는 방식이다. 스트림은 파일에 작성되거나 파일에서 나가는 비트와 같은 일련의 “항목”을 나타낸다. 흐름 내에서 중요한 일이 벌어지면 이벤트가 발생하고 핸들러를 기반으로 애플리케이션 코드에서 호출된다.
비동기 프로그래밍에 대한 이야기와 비슷하게 들릴 수 있는데, 이는 두 개념이 밀접하게 관련되기 때문이다. 스트림은 노드가 대용량일 수도 있는 데이터 흐름을 논블로킹 방식으로 처리하기 위해 제공하는 코드 메커니즘이다.
예를 들어 노드에서 fs(파일 시스템) 표준 라이브러리를 사용하면 방금 본 것과 같이 파일을 열 수 있고, 파일에서 이벤트를 소비해서 데이터를 청크로 받고 파일 끝과 오류에 대한 알림을 받을 수 있다. 이는 대용량 파일을 처리할 때 매우 중요하다. (이렇게 하지 않으면 메모리에 전체 데이터 집합을 로드해야 할 것이다.)
const filePath = 'hugefile.txt';const readableStream = fs.createReadStream(filePath, { encoding: 'utf8' });readableStream.on('data', (chunk) => { console.log(`Received ${chunk.length} characters of data.`); }readableStream.on('end', () => { console.log('Finished reading the file.');});readableStream.on('error', (error) => { console.error(`An error occurred: ${error.message}`); });.on() 콜백은 관찰할 이벤트와 이후 작업을 수행하기 위한 실제 함수를 정의할 수 있게 해준다.
모듈
자바스크립트에서는 모듈을 사용해 코드를 관리하기 용이한 청크로 정리한다. 파일 시스템 라이브러리와 마찬가지로 다른 사람이 정의한 기능을 활용하기 위해 하나 이상의 모듈을 코드로 가져오는 방식이 일반적이다.
import fs from 'fs/promises';또는 작업 중인 프로젝트의 로컬 파일에서 코드를 가져올 수도 있다.
import utils from './utils/mjs';내보내기할 때 이름이 지정됐다면 모듈의 특정 부분만 가져올 수도 있다.
import { specificUtil } from './utils.mjs'; 모듈 내보내기를 수행하는 방법은 다음과 같다.
export const utilA = () => { /* ... */ }; export function utilB() { /* ... */ };const defaultValue = { /* ... */ }; export default defaultValue;기본 내보내기는 직접적인 가져오기를 수행할 때 받게 되고, 명명된 내보내기는 이름이 지정된 가져오기에 매핑된다. 또한 utilA와 utilB에서 볼 수 있듯이 두 가지 구문으로 내보내기를 정의할 수 있다. 여기서 모듈 파일의 확장자는 .mjs인데, NPM package.json 파일에 "type": "modules"를 설정하지 않았다면 이렇게 해야 한다. 이 부분에 대해서는 아래 NPM 섹션에서 더 자세히 다룬다.
중요한 참고 사항 : 지금까지 살펴본 내용은 노드와 자바스크립트가 대체로 표준화된 ESM(ECMASCIPT 모듈 구문)이다. 그러나 노드에서 오랜 기간 사용된 또 다른 구문인 CommonJS도 있다. 많은 경우 노드 코드에서는 다음과 같은 부분을 볼 수 있다.
// Import:const fs = require('fs'); const myUtil = require('./utils.js'); // Export:function utilityOne() { /* ... */ } module.exports = { utilityOne };exports.utilityOne = utilityOne; 개념적으로 볼 때 이 섹션에서 기억해야 할 가장 중요한 부분은 소프트웨어를 구성요소로 분할하는 것이 복잡성 관리를 위한 절대적인 핵심이라는 점이다. 모듈은 노드에서 이를 구현하기 위한 고수준 메커니즘이다.
클로저와 스코프
자바스크립트와 노드에서 함수를 정의하고 실행할 때 이 함수 주변에 존재하는 다양한 변수를 함수에서 사용할 수 있다. 이 단순한 개념이 바로 클로저이며, 상당히 강력한 기능을 제공한다. 주변 컨텍스트에 정의된 변수의 “스코프”에 중첩된 함수 내부에서 접근할 수 있다는 것이 핵심이다.
표준 함수는 부모 컨텍스트의 변수에 접근할 수 있다.
let album = "Abbey Road"; let rank = function(){ console.log(`${album} is very good!`); // output: “Abbey Road is very good!”}rank();그러나 클로저의 경우 다음과 같이 부모가 완료된 이후에도 내부 함수에서 스코프에 접근할 수 있다.
function setupRanker() { let album = "Abbey Road"; let rank = function() { console.log(`${album} is very good!`); }; return rank; // Return the inner function}const rankerFunction = setupRanker(); rankerFunction(); 주변 컨텍스트의 런타임 실행이 끝난 다음에도 함수에서 해당 변수를 계속 사용할 수 있음을 강조하기 위해 이를 “렉시컬 스코프(lexical scope)”라고도 한다.
위는 아주 간단한 예제이지만, 여기서 rank() 함수가 주변 컨텍스트에 정의된 album 변수에 접근할 수 있음을 명확히 보여준다. 이것이 핵심 개념이다. 이 기본을 명확히 이해하면 그에 따른 여러 영향을 살펴보는 데 도움이 된다.
클로저는 자바스크립트의 핵심 기능이며 노드에서도 중심에 위치한다. 또한 클로저는 함수형 프로그래밍의 중심이기도 하다.
클래스
현대 자바스크립트는 객체를 정의하기 위한 강력한 클래스 기반 구문을 제공한다. 객체의 개념과 객체를 만들고 활용하는 방법은 반드시 이해해야 한다. 자바스크립트 내부적으로는 여전히 프로토타입을 사용하지만 클래스가 더 보편적인 접근 방식이다. 많은 언어에서 광범위하게 사용된다는 점과 그 단순성을 감안하면 당연한 현상이다.
객체를 사용하면 객체 지향 프로그래밍의 더 고급 영역으로 들어갈 수 있지만 객체와 클래스를 사용하기 위해 꼭 고급 영역까지 갈 필요는 없다. 클래스는 다음과 같이 특정 객체 타입을 정의할 뿐이다.
// Definition:class Album { #tracklist; constructor(title) { this.title = title; this.#tracklist = []; } addTrack(trackName) { this.#tracklist.push(trackName); } displayAlbumInfo() { console.log(`Album: ${this.title}`); console.log(`Tracks: ${this.#tracklist.length}`); } static getGenreCategory() { return "Recorded Music"; }}// Usage:const album1 = new Album("Abbey Road");const album2 = new Album("The Joshua Tree");album1.addTrack("Carry That Weight");album2.addTrack("In God’s Country");album1.displayAlbumInfo();album2.displayAlbumInfo();이 예제에는 음악 앨범을 모델링하기 위한 Album 클래스가 있다. 저장되는 데이터를 보면 제목 문자열, 곡 목록 배열 등이므로 이 클래스가 무슨 일을 하는지는 매우 명확하다. 각 객체는 이러한 요소의 자체 변형을 가질 수 있다(해시 구문이 #trackList를 통해 비공개 변수를 제공한다는 점에 주목).
이 단순한 개념이 상호작용하는 데이터 집합을 모델링하는 데 있어 얼마나 강력한 기능을 제공하는지 개념적으로 이해하면 된다. 관련 정보와 “동작”(함수)을 함께 “캡슐화”하는 기본 메커니즘은 노드 개발자라면 반드시 알아야 한다.
NPM과 노드 생태계
대부분의 애플리케이션은 외부 라이브러리에 의존해 작업을 수행한다. 노드에는 이 외부 라이브러리를 지정하고 사용하기 위한 강력한 패키지 관리자인 NPM(Node 패키지 관리자)이 있다. 여기서 볼 수 있듯이 NPM을 중심으로 한 방대한 생태계가 형성돼 있다.
NPM은 프로그램에 넣을 수 있는 package.json 파일을 제공한다. 이 파일은 다양한 속성을 가진 JSON 파일인데, 그 중에서 가장 중요한 속성은 “dependencies”다. NPM은 개발자가 파일에서 지정해둔 종속 항목을 앱의 /node_modules 디렉토리에 설치한다. 앱은 실행 중일 때 이런 모듈을 가져오기 형태로 사용할 수 있다.
이것이 기본적인 개념이지만, 그 외에도 scripts 속성에 프로그램의 스크립트를 정의하고 type 속성을 module로 설정해서 .mjs 확장자 없이 ESM 모듈 파일을 기본값으로 사용하는 등 다른 중요한 기능도 있다. 엄격히 모듈만 사용할 계획이 아닌 프로젝트를 개발할 때도 일반적으로 이렇게 한다. 이 속성을 설정하지 않으면 NPM은 .js 파일이 CommonJS 모듈이라고 전제한다.
NPM은 노드 개발의 주축이다. yarn, pnpm 등 더 새로운 다른 툴도 있지만 표준은 NPM이다. 개념적으로 노드 프로그램의 내부는 자바스크립트지만 외부는 NPM이다.
JSON
JSON(JavaScript Object Notation) 없는 자바스크립트와 노드는 상상하기 어렵다. 사실 JSON 없는 현대 프로그래밍 세계를 상상하는 것부터 어렵다. JSON은 애플리케이션 간에 데이터를 설명하고 공유하기 위한 간단하면서도 놀라울 만큼 다재다능한 방법이다.
기본은 단순하다.
{ “artist”: “Rembrandt”, “work”: “Self Portrait”, “url”: “https://id.rijksmuseum.nl/200107952”}여기에는 딱히 살펴볼 만한 부분이 없다. 중괄호 쌍과 콜론이 붙은 이름/값 쌍이 전부다. 완전한 객체 그래프를 위해 이 안에 배열 및 다른 JSON 객체를 포함할 수도 있다. 그 수준을 넘어가면 내장된 JSON.stringify와 JSON.parse부터 JSON을 문자열로 변환하거나 문자열을 JSON으로 변환하는 데 이르기까지 JSON을 다루기 위한 다양한 기법과 툴이 존재한다.
JSON은 클라이언트에서 서버 및 다른 위치로 데이터를 전송하는 방법이라고 할 수 있다. 또한 요즘은 특히 몽고DB와 같은 NoSQL 데이터베이스의 데이터스토어에서도 보편적으로 사용된다. JSON을 잘 익히면 훨씬 더 매끄러운 노드 프로그래밍이 가능하다.
오류 처리
좋은 오류 처리는 노드의 핵심 개념이다. 노드에는 표준 런타임 오류와 비동기 오류, 두 가지 종류의 오류가 있다.
Promise의 비동기 오류는 .catch() 콜백으로, 일반적인 런타임 오류는 try/catch 키워드 블록으로 처리된다.
try { /* do something risky */} catch (error) { /* do something with the error */}비동기 작업 중 오류가 발생하면 이 콜백이 실행돼 오류 객체를 받는다.
async/await를 사용할 때는 표준 try/catch 블록을 사용한다.
개념적으로 유의해야 할 중요한 점은 언제든 뭔가 잘못될 수 있으며 이에 대처하기 위해서는 오류를 포착해야 한다는 것이다. 가급적이면 작업을 복구하는 것이 좋다. 그렇지 않으면 최종 사용자 관점에서 오류 조건이 최대한 불편하지 않게 해야 한다. 또한 가능하면 오류를 로그로 남기는 것이 좋다. 외부 서비스와 상호작용한다면 오류 코드를 반환할 수도 있다.
오류를 처리하는 최선의 방법은 상황에 따라 다르다. 핵심은 항상 오류에 유의하는 것이다. 미리 생각했지만 잘못되는 경우보다 미쳐 생각하지 못한 오류 조건이 일반적으로 더 골칫거리가 된다. 또한 오류 “삼키기”, 즉 아무런 일도 하지 않는 catch 블록을 정의하지 않도록 주의해야 한다.
유연성 유지
기사를 시작하면서 언급한 바와 같이 자바스크립트는 함수형, 객체지향형, 반응형, 명령형, 프로그래밍 방식 등 다양한 프로그래밍 스타일에 대응할 수 있다. 여기에는 강력한 개념이 폭넓게 존재하며 그 중 일부는 프로그래밍에서 가장 중요한 개념이다. 다음 단락에서는 각각을 세부적으로 살펴볼 것이다.
농담이다! 위의 모든 프로그래밍 스타일을 다루려면 글이 책 한권 분량을 훌쩍 넘어설 것이다. 핵심은 자바스크립트가 모든 스타일을 포용할 만큼 유연하다는 사실이다. 아는 기능을 실제로 사용하고 새로운 개념에 대해서도 마음을 열어야 한다.
우리는 해내야 할 일에 계속 직면하며 그 일을 수행하는 방법은 항상 둘 이상 존재한다. 더 멋지고 빠르고 효율적으로 할 방법이 있는지에 대한 고민은 건강한 고민이다. 지금 작업을 수행하는 것과 나중을 위해 새로운 기술과 베스트 프랙티스를 연구하는 것 사이에서 균형을 맞춰야 한다.
이해가 깊어질수록 특정 패러다임이 잘 맞는 작업과 분야도 더 자연스럽게 발견하게 된다. 프로그래밍을 진정으로 사랑하는 사람은 “내 패러다임/툴/프레임워크가 네것보다 낫다”는 아집에 사로잡히지 않는다. 그보다는 “내가 아는 범위에서는 이 작업에는 이 툴이 최선이지만, 내 사고는 언제든 개선을 위해 열려 있다”는 생각을 갖는 것이 좋고, 경력에도 더 도움이 된다.
음악, 무술 등 모든 분야가 그렇듯이 진정한 마스터는 탐구할 대상은 항상 존재한다는 사실을 잘 안다. 프로그래밍에서 지금 그 대상은 노드를 사용한 서버 측 자바스크립트 앱 구축이다.
dl-itworldkorea@foundryco.com
관련자료
-
링크
-
이전
-
다음






