이 글은 프로그래밍 경험이 없는 분들을 대상으로 자바스크립트 웹 앱 제작법을 알려드리는 ‘웹 개발 배우기’ 시리즈의 일부인데요.
이번 시간에는 아주 널리 쓰이는 데이터 포맷인 JSON에 대해 알아볼 건데요.
그리고 노드제이에스(Node.js)를 통해 파일을 읽고 쓰는 셸 명령어를 직접 구현해 볼 겁니다.
JSON (제이슨) #
‘JSON(JavaScript Object Notation)‘은 데이터를 텍스트 형태로 표현(인코딩)하는 한 가지 방법인데요.
예를 들어 텍스트 파일에 데이터를 저장할 때 사용됩니다.
이름에서 알 수 있듯이 그 문법은 자바스크립트의 일부이기도 합니다.
다시 말해, 모든 JSON 데이터는 그 자체로 유효한 자바스크립트 코드(표현식)라는 의미거든요.
아래는 JSON 데이터가 담긴 텍스트 파일의 한 예시입니다.
{
"first": "Jane",
"last": "Porter",
"married": true,
"born": 1890,
"friends": [ "Tarzan", "Cheeta" ]
}
JSON의 문법 #
JSON의 문법은 다음과 같이 동작하는데요.
JSON은 null, 불리언, 숫자, 문자열과 같은 원시 값을 지원합니다.
단, 문자열은 반드시 ‘큰따옴표’로 감싸야 하고, NaN이나 Infinity 같은 에러 값은 지원하지 않습니다.
객체와 배열도 만들 수 있는데요.
객체 리터럴의 프로퍼티 키는 반드시 큰따옴표로 감싸야 하고, 배열과 객체 모두 마지막 요소 뒤에 쉼표를 붙이는 ‘트레일링 콤마’는 허용되지 않는다는 점이 중요합니다.
JSON.parse()로 JSON 파싱하기
#
JSON 파싱은 텍스트를 자바스크립트 값으로 변환하는 것을 의미하는데요.
JSON.parse() 함수를 사용하면 됩니다.
> JSON.parse('123')
123
> JSON.parse('"abc"')
'abc'
> JSON.parse('["dog", "cat"]')
[ 'dog', 'cat' ]
문자열을 인코딩하는 JSON 문자열을 파싱하는 것은 문자열 안에 또 다른 문자열이 포함되어 있기 때문에 흥미롭습니다.
JSON.stringify()로 자바스크립트 값을 JSON으로 변환하기
#
문자열화(Stringifying)는 반대로 자바스크립트 값을 텍스트로 변환하는 것을 의미하는데요.
JSON.stringify() 함수를 사용하면 됩니다.
> JSON.stringify(123)
'123'
> JSON.stringify('abc')
'"abc"'
> JSON.stringify(['dog', 'cat'])
'["dog","cat"]'
JSON.stringify()에 세 번째 인자로 숫자를 전달하면, 출력되는 JSON 문자열에 예쁘게 들여쓰기를 추가할 수 있거든요.
const obj = {first: 'Jane', last: 'Porter'};
console.log(JSON.stringify(obj, null, 2));
결과는 다음과 같습니다.
{
"first": "Jane",
"last": "Porter"
}
array.slice()
#
이 배열 메서드는 기존 배열의 일부를 복사해서 새로운 배열을 만드는 역할을 하는데요.
인자 없이 호출하면 배열 전체를 복사합니다.
인자를 하나만 전달하면 해당 인덱스부터 끝까지 복사하고요.
인자를 두 개 전달하면 첫 번째 인덱스부터 두 번째 인덱스 ‘전’까지 복사합니다.
노드제이에스에서 파일 읽고 쓰기 #
노드제이에스(Node.js)에는 파일 관련 작업을 처리하는 내장 모듈이 있는데요.
바로 node:fs 입니다.
fs.readFileSync() 함수는 파일 경로를 인자로 받아 그 내용을 문자열로 반환하고요.
fs.writeFileSync() 함수는 파일 경로와 문자열을 인자로 받아 텍스트 파일을 생성하거나 덮어씁니다.
프로젝트 item-store.js
#
item-store.js는 JSON 파일에 문자열을 추가하는 셸 명령어인데요.
item-store.js add <file-path> "string to add" 와 같은 형태로 사용합니다.
먼저, 주어진 경로의 JSON 파일을 읽어 자바스크립트 값으로 반환하는 함수를 만들어 보겠습니다.
const readData = (filePath) => {
try {
const json = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(json);
} catch (err) {
if (err instanceof Error && err.code === 'ENOENT') {
return { items: [] }; // 파일이 없으면 새 데이터로 시작
} else {
throw err;
}
}
};
만약 파일이 아직 존재하지 않는다면 에러가 발생하는데요.
이때 err.code가 ‘ENOENT’인지 확인해서, 파일이 없어서 발생한 에러가 맞다면 기본 데이터를 반환합니다.
다음은 데이터를 다시 JSON 파일로 쓰는 함수입니다.
const writeData = (filePath, data) => {
const json = JSON.stringify(data, null, 2);
fs.writeFileSync(filePath, json);
};
이제 이 함수들을 조합해서 셸 명령어의 핵심 로직을 완성해 볼까요?
const SUBCMD_ADD = 'add';
const main = () => {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(`item-store.js ${SUBCMD_ADD} <file-path> "string to add"`);
return;
}
const subcommand = args[0];
if (subcommand === SUBCMD_ADD) {
// ... 인자 개수 확인 로직 ...
const filePath = args[1];
const strToAdd = args[2];
const data = readData(filePath);
data.items.push(strToAdd);
writeData(filePath, data);
} else {
throw new Error('Unknown subcommand: ' + subcommand);
}
};
main();
process.argv.slice(2)를 사용해 실제 인자들만 args 배열에 담았는데요.
사용자가 add 서브커맨드를 입력하면, 파일을 읽고, items 배열에 새로운 문자열을 추가한 뒤, 다시 파일에 쓰는 과정을 거칩니다.
URL 객체와 import.meta.url
#
URL 클래스는 URL 주소를 다루는 데 아주 유용한 내장 클래스인데요.
URL의 각 부분을 쉽게 추출할 수 있고, 상대 참조를 기반 URL과 조합하는 기능도 제공합니다.
> new URL('../toc.html', 'http://example.com/book/chap/index.html').href
'http://example.com/book/toc.html'
그리고 import.meta.url은 현재 실행 중인 모듈의 URL을 문자열로 담고 있는 특별한 값인데요.
보통 file:///로 시작하는 파일 경로 URL을 가지고 있습니다.
프로젝트 random-quote-nodejs/
#
이 프로젝트는 JSON 파일에서 무작위로 명언을 하나 골라 터미널에 출력하는 셸 명령어인데요.
명언 데이터는 quotes.json 파일에 저장되어 있습니다.
random-quote-nodejs.js 파일과 quotes.json 파일은 같은 디렉토리에 위치하는데요.
이럴 때 import.meta.url을 사용하면 아주 편리하게 quotes.json 파일의 전체 경로를 알아낼 수 있습니다.
const fileUrl = new URL('quotes.json', import.meta.url);
이렇게 하면 셸 명령어를 어느 위치에서 실행하든 항상 정확한 quotes.json 파일의 경로를 찾을 수 있습니다.
나머지 코드는 이 fileUrl을 사용해 파일을 읽고, 파싱한 뒤, 무작위로 명언을 하나 골라 출력하는 간단한 로직으로 구성됩니다.
const main = () => {
const json = fs.readFileSync(fileUrl, 'utf-8');
const quotes = JSON.parse(json);
const randomIndex = /* ... */;
const randomQuote = quotes[randomIndex];
console.log(randomQuote.quote);
console.log('— ' + randomQuote.author);
};