๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐Ÿค Chapter 12: ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์‹ค์Šต

๐Ÿฆ„ ๋น…๋ฐ์ดํ„ฐ ๋ฐฐ์น˜ ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐโ€‹

  • ์‹ค์Šต์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด๋ฏ€๋กœ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฑ…์„ ์ฐธ๊ณ (P.340 ~ P.365)

๐Ÿ“š ๋…ธ๋“œ์ œ์ด์—์Šค์—์„œ ํ”„๋กœ๊ทธ๋žจ ๋ช…๋ น ์ค„ ์ธ์ˆ˜ ์ฝ๊ธฐโ€‹

export type FileNameAndNumber = [string, number];

export const getFileNameAndNumber = (
defaultFilename: string,
defaultNumberOfFakeData: number,
): FileNameAndNumber => {
const [bin, node, filename, numberOfFakeData] = process.argv;

return [
filename || defaultFilename,
numberOfFakeData ? parseInt(numberOfFakeData, 10) : defaultNumberOfFakeData,
];
};

๐Ÿ“š ํŒŒ์ผ ์ฒ˜๋ฆฌ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ํ”„๋กœ๋ฏธ์Šค๋กœ ๊ตฌํ˜„ํ•˜๊ฐ€โ€‹

๐ŸŽˆ fs.access API๋กœ ๋””๋ ‰ํ„ฐ๋ฆฌ๋‚˜ ํŒŒ์ผ ํ™•์ธํ•˜๊ธฐโ€‹

  • ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋‹ค ๋ณด๋ฉด ํŒŒ์ผ์ด๋‚˜ ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ํ˜„์žฌ ์žˆ๋Š”์ง€ ์—†๋Š”์ง€๋ฅผ ํ™•์ธํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์ƒ๊ธด๋‹ค.
import * as fs from 'fs';

export const fileExist = (
filepath: string,
): Promise<boolean> => new Promise((resolve) => fs.access(filepath, (error) => resolve(!error)));
  • fileExists-test.ts
import { fileExist } from '../fileApi/fileExists';

const exists = async (filepath) => {
const result = await fileExist(filepath);
console.log(`${filepath} ${result ? 'exists' : 'not exists'}`);
};

exists('./package.json');
exists('./package');

๐ŸŽˆ mkdirp ํŒจํ‚ค์ง€๋กœ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ƒ์„ฑ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐโ€‹

import mkdirp from 'mkdirp';

import { fileExist } from './fileExists';

export const mkdir = (dirname: string): Promise<string> => new Promise(async (resolve, reject) => {
const alreadyExists = await fileExist(dirname);

if (alreadyExists) {
resolve(dirname);
return;
}

mkdirp(dirname)
.then(() => resolve(dirname))
.catch((error) => reject(error));
});

๐ŸŽˆ rimraf ํŒจํ‚ค์ง€๋กœ ๋””๋ ‰ํ„ฐ๋ฆฌ ์‚ญ์ œ ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐโ€‹

import rimraf from 'rimraf';

import { fileExist } from './fileExists';

export const rmdir = (dirname: string): Promise<string> => new Promise(async (resolve, reject) => {
const alreadyExists = await fileExist(dirname);

if (!alreadyExists) {
resolve(dirname);
return;
}

rimraf(dirname, (error) => (error ? reject(error) : resolve(dirname)));
});

๐ŸŽˆ fs.writeFile API๋กœ ํŒŒ์ผ ์ƒ์„ฑํ•˜๊ธฐโ€‹

  • ๋…ธ๋“œ์ œ์ด์—์Šค ํ™˜๊ฒฝ์—์„œ ํŒŒ์ผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ฑฐ๋‚˜ ์“ธ ๋•Œ๋Š” ๋Œ€๋ถ€๋ถ„ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค.
  • ์ด๋•Œ ํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” ์œ ๋‹ˆ์ฝ”๋“œ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
import * as fs from 'fs';

export const writeFile = (
filename: string, data: any,
): Promise<any> => new Promise((resolve, reject) => {
fs.writeFile(filename, data, 'utf8', (error: Error) => {
if (error) {
reject(error);
return;
}

resolve(data);
});
});
  • writeFile-test.ts
import { writeFile } from '../fileApi/writeFile';
import { mkdir } from '../fileApi/mkdir';

const writeTest = async (filename: string, data: any) => {
const result = await writeFile(filename, data);
console.log(`write ${result} to ${filename}`);
};

mkdir('./data')
.then(() => writeTest('./data/hello.txt', 'hello world'))
.then(() => writeTest('./data/test.json', JSON.stringify({ name: 'Jack', age: 32 }, null, 2)))
.catch((e: Error) => console.log(e.message))

๐ŸŽˆ fs.readFile API๋กœ ํŒŒ์ผ ๋‚ด์šฉ ์ฝ๊ธฐโ€‹

import * as fs from 'fs';

export const readFile = (filename: string): Promise<any> => new Promise<any>((resolve, reject) => {
fs.readFile(filename, 'utf8', (error: Error, data: any) => {
if (error) {
reject(error);
return;
}

resolve(data);
});
});
  • readFile-test.ts
import { readFile } from '../fileApi/readFile';

const readTest = async (filename: string) => {
const result = await readFile(filename);
console.log(`read ${result} from ${filename} file.`);
};

readTest('./data/hello.txt')
.then(() => readTest('./data/test.json'))
.catch((e: Error) => console.log(e.message));

๐ŸŽˆ fs.appendFile API๋กœ ํŒŒ์ผ์— ๋‚ด์šฉ ์ถ”๊ฐ€ํ•˜๊ธฐโ€‹

import * as fs from 'fs';

export const appendFile = (
filename: string, data: any,
): Promise<any> => new Promise((resolve, reject) => {
fs.appendFile(filename, data, 'utf8', (error: Error) => {
if (error) {
reject(error);
return;
}

resolve(data);
});
});
  • appendFile-test.ts
import { appendFile } from '../fileApi/appendFile';
import { mkdir } from '../fileApi/mkdir';

const appendTest = async (filename: string, data: any) => {
const result = await appendFile(filename, data);
console.log(`append ${result} to ${filename}`);
};

mkdir('./data')
.then(() => appendTest('./data/hello.txt', 'Hi there!'))
.catch((e: Error) => console.log(e.message));
import * as fs from 'fs';

import { fileExists } from './fileExists';

export const deleteFile = (
filename: string,
): Promise<string> => new Promise<any>(async (resolve, reject) => {
const alreadyExists = await fileExists(filename);

if (!alreadyExists) {
resolve(filename);
return;
}

fs.unlink(filename, (error) => (error ? reject(error) : resolve(filename)));
});
  • deleteFile-test.ts
import { deleteFile } from '../fileApi/deleteFile';
import { rmdir } from '../fileApi/rmdir';

const deleteTest = async (filename: string) => {
const result = await deleteFile(filename);
console.log(`delete ${result} file.`);
};

Promise.all([deleteTest('./data/hello.txt'), deleteTest('./data/test.json')])
.then(() => rmdir('./data'))
.then((dirname) => console.log(`delete ${dirname} dir`))
.catch((e: Error) => console.log(e.message));

๐ŸŽˆ src/fileApi/index.ts ํŒŒ์ผ ๋งŒ๋“ค๊ธฐโ€‹

import { fileExists } from './fileExists';
import { mkdir } from './mkdir';
import { rmdir } from './rmdir';
import { writeFile } from './writeFile';
import { readFile } from './readFile';
import { appendFile } from './appendFile';
import { deleteFile } from './deleteFile';

export {
fileExists, mkdir, rmdir, writeFile, readFile, appendFile, deleteFile,
};

๐Ÿ“š ๊ทธ๋Ÿด๋“ฏํ•œ ๊ฐ€์งœ ๋ฐ์ดํ„ฐ ๋งŒ๋“ค๊ธฐโ€‹

export interface IFake {
name: string;
email: string;
sentence: string;
profession: string;
birthday: Date;
}
  • makeFakeData.ts
import { Chance } from 'chance';

import { IFake } from './IFake';

const c = new Chance();

export const makeFakeData = (): IFake => ({
name: c.name(),
email: c.email(),
profession: c.profession(),
birthday: c.birthday(),
sentence: c.sentence(),
});

export { IFake };
  • makeFakeData-test.ts
import { makeFakeData, IFake } from '../fake/makeFakeData';

const fakeData: IFake = makeFakeData();

console.log(fakeData);

๐Ÿ“š Object.keys์™€ Object.values ํ•จ์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐโ€‹

import { IFake, makeFakeData } from '../fake/makeFakeData';

const data: IFake = makeFakeData();
const keys = Object.keys(data);

console.log('keys: ', keys);

const values = Object.values(data);

console.log('values: ', values);

๐Ÿ“š CSV ํŒŒ์ผ ๋งŒ๋“ค๊ธฐโ€‹

  • ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ƒ์„ฑ
export function* range(max: number, min: number = 0) {
while (min < max) {
yield min++;
}
}
  • makeFakeData๋ฅผ ์‚ฌ์šฉํ•ด numberOfItems๋งŒํผ IFake ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์†์„ฑ๋ช…๊ณผ ์†์„ฑ๊ฐ’์˜ ๋ฐฐ์—ด์„ ๊ฐ๊ฐ ์ถ”์ถœํ•ด filename ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.
import * as path from 'path';

import { mkdir } from '../fileApi/mkdir';
import { range } from '../utils/range';
import { IFake } from './IFake';
import { makeFakeData } from './makeFakeData';
import { writeFile } from '../fileApi/writeFile';
import { appendFile } from '../fileApi/appendFile';

export const writeCsvFormatFakeData = async (
filename: string, numberOfItems: number,
) : Promise<string> => {
const dirname = path.dirname(filename);
await mkdir(dirname);

const comma = ',';
const newLine = '\n';

for (const n of range(numberOfItems)) {
const fake: IFake = makeFakeData();

if (n === 0) {
const keys = Object.keys(fake).join(comma);
await writeFile(filename, keys);
}

const values = Object.values(fake).join(comma);
await appendFile(filename, newLine + values);
}

return `write ${numberOfItems} items to ${filename} file`;
};

๐Ÿ“š ๋ฐ์ดํ„ฐ๋ฅผ CSV ํŒŒ์ผ์— ์“ฐ๊ธฐโ€‹

  • CSV ํŒŒ์ผ ํฌ๋งท์œผ๋กœ IFake ํƒ€์ž… ๊ฐ์ฒด๋ฅผ ์ €์žฅํ•˜๋Š” ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
import { getFileNameAndNumber } from './utils/getFileNameAndNumber';
import { writeCsvFormatFakeData } from './fake/writeCsvFormatFakeData';

const [filename, numberOfFakeData] = getFileNameAndNumber('./data/fake', 1);
const csvFilename = `${filename}-${numberOfFakeData}.csv`;

writeCsvFormatFakeData(csvFilename, numberOfFakeData)
.then((result) => console.log(result))
.catch((e: Error) => console.log(e.message));

๐Ÿ“š zip ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐโ€‹

  • CSV ํฌ๋งท ํŒŒ์ผ์„ ์ฝ๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ
  • ๊ฐ์ฒด์˜ ์†์„ฑ๋ช… ๋ฐฐ์—ด๊ณผ ์†์„ฑ๊ฐ’ ๋ฐฐ์—ด์„ ๊ฒฐํ•ฉํ•ด ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•œ๋ฐ ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ํ•จ์ˆ˜๋Š” ๋ณดํ†ต zip๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.
export const zip = (keys: string[], values: any[]) => {
const makeObject = (key: string, value: any) => ({ [key]: value });
const mergeObject = (a: any[]) => a.reduce((sum, val) => ({ ...sum, ...val }), {});

const tmp = keys
.map((key, index) => [key, values[index]])
.filter((a) => a[0] && a[1])
.map((a) => makeObject(a[0], a[1]));

return mergeObject(tmp);
};
  • zip-test.ts
import { zip } from '../utils';
import { makeFakeData, IFake } from '../fake';

const data = makeFakeData();
const keys = Object.keys(data);
const values = Object.values(data);

const fake: IFake = zip(keys, values) as IFake;

console.log(fake);

๐Ÿ“š CSV ํŒŒ์ผ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐโ€‹

  • ๋‹ค์Œ ์ฝ”๋“œ๋Š” 1,024Byte์˜ Buffer ํƒ€์ž… ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด ํŒŒ์ผ์„ 1,024Byte์”ฉ ์ฝ์œผ๋ฉด์„œ ํ•œ ์ค„์”ฉ ์ฐพ์€ ๋’ค, ์ฐพ์€ ์ค„(์ฆ‰, \n์œผ๋กœ ๋๋‚œ ์ค„)์˜ ๋ฐ์ดํ„ฐ๋ฅผ yield๋ฌธ์œผ๋กœ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์˜ˆ์ด๋‹ค.
import * as fs from 'fs';

function readLine(fd: any, buffer: Buffer, bufferSize: number, position: number): [string, number] {
let line = '';
let readSize;
const crSize = '\n'.length;

while (true) {
readSize = fs.readSync(fd, buffer, 0, bufferSize, position);

if (readSize > 0) {
const temp = buffer.toString('utf8', 0, readSize);
const index = temp.indexOf('\n');

if (index > -1) {
line += temp.substr(0, index);
position += index + crSize;
break;
} else {
line += temp;
position += temp.length;
}
} else {
position = -1; // end of file
break;
}
}

return [line.trim(), position];
}

export function* readFileGenerator(filename: string): any {
let fd: any;

try {
fd = fs.openSync(filename, 'rs');

const stats = fs.fstatSync(fd);
const bufferSize = Math.min(stats.size, 1024);
const buffer = Buffer.alloc(bufferSize + 4);
let filepos = 0;
let line;

while (filepos > -1) {
[line, filepos] = readLine(fd, buffer, bufferSize, filepos);

if (filepos > -1) {
yield line;
}
}

yield buffer.toString(); // ๋งˆ์ง€๋ง‰ ์ค„
} catch (error) {
console.log('readLine: ', error.message);
} finally {
fd && fs.closeSync(fd);
}
}
  • readFileGenerator-test.ts
  • readFileGenerator๋Š” ๋‹จ์ˆœํžˆ ํ•œ ์ค„ ํ•œ ์ค„ ์ฝ๋Š”๋‹ค.
import { readFileGenerator } from '../fileApi';

for (const value of readFileGenerator('data/fake-10000.csv')) {
console.log('<line>', value, '</line>');
break;
}

// <line> name,email,profession,birthday,sentence </line>
  • CSV ํŒŒ์ผ์„ ํ•ด์„ํ•˜๋ฉด์„œ ์ฝ๋Š” ์ฝ”๋“œ์ด๋‹ค.
import { readFileGenerator } from '../fileApi';
import { zip } from '../utils';

export function* csvFileReaderGenerator(filename: string, delim: string = ',') {
let header = [];

for (const line of readFileGenerator(filename)) {
if (!header.length) {
header = line.split(delim);
} else {
yield zip(header, line.split(delim));
}
}
}
  • readCsv.ts
import { getFileNameAndNumber } from './utils';
import { csvFileReaderGenerator } from './csv/csvFileReaderGenerator';

const [filename] = getFileNameAndNumber('./data/fake-10000.csv', 1);

let line = 1;

for (const object of csvFileReaderGenerator(filename)) {
console.log(`[${line++}] ${JSON.stringify(object)}`);
}

console.log('\n read complete.');

๐Ÿฆ„ ๋ชฝ๊ณ DB์— ๋ฐ์ดํ„ฐ ์ €์žฅํ•˜๊ธฐโ€‹

๐Ÿ“š ๋ชฝ๊ณ DB์— ์ ‘์†ํ•˜๊ธฐโ€‹

  • mongodb ํŒจํ‚ค์ง€๊ฐ€ ์ œ๊ณตํ•˜๋Š” MongoClient ๊ฐ์ฒด์˜ connect ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชฝ๊ณ DB์— ์ ‘์†
import { MongoClient } from 'mongodb';

export const connect = (mongoUrl: string = 'mongodb://localhost:27017') => MongoClient.connect(mongoUrl, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
  • ์ •์ƒ ์—ฐ๊ฒฐ
import { connect } from '../mongodb/connect';

const connectTest = async () => {
let connection;

try {
connection = await connect();
console.log('connection OK.', connection);
} catch (error) {
console.log(error.message);
} finally {
connection.close();
}
};

connectTest();

๐Ÿ“š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐโ€‹

const db = await connection.db('ch12-2');

๐Ÿ“š ์ปฌ๋ ‰์…˜์„ ์ƒ์„ฑโ€‹

const personsCollection = db.collection('persons');
const addressesCollection = db.collection('addresses');

๐Ÿ“š ๋ฌธ์„œ๋ฅผ ์ปฌ๋ ‰์…˜์— ์ €์žฅํ•˜๊ธฐโ€‹

const personsCollection = db.collection('persons');
const person = { name: 'Jack', age: 32 };

let result = await personsCollection.insertOne(person);

๐Ÿ“š ๋ฌธ์„œ ์ฐพ๊ธฐ, ๋ฌธ์„œ ์‚ญ์ œํ•˜๊ธฐ, ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ ฌโ€‹

  1. ๋ฌธ์„œ ์ฐพ๊ธฐ
// name ์†์„ฑ๊ฐ’์ด Jack์ธ ๋ฌธ์„œ ์ฐพ๊ธฐ
const cursor = personsCollection.find({ name: 'Jack' });
// ์ „์ฒด
const cursor = personsCollection.find({});
  1. ์กฐ๊ฑด์— ๋งž๋Š” ๋ฌธ์„œ ํ•˜๋‚˜๋งŒ ์ฐพ๊ธฐ
const result = await personsCollection.findOne({ _id });
  1. ๋ฌธ์„œ ์‚ญ์ œํ•˜๊ธฐ
let result = await personsCollection.deleteOne({ name: 'Tom' });
result = await personsCollection.deleteMany({});
  1. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์ •๋ ฌํ•˜๊ธฐ
const cursor = personsCollection.find({ name: 'Jack' }).sort({ age: -1 });
  • ์ปฌ๋ ‰์…˜์— ๋ฌธ์„œ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๊ฒ€์ƒ‰ ์‹œ๊ฐ„์ด ๋Š๋ ค์ง€๋Š”๋ฐ, ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ปฌ๋ ‰์…˜์— ์ธ๋ฑ์Šค๋ฅผ ๋งŒ๋“ค๊ฒŒ ๋œ๋‹ค.
// 1: ์˜ค๋ฆ„์ฐจ์ˆœ, -1: ๋‚ด๋ฆผ์ฐจ์ˆœ
await personsCollection.createIndex({ name: 1, age: -1 });

๐Ÿ“š CSV ํŒŒ์ผ ๋ชฝ๊ณ DB์— ์ €์žฅํ•˜๊ธฐโ€‹

  • ๋‹ค์Œ ์ฝ”๋“œ๋Š” CSV ํŒŒ์ผ์„ ์ฝ์–ด์„œ users๋ผ๋Š” ์ปฌ๋ ‰์…˜์— ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๊ณ , birthday์™€ name ์†์„ฑ์— ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋‚ด์šฉ์„ ๊ตฌํ˜„ํ•œ ์˜ˆ์ด๋‹ค.
import { connect } from './mongodb/connect';
import { csvFileReaderGenerator } from './csv/csvFileReaderGenerator';
import { getFileNameAndNumber } from './utils';

const insertCsvToMongo = async (csvFilename, collectionName, index) => {
let connection;

try {
connection = await connect();
const db = await connection.db('ch12-2');
const collection = db.collection(collectionName);
await collection.deleteMany({});
await collection.createIndex(index);

let line = 1;

for (const object of csvFileReaderGenerator(csvFilename)) {
await collection.insertOne(object);
console.log(`${line++} inserted.`);
}

console.log('\n insertion complete.');
} catch (error) {
console.log(error.message);
} finally {
connection.close();
}
};

const [filename] = getFileNameAndNumber('./data/fake-1000.csv', 1);
insertCsvToMongo(filename, 'users', { birthday: -1, name: 1 });

๐Ÿ“š limit์™€ skip ๋ฉ”์„œ๋“œโ€‹

  • users ์ปฌ๋ ‰์…˜์˜ ๋ฐ์ดํ„ฐ ์ค‘์—์„œ ๋‹ค์„ฏ ๊ฑด์„ ์–ป์–ด์™€ name๊ณผ birthday ์†์„ฑ๊ฐ’๋งŒ ํ™”๋ฉด์— ์ถœ๋ ฅํ•˜๋Š” ๋‚ด์šฉ์ด๋‹ค.
import { connect } from './mongodb/connect';
import { IFake } from './fake/IFake';

const findLimitSkip = async () => {
let connection;

try {
connection = await connect();
const db = await connection.db('ch12-2');
const usersCollection = db.collection('users');

const cursor = await usersCollection.find({})
.sort({ birthday: -1, name: 1 })
.skip(100)
.limit(5);

const result = await cursor.toArray();

console.log(result.map((user: IFake) => ({
name: user.name,
birthday: user.birthday,
})));
} catch (error) {
console.log(error.message);
} finally {
connection.close();
}
};

findLimitSkip();

๐Ÿฆ„ ์ต์Šคํ”„๋ ˆ์Šค๋กœ API ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐโ€‹

๐Ÿ“š ์ต์Šคํ”„๋ ˆ์Šค ํ”„๋ ˆ์ž„์›Œํฌโ€‹

  • ์ต์ŠคํŽ˜์ด์Šค ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์›น ์„œ๋ฒ„๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
import express from 'express';

const app = express();
const port = 4000;

app
.get('/', (req, res) => res.json({ message: 'Hello world!' }))
.listen(port, () => console.log(`http://localhost:${port} started...`));

๐Ÿ“š ๋ผ์šฐํŒ… ๊ธฐ๋Šฅ ๊ตฌํ˜„โ€‹

import express from 'express';

const app = express();
const port = 4000;

app
.get('/', (req, res) => res.json({ message: 'Hello world!' }))
.get('/users/:skip/:limit', (req, res) => {
const { skip, limit } = req.params;

res.json({ skip, limit });
})
.listen(port, () => console.log(`http://localhost:${port} started...`));

๐Ÿ“š ์ต์Šคํ”„๋ ˆ์Šค ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€โ€‹

  • REST ๋ฐฉ์‹์˜ API ์„œ๋ฒ„๋“ค์€ ์›น ํŽ˜์ด์ง€์˜ ๋ณธ๋ฌธ ๋‚ด์šฉ์„ ๋ถ„์„ํ•˜๋ ค๊ณ  ํ•  ๋•Œ bodyParser์™€ cors๋ผ๋Š” ํŒจํ‚ค์ง€๋ฅผ use ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค์Œ์ฒ˜๋Ÿผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.
import bodyParser from 'body-parser';
import cors from 'cors';

app
.use(bodyParser.urlencoded({ extended: true }))
.use(cors())

๐Ÿ“š ๋ชฝ๊ณ DB ์—ฐ๊ฒฐโ€‹

  • ๋ชฝ๊ณ DB ์„œ๋ฒ„์— ์ ‘์†ํ•˜๋Š” ์ฝ”๋“œ
import { runServer } from './runServer';
import { connect } from './mongodb/connect';

connect()
.then(async (connection) => {
const db = await connection.db('ch12-2');
return db;
})
.then(runServer)
.catch((e: Error) => console.log(e.message));
  • runServer.ts
import cors from 'cors';
import express from 'express';
import bodyParser from 'body-parser';

export const runServer = (mongodb) => {
const app = express();
const port = 4000;

app
.use(bodyParser.urlencoded({ extended: true }))
.use(cors())
.get('/', (req, res) => res.json({ message: 'Hello world!' }))
.get('/users/:skip/:limit', async (req, res) => {
const { skip, limit } = req.params;

const usersCollection = await mongodb.collection('users');
const cursor = await usersCollection
.find({})
.sort({ name: 1 })
.skip(parseInt(skip, 10))
.limit(parseInt(limit, 10));

const result = await cursor.toArray();

res.json(result);
})
.listen(port, () => console.log(`http://localhost:${port} started...`));
};

๐Ÿฆ„ ๋ฆฌ์•กํŠธ์™€ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ์œผ๋กœ ํ”„๋ŸฐํŠธ์—”๋“œ ์›น ๋งŒ๋“ค๊ธฐโ€‹

  • ./frontend ํด๋” ์ฐธ๊ณ 

๐Ÿ“š App.tsx ํŒŒ์ผ ์ˆ˜์ •โ€‹

import React from 'react';

const App: React.FC = () => {
const user = {
name: 'Jack',
age: 32,
}

return (
<div className="App">{
JSON.stringify(user)
}</div>
)
}

export default App;

๐Ÿ“š API ์„œ๋ฒ„์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐโ€‹

export interface IUser {
_id: string;
name: string;
email: string;
sentence: string;
profession: string;
birthday: string;
}
  • getDataPromise.ts
import { IUser } from './IUser';

type GetDataPromiseCallback = (a: IUser[]) => void;

export const getDataPromise = (fn: GetDataPromiseCallback) => (
skip: number,
limit: number,
) => fetch(`http://localhost:4000/users/${skip}/${limit}`)
.then((res) => res.json())
.then(fn);
  • App.tsx
import React, { useState, useEffect } from 'react';

import { getDataPromise } from './getDataPromise';
import { IUser } from './IUser';

const App: React.FC = () => {
const [users, setUsers] = useState<IUser[]>([]);

useEffect(() => {
getDataPromise((receivedUsers: IUser[]) => {
setUsers([...users, ...receivedUsers]);
})(0, 1);
}, []);

return (
<div className="App">{JSON.stringify(users)}</div>
)
}

export default App;

๐Ÿ“š ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ ๊ณ„์† ๊ฐ€์ ธ์˜ค๊ธฐโ€‹

import React, { useState, useEffect } from 'react';

import { getDataPromise } from './getDataPromise';
import { IUser } from './IUser';

const App: React.FC = () => {
const [skip, setSkip] = useState(0);
const [users, setUsers] = useState<IUser[]>([]);

const limit = 1;
const onClick = () => {
getDataPromise((receivedUsers: IUser[]) => {
setSkip(skip + limit);
setUsers([...users, ...receivedUsers]);
})(skip, limit);
}
useEffect(onClick, []);

return (
<div className="App">
<p>
<button onClick={onClick}>more data...</button>
</p>
<p>{JSON.stringify(users)}</p>
</div>
)
}

export default App;

๐Ÿ“š ๋ถ€ํŠธ์ŠคํŠธ๋žฉ CSS ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉํ•˜๊ธฐโ€‹

๐Ÿ“š ์นด๋“œ ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐโ€‹

  • Card.tsx
import React from 'react';

import { IUser } from './IUser';

const random = (max: number) => Math.floor(Math.random() * max);

const Card: React.FC<{ user: IUser, click: () => void }> = ({user, click}) => {
const { name, email, sentence, profession, birthday } = user;
const b = new Date(birthday);
const src = `https://source.unsplash.com/random/1000x${random(300) + 500}`;

return (
<div className="card">
<img src={src} className="card-img-top" />
<div className="card-body">
<h5 className="card-title">{name}({email})</h5>
<h6 className="card-subtitle mb-2 text-muted">
{profession} birthday: {b.getFullYear()}
</h6>
<p className="card-text">{sentence}</p>
<a href="#" className="btn btn-primary" onClick={click}>more data...</a>
</div>
</div>
);
};

export default Card;
  • App.tsx
import React, { useState, useEffect } from 'react';

import { getDataPromise } from './getDataPromise';
import { IUser } from './IUser';

import Card from './Card';

const App: React.FC = () => {
const [skip, setSkip] = useState(0);
const [users, setUsers] = useState<IUser[]>([]);

const limit = 1;
const onClick = () => {
getDataPromise((receivedUsers: IUser[]) => {
setSkip(skip + limit);
setUsers([...users, ...receivedUsers]);
})(skip, limit);
}
useEffect(onClick, []);

return (
<div className="App">
{users.map((user: IUser, key: number) => (
<Card click={onClick} user={user} key={key.toString()} />
))}
</div>
)
}

export default App;