DevFest WebTech CodeLab
DevFest WebTech CodeLab

DevFest WebTech CodeLab

์ด ์ฝ”๋“œ๋žฉ์€ DevFest WebTech 2019 ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ์Šต๋‹ˆ๋‹ค. ํ•ด๋งˆ๋‹ค ์กฐ๊ธˆ์”ฉ ๋ณ€ํ™”ํ•˜๊ณ  ์žˆ๋Š” JavaScript์˜ ์ƒˆ๋กœ์šด ๋ช…์„ธ๋ฅผ ์•Œ์•„๋ณด๊ณ  ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ ๋ฐ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„์ง€ ํ•จ๊ป˜ ์•Œ์•„๋ด…์‹œ๋‹ค!

CodeLab ๊ณผ์ •

์ด CodeLab์€ ์•„๋ž˜์™€ ๊ฐ™์€ ์ˆœ์„œ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.
  1. ES6์™€ ES7์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๊ธฐ
  1. ES8, ES9, ES10์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๊ธฐ
  1. ES6~10์„ ํ™œ์šฉํ•ด ๊ฐ„๋‹จํ•œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋งŒ๋“ค์–ด๋ณด๊ธฐ feat. RxJS(option)
๊ฐ ์„ธ์…˜์€ ๋…๋ฆฝ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋‹ˆ ๊ผญ ์ˆœ์„œ๋Œ€๋กœ ๋“ฃ์ง€ ์•Š์œผ์…”๋„ ๋˜๊ณ , ์›ํ•˜๋Š” ๊ฒƒ๋งŒ ๋“ค์œผ์…”๋„ ๋ฌด๋ฐฉํ•ฉ๋‹ˆ๋‹ค.

ECMAScript?

ํ˜„์žฌ ์šฐ๋ฆฌ๊ฐ€ ์“ฐ๊ณ ์žˆ๋Š” JavaScript๋Š” ์ •๋ณด์™€ ํ†ต์‹  ์‹œ์Šคํ…œ์„ ์œ„ํ•ด ์กด์žฌํ•˜๋Š” ๊ตญ์ œ ํ‘œ์ค€ํ™” ๊ธฐ๊ตฌ Ecma International์˜ ECMA-262 ๊ธฐ์ˆ  ๊ทœ๊ฒฉ์— ์ •์˜ ๋ฐ ํ‘œ์ค€ํ™”๋œ ECMAScript์™€ BOM(Browser Object Model) ๊ทธ๋ฆฌ๊ณ  DOM(Document Object Model) ์ด ์„ธ ๊ฐ€์ง€๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์–ด์š”. ์ด ์ค‘ ECMAScript๋Š” JavaScript์˜ ํ‘œ์ค€ ๊ทœ๊ฒฉ์„ ๋‹ด๋‹นํ•˜๊ณ  2015๋…„๋ถ€ํ„ฐ ์•ฝ 1๋…„์„ ์ฃผ๊ธฐ๋กœ ๋ช…์„ธ๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ์ถ”๊ฐ€ ๋ฐ ์ˆ˜์ •๋˜๋ฉด์„œ ์—…๋ฐ์ดํŠธ ๋˜๊ณ ์žˆ์–ด์š”.

ES6 & ES7์—์„œ ๋ฌด์—‡์ด ๋‹ฌ๋ผ์กŒ๋‚˜์š”?

ES6๋ถ€ํ„ฐ ํ˜„์žฌ 2019๋…„๋„์— ๋‚˜์˜จ ES10๊นŒ์ง€ ECMAScript์˜ ๋ฒ„์ „ ์ค‘ ES6์—์„œ ๊ฐ€์žฅ ๋งŽ์€ ๋ณ€ํ™”๊ฐ€ ์ƒ๊ฒผ์–ด์š”. ์•ฝ 20๊ฐœ ์ด์ƒ์˜ ๋ช…์„ธ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ๋Š”๋ฐ ๊ทธ ์ค‘ ๋Œ€ํ‘œ์ ์ธ ๋ณ€ํ™”๋ฅผ ์•Œ์•„๋ณด๊ณ  ํ”„๋กœ๋•ํŠธ์—์„œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์„ ์ง์ ‘ ํ™œ์šฉํ•ด ๋ด…์‹œ๋‹ค.

ES6 โ†’ http://www.ecma-international.org/ecma-262/6.0/

Arrow functions / Arrow function expressions

โ†’ Regular function ๋Œ€์‹  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ๋ฒ•์ ์œผ๋กœ ๊ฐ„์†Œํ™”๋œ ํ•จ์ˆ˜
โ†’ ์ฃผ์˜ํ•  ์ : ์ด Arrow function์€ regular function ๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ this, arguments, super, new.target keyword์— ๋Œ€ํ•œ ๋ฐ”์ธ๋”ฉ์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
// ๋‘ ํ•จ์ˆ˜์˜ ์ƒ๊น€์ƒˆ // Regular Function function generalFunction(a, b) { // ... } // Arrow Function const arrowFunction = (a, b) => { // ... }

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

const cats = ['ebony', 'ivory', 'harmony', 'rhythm'] // Statement body cats.forEach((cat) => { console.log(cat) registerCat(cat) }) // Expression body const wrappedCats = cats.map((catName, index) => ({catName, age: index})) const olderCats = wrappedCats.filter((cat) => cat.age > 1) const catHarmony = olderCats.find((cat) => cat.catName === 'harmony')
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Classes

โ†’ Syntactical sugar over JavaScript's existing prototype-based inheritance
โ†’ JavaScript์— ์ƒˆ๋กœ์šด Object Oriented Pattern์„ ์ œ์‹œํ•œ ๊ฑด ์•„๋‹ˆ์—์š”!
โ†’ ์ฃผ์˜ํ•  ์ : Class๋Š” Function declaration ์ฒ˜๋Ÿผ hoisted ๋˜์ง€ ์•Š์•„์š”.
class nameOfClass extends existingClass { constructor() { // ... } someFunction() { // ... } get someVariable() { // ... } set someVariable(variable) { // ... } static someStaticFunction() { // ... } }

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

class GDGEvent { constructor(eventName, eventDate) { this.eventName = eventName this.eventDate = eventDate } updateEvent(eventName, eventDate) { // ... } } class GDGWebTechEvent extends GDGEvent { constructor(eventName, eventDate, focusingTopic) { super(eventName, eventDate) this.focusingTopic = focusingTopic this.eventPlace = GDGWebTechEvent.defaultEventPlace() } updateEvent(eventName, eventDate, focusingTopic) { // do something with focusingTopic super.updateEvent(eventName, eventDate) } get eventInformation() { return `${this.eventName} will be held at ${this.eventDate} focusing on ${this.focusingTopic}` } static defaultEventPlace() { return 'Google for Startups located in Seoul, Korea' } } const devFestWebTech2019 = new GDGWebTechEvent( 'DevFest WebTech', '21 Nov 2019', 'web technologies' ) console.log(devFestWebTech2019.eventInformation) // 'DevFest WebTech will be held at 21 Nov 2019 focusing on web technologies'
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Enhanced Object Literals

โ†’ Object Literals ๊ฐ€ ์•„๋ž˜์˜ ๋‚ด์šฉ์„ ์ง€์›ํ•˜๋„๋ก ํ™•์žฅ๋˜์—ˆ์–ด์š”.
โ†’ setting the prototype at construction
โ†’ shorthand for foo: foo assignments
โ†’ defining methods(+making super calls)
โ†’ computing property names with expressions
const exampleObjectLiteral = { // __proto__ __proto__: theProtoObj, // Shorthand for โ€˜handler: handlerโ€™ handler, // Methods toString() { // Super calls return "d " + super.toString() }, // Computed (dynamic) property names [ 'prop_' + (() => 42)() ]: 42 }

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

const height = 30 const width = 60 const weight = '3kg' const color = 'ivory' const myCat = { height, width, weight, color, [`computed${height * width}`]: height * width }
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Template Strings

โ†’ string literals allowing embedded expressions

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Basic literal string creation const example1 = 'string '/n' text' // Multiline strings const example2 = `string text line 1 string text line 2` // String interpolation const exmaple3 = `string text ${example1} string text`
// Tagged templates const cat = 'Mambo' const age = 8 function simpleTag(strings, catExp, ageExp) { const firstString = strings[0] const secondString = strings[1] return `${firstString}${catExp}${secondString}${ageExp > 7 ? 'old' : 'not old'}` } const catString = simpleTag`My cat ${ cat } is ${ age }` console.log(catString)
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Destructuring

โ†’ binding using pattern matching

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

const exampleObject = { animal: { cat: ['Navy', 'Black', 'Yello'], dog: ['Joy', 'Sam'], }, car: { sedan: { bmw: ['528i', '530i'], mercedes: ['E300', 'S350D'], }, suv: { bmw: ['X3', 'X5'], mercedes: ['GLE', 'GLC'], } } } // list matching const [a, , b] = [1,2,3] // object matching const { animal: a, car: { sedan: s } } = exampleObject // object matching shorthand const {animal, car} = exampleObject // Can be used in parameter position function exampleFunction({name: awesomeName}) { console.log(awesomeName) } exampleFunction({name: 'Duke'}) // Fail-soft destructuring const [a] = [] a === undefined // Fail-soft destructuring with defaults const [a = 1] = [] a === 1
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Default parameters & Rest parameters & Spread Operator

โ†’ Default parameters: ํ•จ์ˆ˜์˜ ์„ ์–ธ๋ถ€์—์„œ ํŒŒ๋ฆฌ๋ฏธํ„ฐ์˜ ๊ธฐ๋ณธ ๊ฐ’์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ
โ†’ Rest parameters: ํ•จ์ˆ˜์˜ ์„ ์–ธ๋ถ€์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ผ๋ถ€๋ฅผ ํ•˜๋‚˜์˜ ๋ฐฐ์—ด๋กœ ์ „๋ถ€ ๋ฐ›์Œ
โ†’ Spread Operator: ๋ฐฐ์—ด๊ณผ ๋ฌธ์ž์—ด์˜ ์š”์†Œ๋ฅผ ์ „๋ถ€ ํ’€์–ด ํ•จ์ˆ˜์˜ ์ธ์ˆ˜๋‚˜ ๋ฐฐ์—ด์˜ ์š”์†Œ๋กœ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Œ

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Default parameters function interpretAge(age = 1) { if (age < 3) { return 'baby' } return '์œผ๋ฅธ' } console.log(interpretAge()) // baby console.log(interpretAge(1)) // baby console.log(interpretAge(5)) // ์œผ๋ฅธ // Rest parameters function organizeRoom(clothes, chairs, ...others) { console.log(clothes) // ['Jeans', 'Shirts'] console.log(chairs) // ['a', 'b'] console.log(others) // [['Book1', 'Book2'], ['Note1']] } organizeRoom(['Jeans', 'Shirts'], ['a', 'b'], ['Book1', 'Book2'], ['Note1']) // Spread Operator function sumNumbers(x, y, z, s) { return x + y + z + s } const numbers = [1, 2, 5, 6] sumNumbers(...numbers) // 14
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Let & Const

โ†’ Block scope๊ฐ€ ์ ์šฉ๋˜๋Š” ๋ณ€์ˆ˜
โ†’ ์ฃผ์˜ํ•  ์ : var keyword๋กœ ์„ ์–ธํ• ๋•Œ ์ฒ˜๋Ÿผ let ๋ฐ const ๋กœ ์„ ์–ธ๋˜๋Š” ๋ณ€์ˆ˜๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ window ๊ฐ์ฒด์˜ ํ”„๋กœํผํ‹ฐ๋กœ ๋“ฑ๋ก๋˜์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ๊ณต๊ฐ„์— ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. var๋Š” Object Environment Record, let๊ณผ const๋Š” Declarative Environment Record ๋กœ์„œ ์„œ๋กœ ๋‹ค๋ฅด๊ฒŒ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// ์ผ๋ฐ˜ ํ•จ์ˆ˜์—์„œ์˜ ์“ฐ์ž„์ƒˆ function f() { { let x { // ๋‹ค๋ฅธ ๋ธ”๋ก ์Šค์ฝ”ํ”„๋ผ์„œ ๋ฌธ์ œ์—†์ด ์‹คํ–‰ const x = 'sneaky' // ๊ฐ™์€ ๋ธ”๋ก ์Šค์ฝ”ํ”„์—์„œ const ๋ณ€์ˆ˜๋ฅผ ์žฌํ• ๋‹นํ•˜๋ฏ€๋กœ error x = 'foo' } // ์ด๋ฏธ ๊ฐ™์€ ๋ธ”๋ก ์Šค์ฝ”ํผ์— ๋™์ผํ•œ ์ด๋ฆ„์˜ let ๋ณ€์ˆ˜๊ฐ€ ์„ ์–ธ๋˜์–ด ์žˆ์–ด error let x = 'inner' } } // ๊ณ ์ „์ ์ด๋ฉด์„œ๋„ ์œ ๋ช…ํ•œ ์˜ˆ for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i) }, 3000) } // ์—ด๋ฒˆ ๋ชจ๋‘ 10 // Versus for (let i = 0; i < 10; i++) { setTimeout(() => { console.log(i) }, 3000) } // 0 ~ 9
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Modules

โ†’ Language-level support for modules for component definition
โ†’ ๋“œ๋””์–ด ์–ธ์–ด ๋ ˆ๋ฒจ์—์„œ ์ง€์›๋˜๋Š” import, export ๊ตฌ๋ฌธ
โ†’ ์ฃผ์˜ํ•  ์ : ์•„์ง ๋ธŒ๋ผ์šฐ์ €์—์„œ ์™„์ „ํžˆ ์ง€์›ํ•˜์ง€๋Š” ์•Š์•„์„œ script tag์˜ type ์†์„ฑ์— module ์„ ์ง€์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// math.js export const sum = (x, y) => x + y export const subtract = (x, y) => x - y export const multiply = (x, y) => x * y export const PI = 3.141593 export default { sum, subtract, multiply, PI, } // app.js import math from 'math' console.log('2ฯ€ = ' + math.sum(math.PI, math.PI)) // or import { sum, multiply, PI } from 'math' console.log(multiply(sum(3, 8), sum(2, 10)) / PI) // 42.01...
<script type="module" src="app.js"></script>
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Array API: from, of, fill, find ...

โ†’ Array์— ์—ฌ๋Ÿฌ conversion helpers๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์–ด์š”!

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Array-like DOM Nodes๋ฅผ ์‹ค์ œ ๋ฐฐ์—ด ํƒ€์ž…์œผ๋กœ ๋ž˜ํ•‘ Array.from(document.querySelectorAll('*')) Array.of(1, 2, 3) // 7๋กœ 1๋ฒˆ์งธ๋ถ€ํ„ฐ ์ฑ„์šฐ๊ธฐ [0, 0, 0].fill(7, 1) // [0, 7, 7] // ํŠน์ • ์š”์†Œ ๋ฐ ์ธ๋ฑ์Šค ์ฐพ๊ธฐ [1, 2, 3].find(x => x === 3) // 3 [1, 2, 3].findIndex(x => x === 2) // 1
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Promise

โ†’ JavaScript์˜ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ์ผ๊ด€์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋‹ค๊ฐ€ ES6์— ์ด๋ฅด๋Ÿฌ์„œ JavaScript ๋ช…์„ธ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์–ด์š”.
new Promise((resolve, reject) => { ... })

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// ํƒ€์ด๋จธ function timeout(duration = 0) { return new Promise((resolve) => { setTimeout(resolve, duration); }) } const promiseExample = timeout(1000) .then(() => { return timeout(2000) }) .then(() => { throw new Error('hmm') }) .catch((error) => { Promise.all([timeout(100), timeout(200)]) return '๋งˆ์ด ๊ธฐ๋‹ค๋ ธ๋‹ค..' }) // API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ fetch('https://awesome.com/api/cats', { method: 'GET' }) .then((response) => response.json()) .then(({status, data}) => { // ... })
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

ES7 โ†’ http://www.ecma-international.org/ecma-262/7.0/

Array.prototype.includes

โ†’ ๊ธฐ์กด์˜ indexOf ๋Š” ํŠน์ • ์š”์†Œ๊ฐ€ ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•˜๊ณ  0 ๋˜๋Š” 1์„ ๋ฐ˜ํ™˜ํ–ˆ์ง€๋งŒ, includes ๋Š” ๋™์ผํ•˜๊ฒŒ ์š”์†Œ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ  boolean์„ ๋ฐ˜ํ™˜ํ•ด์š”. ๊ทธ๋ž˜์„œ number type์„ boolean type์œผ๋กœ conversion ํ•˜๋Š” ์ผ์„ ์•ˆํ•˜๊ฒŒ ๋˜์—ˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// arrayVariable.includes(valueToFind[, fromIndex]) [1, 2, 3].includes(-1) // false [1, 2, 3].includes(1) // true [1, 2, 3].includes(3, 4) // false [1, 2, 3].includes(3, 3) // false [1, 2, NaN].includes(NaN) // true ['foo', 'bar', 'quux'].includes('foo') // true ['foo', 'bar', 'quux'].includes('norf') // false
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Arithmetic Operators: Exponentiation(**)

โ†’ ์ˆ˜๋ฅผ ์ œ๊ณฑํ•˜๋Š” Operator๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// 5์˜ ์„ธ์ œ๊ณฑ์„ ๊ตฌํ• ๋•Œ ๊ธฐ์กด์˜ ๋ฐฉ๋ฒ• Math.pow(5, 3) // ์ด์ œ๋Š” operand ** operand ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š” 5 ** 3 // 125
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

ES8 & ES9์—์„œ ๋ฌด์—‡์ด ๋‹ฌ๋ผ์กŒ๋‚˜์š”?

ES8 โ†’ http://www.ecma-international.org/ecma-262/8.0/

Async function

โ†’ async๋ฅผ ๋ถ™์ด๋Š” ๊ฒƒ์„ ์ œ์™ธํ•˜๊ณ ๋Š” Regular function๊ณผ ๊ตฌ์กฐ์ ์œผ๋กœ ๋™์ผํ•˜์ง€๋งŒ ์•”๋ฌต์ ์œผ๋กœ Async function์€ Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์ด ํ•จ์ˆ˜ ๋‚ด์—์„œ๋Š” await operator๋กœ Promise ํ˜น์€ Thenable objects๋ฅผ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ fulfilled ๋œ ๊ฐ’์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”.
async function asynchronousFunction() { return 3 } const resultAsPromise = asynchronousFunction()

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• async function getFive() { return 5 } getFive().then((value) => { console.log(value) }) // or async function getFive() { return 5 } async function waitForFive() { const value = await getFive() console.log(value) } waitForFive() // Thenable objects๋ฅผ ๊ธฐ๋‹ค๋ฆด๋•Œ async function waitForThenable() { const thenable = { then: function(resolve, reject) { resolve('resolved!') } } console.log(await thenable) // resolved! } waitForThenable() // API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ // Promise๋กœ ํ–ˆ์„๋•Œ(ES6์—์„œ์˜ ์˜ˆ์ œ์™€ ๋™์ผ) fetch('https://awesome.com/api/cats', { method: 'GET' }) .then((response) => response.json()) .then(({status, data}) => { // ... }) // Async function๊ณผ await์„ ์‚ฌ์šฉํ–ˆ์„๋•Œ async function apiHandler() { const response = await fetch('https://awesome.com/api/cats', { method: 'GET' }) const { data } = await response.json() console.log(data) } apiHandler()
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Object entries, values

โ†’ Object์˜ enumerable property pair๋ฅผ ๊บผ๋‚ด์˜ค๊ฑฐ๋‚˜ value๋งŒ ๋ฐฐ์—ด๋กœ ๊บผ๋‚ด์˜ฌ ์ˆ˜ ์žˆ์–ด์š”.
Object.entries(objectVariable) Object.values(objectVariable)

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Object.entries() const obj = { eventName: 'DevFest WebTech', eventPlace: 'Google for Startups' } console.log(Object.entries(obj)) // [ ['eventName', 'DevFest WebTech'], ['eventPlace', 'Google for Startups'] ] // Object.values() const obj = { foo: 'foo', bar: [100, 200], baz: 55 } console.log(Object.values(obj)) // ['foo', [100, 200], 55 ] const myStr = 'Lufthansa' console.log(Object.values(myStr)) // ["L", "u", "f", "t", "h", "a", "n", "s", "a"]
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Trailing commas in function declarations and calls

โ†’ Trailing commas๋ฅผ ํ•จ์ˆ˜ ์„ ์–ธ๋ถ€์™€ ํ˜ธ์ถœ๋ถ€์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”.
โ†’ ์ด ๋ฌธ๋ฒ•์œผ๋กœ ์ด์ œ๋Š” Array, Object, Function ๋“ฑ์„ ํ™œ์šฉํ•จ์— ์žˆ์–ด์„œ ์‹ค์ œ ํ”„๋กœ๋•ํŠธ๋ฅผ ๋งŒ๋“ค๋•Œ ์ž์ฃผ ํ•˜๋Š” ์—ด๊ฑฐํ˜• ๊ฐ’ ๋ณต์‚ฌ๋ฅผ Syntax Error๋‚˜ Code Convention(ESLint ํ˜น์€ Prettier์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด) ๋“ฑ์„ ๊ฑฑ์ •ํ•˜์ง€ ์•Š๊ณ  ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”!
// ES5์—์„œ๋„ legalํ•œ ๋ฌธ๋ฒ•์ด์—ˆ๋˜ trailing commas in objects const arr = [ 1, 2, 3, ]; const exampleObject = { foo: "bar", baz: "qwerty", age: 42, }; // ์ด์ œ๋Š” ํ•จ์ˆ˜์˜ ์„ ์–ธ๋ถ€์™€ ํ˜ธ์ถœ๋ถ€์—์„œ๋„ legal! const awesomeFunction = ( a, b, c, d, ) => a + b + c + d const a = 3 const b = 22 const c = 1 const d = 5 awesomeFunction(a, b, c, d,)
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

ES9 โ†’ http://www.ecma-international.org/ecma-262/9.0/

Object rest & spread

โ†’ ์•ž์„œ ์„ค๋ช…ํ–ˆ๋˜ Array rest์™€ spread ์—ฐ์‚ฐ์ž๊ฐ€ Object์—๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Object rest const { fname, lname, ...rest } = { fname: 'Hemanth', lname: 'HM', location: 'Earth', type: 'Human', } fname // "Hemanth" lname // "HM" rest // { location: "Earth", type: "Human" } // Object spread const rest = { location: 'Earth', type: 'Human', } const info = { fname, lname, ...rest } info // { fname: "Hemanth", lname: "HM", location: "Earth", type: "Human" }
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Promise.prototype.finally

โ†’ ์•ž์„œ ์„ค๋ช…ํ–ˆ๋˜ Promise์— then, catch ๋‹ค์Œ์œผ๋กœ finally ๊ตฌ๋ฌธ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”. ์ด์   then๊ณผ catch์—์„œ ํ–ˆ๋˜ clean up ์ž‘์—…์„ finally ๊ตฌ๋ฌธ์—์„œ ์ผ๊ด„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

function testFinally() { return new Promise((resolve,reject) => resolve()) } testFinally() .then(() => console.debug('resolved')) .finally(() => console.debug('finally')) // resolved // finally
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

ES10 โ†’ http://www.ecma-international.org/ecma-262/10.0/

Array flat & flatMap

โ†’ Array์— ๋ฐฐ์—ด์˜ ๋ฐฐ์—ด์„ flat ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” conversion helper๊ฐ€ ์ถ”๊ฐ€ ๋˜์—ˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// Array flat let arr = ['a', 'b', ['c', 'd']] let flattened = arr.flat() console.log(flattened) // ["a", "b", "c", "d"] arr = ['a', , , 'b', ['c', 'd']]; flattened = arr.flat() console.log(flattened) // ["a", "b", "c", "d"] arr = [10, [20, [30]]] console.log(arr.flat()) // [10, 20, [30]] console.log(arr.flat(1)) // [10, 20, [30]] console.log(arr.flat(2)) // [10, 20, 30] // Array flatMap const arr = [4.25, 19.99, 25.5] console.log(arr.map((value) => [Math.round(value)])) // [[4], [20], [26]] console.log(arr.flatMap((value) => [Math.round(value)])) // [4, 20, 26]
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Object fromEntries

โ†’ ์ด์ „์— ์„ค๋ช…ํ–ˆ์—ˆ๋˜ Object์˜ entries()๋กœ ๋ฆฌํ„ฐ๋Ÿด ๊ฐ์ฒด์˜ property pair๋ฅผ ๋ฐฐ์—ด๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์—ˆ์–ด์š”. ๊ทธ๋Ÿฐ๋ฐ ๋ฐ˜๋Œ€๋กœ ๊ทธ ๋ฐฐ์—ด๋กœ ๋ฆฌํ„ฐ๋Ÿด ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๊ฐ€ ์ƒ๊ฒผ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

const myArray = [['one', 1], ['two', 2], ['three', 3]] const obj = Object.fromEntries(myArray) console.log(obj) // {one: 1, two: 2, three: 3}
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

String trimStart & trimEnd

โ†’ ์‚ฌ์‹ค ์ด String์˜ ํ•จ์ˆ˜๋“ค์€ ๊ฐ๊ฐ trimLeft์™€ trimRight์™€ ๊ฐ™์•„์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

const str = ' string ' // ES10 console.log(str.trimStart()) // "string " console.log(str.trimEnd()) // " string" // the same as console.log(str.trimLeft()) // "string " console.log(str.trimRight()) // " string"
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

Optional catch binding

โ†’ ์ด์ œ catch binding์„ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”.

์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

// ์ด์ œ ์ด๋ ‡๊ฒŒ catch๊ฐ€ ์–ด๋–ค Error ๊ฐ์ฒด๋ฅผ ๋ฐ›๋˜์ง€ ์ƒ๋žตํ•˜๊ณ  ํ•„์š”ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”! try { // ์—๋Ÿฌ๊ฐ€ ๋‚  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ์ž‘์—…๋“ค์„ ์ˆ˜ํ–‰ } catch { // ์—๋Ÿฌ์˜ ์ข…๋ฅ˜์— ์ƒ๊ด€์—†์ด ํ•„์š”ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ }
์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

๊ฐ„๋‹จํ•œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋งŒ๋“ค์–ด๋ณด๊ธฐ feat. RxJS

์ค€๋น„๋ฌผ

  • Editor(or IDE)
  • npm
  • Browser(Chrome์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค)
ย 

๋ฌด์—‡์„ ๋งŒ๋“œ๋‚˜์š”?

์ด๋ฒˆ ์„ธ์…˜์—๋Š” ์•ž์„œ ์‚ดํŽด๋ณด์•˜๋˜ ES6๋ถ€ํ„ฐ 10๊นŒ์ง€ ์ผ๋ถ€ ๊ธฐ๋Šฅ์„ ์ด์šฉํ•˜๊ณ  ๊ณต๊ฐœ ๋˜์–ด์žˆ๋Š” Public API(์Œ์‹ ๋ ˆ์‹œํ”ผ API)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ ๊ฒ€์ƒ‰ UI๋ฅผ ๋งŒ๋“ค์–ด ๋ณผ๊ฑฐ์—์š”. ๋จผ์ € Vanilla JS๋กœ ๋งŒ๋“ค์–ด๋ณด๊ณ , ๊ทธ ๋‹ค์Œ RxJS๋ฅผ ํ™œ์šฉํ•ด ์ฝ”๋“œ๋ฅผ ์ปจ๋ฒ„ํŒ… ํ•ด๋ณผ๊ฒŒ์š”.
ย 

์–ด๋–ป๊ฒŒ ๋”ฐ๋ผํ•˜๋ฉด ๋˜๋‚˜์š”?

์ด๋ฒˆ ์„ธ์…˜์€ ์•ž์„  ์„ธ์…˜๊ณผ ๋‹ฌ๋ฆฌ API ํ˜ธ์ถœ์„ ํ•˜๊ณ  ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ npm์œผ๋กœ ๊ฐ€์ ธ์™€ ์›น์„ ๋งŒ๋“ค์–ด ๋ณผ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— HTML, CSS, JS ํŒŒ์ผ์„ ํ•˜๋‚˜์”ฉ ๋งŒ๋“ค๊ณ , JS ํŒŒ์ผ์€ webpack์œผ๋กœ ๋นŒ๋“œํ•ด์„œ index.html์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ง„ํ–‰ํ•˜๊ฒŒ ๋  ๊ฑฐ์—์š”. ๋Œ€๋žต์ ์ธ ๊ตฌ์กฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • ํ”„๋กœ์ ํŠธ ํด๋”์— index.html, app.js, app.css ํŒŒ์ผ์ด ์žˆ๋‹ค.
  • webpack์œผ๋กœ app.js ํŒŒ์ผ์„ ๋นŒ๋“œํ•ด์„œ index.html์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•œ๋‹ค.
    • webpack -p --watch ./*.js
  • index.html์„ ๋กœ์ปฌ์—์„œ ์„œ๋น™ํ•œ๋‹ค.
    • Ex) npx http-server
  • ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋กœ์ปฌํ˜ธ์ŠคํŠธ๋กœ ์ ‘์†ํ•ด ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ™•์ธํ•œ๋‹ค.
ย 
๋จผ์ € ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ํด๋”๋ฅผ ์›ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”.
mkdir awesome-search cd awesome-search
๊ทธ๋ฆฌ๊ณ  ํ”„๋กœ์ ํŠธ ํด๋” ์•ˆ์—์„œ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด์„œ npm init์„ ํ•ด์ฃผ์„ธ์š”.
npm init
๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ lodash๋ฅผ ์‚ฌ์šฉํ•ด๋ณผ๊ฒŒ์š”. npm์œผ๋กœ lodash๋ฅผ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”. ์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋งŒ๋“ค๊ณ  ๋”ฐ๋ผํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค!
npm install lodash-es --save
ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ–ˆ์œผ๋‹ˆ ์ด์ œ HTML ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="./app.css"> <title>DevFest WebTech 2019 CodeLab</title> </head> <body> <div class="container"> <header> Find your recipes! </header> <main> <input class="searchInput" type="text"> <ul class="recipeList"></ul> </main> </div> <script src=""></script> </body> </html>
* { box-sizing: border-box; } html, body { margin: 0; padding: 0; } .container { display: flex; flex-direction: column; align-items: center; min-width: 100vw; min-height: 100vh; margin-top: 20px; } header { display: flex; width: 450px; height: 50px; font-size: 30px; font-weight: bolder; border: 2px solid seagreen; align-items: center; justify-content: center; } .searchInput { margin-top: 30px; width: 450px; height: 40px; font-size: 20px; font-weight: 400; padding: 0 15px; border: 2px solid darkorange; } .searchInput:focus { outline: none; } .recipeList { display: flex; flex-direction: column; width: 450px; list-style: none; padding: 5px; height: 500px; overflow-y: scroll; border: 2px solid seagreen; } .recipe { display: flex; flex-direction: row; min-height: 300px; width: 100%; border-bottom: 2px solid gold; margin-bottom: 4px; } .recipeColumn { width: 270px; padding-left: 10px; } .recipeInstructions { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
๋ญ”๊ฐ€ ์ดˆ๋ผํ•ด ๋ณด์ด์ฃ ? CSS๋กœ ์›ํ•˜๋Š”๋Œ€๋กœ ๋””์ž์ธ ํ•ด์ฃผ์„ธ์š”! ํ˜น์€ ์ด CSS๋ฅผ ์‚ฌ์šฉํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค. ์ด์ œ JS ํŒŒ์ผ์„ ๋งŒ๋“ญ์‹œ๋‹ค. ์ด๋ฆ„์€ ์•„๋ฌด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”.
// app.js const searchInput = document.querySelector('.searchInput') const recipeList = document.querySelector('.recipeList') const inputHandler = (event) => { // ... } searchInput.addEventListener('input', inputHandler)
์šฐ๋ฆฌ์˜ ๋ชฉํ‘œ๋Š” input tag์—์„œ ๋ฌธ์ž์—ด์„ ์ž…๋ ฅ๋ฐ›์•„ ์›ํ•˜๋Š” ํƒ€์ด๋ฐ์— API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์‘๋‹ต ๊ฒฐ๊ณผ๋ฅผ ul tag์— ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด๋‹ˆ ์ดˆ๊ธฐ์—” ์•„๋งˆ ์ด๋Ÿฐ ๋ชจ์Šต์„ ํ•˜๊ณ ์žˆ๊ฒ ์ฃ ?
๊ทธ๋Ÿฐ๋ฐ ๋งค๋ฒˆ input tag์—์„œ ํ‚ค๊ฐ€ ์ž…๋ ฅ๋  ๋•Œ๋งˆ๋‹ค ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ์ด ๋ ๊ฑฐ์—์š”. ์ด ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์•ˆ์—์„œ API ํ˜ธ์ถœ์„ ํ• ํ…๋ฐ ํ‚ค๊ฐ€ ์ž…๋ ฅ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ์ด ์ผ์–ด๋‚˜๋ฉด Network IO๊ฐ€ ๋„ˆ๋ฌด ์ง€๋‚˜์น˜๊ฒŒ ์ผ์–ด๋‚˜๋‹ˆ ๋‚ญ๋น„๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ์ค„์—ฌ๋ณผ๊ฒŒ์š”.
import { debounce } from 'lodash-es' const debouncedInputHandler = debounce((event) => { // ... }, 1000)
์ด๋ ‡๊ฒŒ ๊ธฐ์กด์˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ lodash์˜ debounce์— ๋„ฃ์–ด์ฃผ๋ฉด, debounce๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ setTimeout์„ ํ™œ์šฉํ•ด ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ๋„˜์–ด์˜จ ๋ฐ€๋ฆฌ์„ธ์ปจ๋“œ ๋งŒํผ์˜ ์‹œ๊ฐ„ ์•ˆ์—์„œ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ๋ฐœ์ƒํ•œ event๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. ๋‹ค์‹œ ๋งํ•ด, ์ด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ์ด์ œ ํ‚ค๋ณด๋“œ๋ฅผ ์—ฐ์†์œผ๋กœ ํƒ€์ดํ•‘ ํ–ˆ์„๋•Œ 1์ดˆ ๋ฏธ๋งŒ์˜ ๊ฐ„๊ฒฉ์œผ๋กœ ํƒ€์ดํ•‘๋ผ์„œ ๋„˜์–ด์˜ค๋Š” event๋Š” ๋ฌด์‹œํ•˜๊ฒŒ ๋  ๊ฑฐ์—์š”.
์ž, ์ด์   ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ํƒ€์ด๋ฐ์— API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์œผ๋‹ˆ API ํ˜ธ์ถœ์„ ํ•ด๋ณผ๊นŒ์š”?
fetch('some url', { method: 'GET', }) .then((response) => ...)
๋ธŒ๋ผ์šฐ์ €์— ๋‚ด์žฅ๋˜์–ด ์žˆ๋Š” fetch ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ–ˆ์–ด์š”. ๊ธฐ์กด์— ์žˆ๋˜ XMLHttpRequest๊ณผ๋Š” ํ˜•ํƒœ์™€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์ด ์‚ฌ๋ญ‡ ๋‹ค๋ฅผ ๊ฑฐ์—์š”. fetch๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜๊ณ  Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. ๊ทธ๋ž˜์„œ ์ด fetch๋Š” async await ๊ณผ๋„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
์ด์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์ž˜ ๋ฐ›์•„์™”์œผ๋‹ˆ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๊ทธ๋ ค๋ด…์‹œ๋‹ค.
const apiEndPoint = 'https://www.themealdb.com/api/json/v1/1/search.php' fetch(`${apiEndPoint}?f=${inputValue}`, { method: 'GET', }) .then((response) => response.json()) .then(({ meals }) => { recipeList.innerHTML = meals .map((meal) => ...) })
์ด๋•Œ meals ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๋กœ DOM์œผ๋กœ ๋ฐ”๊ฟ”์„œ ๊ทธ๋ ค์•ผ ํ•˜๋Š”๋ฐ, ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์ด ์žˆ์„ ๊ฒƒ ๊ฐ™์•„์š”. JSON์„ html string์œผ๋กœ ์ „ํ™˜ํ•ด์ฃผ๋Š” ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์„œ map์— ์ ์šฉํ•ด๋„ ๋  ๊ฒƒ ๊ฐ™๊ณ , ์ง์ ‘ ๊ทธ ๋‚ด์šฉ์„ ์ฝœ๋ฐฑ ํ•จ์ˆ˜์— ์ ์–ด๋„ ๋  ๊ฒƒ ๊ฐ™์•„์š”. ์ด๋ฒˆ์—๋Š” ์ด meal ํ•˜๋‚˜๋ฅผ ํ‘œํ˜„ํ•  Class๋ฅผ ์ƒˆ๋กœ์šด meal.js ๋ผ๋Š” ํŒŒ์ผ์— ๋งŒ๋“ค์–ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ ค๋ณผ๊ฒŒ์š”. ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์€ ์ž์œ ์ด๋‹ˆ ๊ทธ๋ƒฅ ๋ณด๊ธฐ๋งŒ ํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค!
class Meal { constructor(mealData) { this.mealData = mealData } renderToString() { const { strArea, strCategory, strIngredient1, strMeasure1, strInstructions, strMeal, strMealThumb, strTags, } = this.mealData return ` <li class="recipe"> <div> <img width="130px" height="130px" src=${strMealThumb} > </div> <div class="recipeColumn"> <span>${strArea}</span> <span>${strCategory}</span> <br /> <span>${strMeal}</span> <br /><br /> Ingredient List <ul> <li>${strIngredient1}: ${strMeasure1}</li> </ul> <br /> Instruction <div class="recipeInstructions">${strInstructions}</div> <br /><br /> <span>${strTags}</span> </div> </li> ` } } export default Meal
์ด Meal ํด๋ž˜์Šค๋Š” ์ธ์Šคํ„ด์Šค๋กœ ๋งŒ๋“ค์–ด์งˆ๋•Œ ์ž์‹ ์„ ํ‘œํ˜„ํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ณ , ๋‚˜์ค‘์— ์ž์‹ ์ด ๊ฐ–๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋กœ ํ™”๋ฉด์— ๊ทธ๋ ค์งˆ ์ˆ˜ ์žˆ๋„๋ก ๋‚ด๋ถ€์ ์œผ๋กœ renderToString ์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๊ฐ–๊ณ  ์žˆ์–ด์š”. ์ •๋ง ๊ฐ„๋‹จํ•œ ํด๋ž˜์Šค๋„ค์š”! ์ด์ œ ์ด ํด๋ž˜์Šค๋ฅผ ํ™œ์šฉํ•ด์„œ ์•„๊นŒ์˜ fetch ํ•จ์ˆ˜์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋ ค๋ณผ๊ฒŒ์š”. ์•„! ์ด ํด๋ž˜์Šค๋Š” meal.js ๋ผ๋Š” ํŒŒ์ผ์— ์ž‘์„ฑ๋˜์–ด ์žˆ์œผ๋‹ˆ app.js์—์„œ ์ด ํŒŒ์ผ์„ ๊ฐ€์ ธ์™€์•ผ๊ฒ ์ฃ ?
import Meal from './meal'
Meal ํด๋ž˜์Šค๋Š” default๋กœ export ๋˜์–ด์žˆ์œผ๋‹ˆ ์ด๋ ‡๊ฒŒ ๊ฐ€์ ธ์˜ค๋ฉด ๋  ๊ฑฐ์—์š”.
์ž, ์ด์ œ ์ •๋ง ๊ทธ๋ ค๋ด…์‹œ๋‹ค.
fetch(`${apiEndPoint}?f=${inputValue}`, { method: 'GET', }) .then((response) => response.json()) .then(({ meals }) => { recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') })
์ด๋ ‡๊ฒŒ ๋ณด๋‹ˆ new ํ‚ค์›Œ๋“œ๋กœ ์ธ์Šคํ„ด์Šคํ™” ํ• ๋ฟ, ํ•จ์ˆ˜๋กœ ์ž‘์„ฑํ•œ๋‹ค๊ณ  ํ–ˆ์„๋•Œ์™€ ํฌ๊ฒŒ ๋‹ฌ๋ผ๋ณด์ด์ง€๋Š” ์•Š์ฃ ? ์ค‘์š”ํ•œ ๊ฑด, ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ฃผ๋กœ ๋‹ค๋ฃฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œํ˜„ํ•˜๊ณ , ์ด ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋‹ค๋ค„์ ธ์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•ด์„œ ์ฝ”๋“œ๋กœ ๋ช…์‹œํ–ˆ๋‹ค๋Š” ์ ์ด์—์š”. ๊ทธ๊ฒƒ์€ ํ•จ์ˆ˜๋กœ ํ•ด๋„ ๋˜๊ณ , ํด๋ž˜์Šค๋กœ ํ•ด๋„ ๋ผ์š”. ๋ฌผ๋ก  ์ €๋Š” ํ•จ์ˆ˜๋ฅผ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค..
ย 
์ž ์ด์ œ ์—ฌ๊ธฐ๊นŒ์ง€ ํ–ˆ์œผ๋ฉด input tag์— ํƒ€์ดํ•‘์„ ํ•˜๋‹ค๊ฐ€ ๋ฉˆ์ถ”๋ฉด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ํ™”๋ฉด์— ๊ทธ๋ ค์งˆ ๊ฑฐ์—์š”! ๊ทธ๋Ÿฐ๋ฐ, ๋งŒ์•ฝ apple pie๋กœ ๊ฒ€์ƒ‰ํ–ˆ๋‹ค๊ฐ€, ํƒ€์ดํ•‘์„ ๋‹ค์‹œ ๋ง‰ ํ–ˆ๋Š”๋ฐ ๊ฒฐ๊ตญ ์ตœ์ข… ์ž…๋ ฅ ๊ฐ’์€ apple pie๋กœ ์ด์ „๊ณผ ๋˜‘๊ฐ™๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? ์ง€๊ธˆ์€ ๋˜‘๊ฐ™์€ query๋กœ API๊ฐ€ ํ˜ธ์ถœ๋  ๊ฑฐ์—์š”. ๋ญ”๊ฐ€ ๋‚ญ๋น„๊ฐ€ ์ƒ๊ธด ๊ฒƒ ๊ฐ™์•„์š”. ์ด์ „์— ๊ฒ€์ƒ‰ํ–ˆ๋˜ ๊ฐ’์„ ์–ด๋”˜๊ฐ€์— ์ €์žฅํ•ด ๋‘์—ˆ๋‹ค๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„๋•Œ๋งŒ API๋ฅผ ํ˜ธ์ถœํ•ด๋ด…์‹œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์•„๋ฌด๊ฒƒ๋„ ์ž…๋ ฅ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋„ API๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๋ง‰์•„๋ด…์‹œ๋‹ค.
let previousInputValue = '' const debouncedInputHandler = debounce((event) => { if (!inputValue || previousInputValue === inputValue) { return } fetch(`${apiEndPoint}?f=${inputValue}`, { method: 'GET', }) .then((response) => response.json()) .then(({ meals }) => { recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') }) previousInputValue = inputValue }, 1000)
์ž, ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ด์ œ ๊ฐ’์ด ์—†๊ฑฐ๋‚˜, ์ด์ „์— ๊ฒ€์ƒ‰ํ–ˆ๋˜ ๊ฐ’์€ ๊ฑด๋„ˆ๋›ฐ๊ฒŒ ๋˜์—ˆ์–ด์š”. ๋ญ”๊ฐ€ ์กฐ๊ธˆ์€ ๊ฐœ์„ ๋œ ๊ฒƒ ๊ฐ™์•„์š”!
๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ API๊ฐ€ ์–ด๋–ค ์ด์œ ๋กœ ์—๋Ÿฌ๋ฅผ ๋‚ด๊ฒŒ ๋˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”? ์šฐ๋ฆฌ์˜ ์ฝ”๋“œ๋Š” ์•„์ง ์—๋Ÿฌ๋ฅผ ๋ฐ›์•„๋“ค์ผ ์ค€๋น„๊ฐ€ ๋˜์ง€ ์•Š์•˜์–ด์š”. ๋‹คํ–‰ํžˆ๋„ ์šฐ๋ฆฌ๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” fetch๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์ฒ˜๋Ÿผ ํ•ธ๋“ค๋ง ํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์—์š”. ๋ฌผ๋ก  try catch ๊ตฌ๋ฌธ์„ ์‚ฌ์šฉํ•ด๋„ ๋ฉ๋‹ˆ๋‹ค.
.then(({ meals }) => { recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') }) .catch(() => { recipeList.innerHTML = '<h3>An error has occurred when fetching data...</h3>' })
ํ , ๊ทธ๋Ÿฐ๋ฐ ๊ฒ€์ƒ‰ API๊ฐ€ ๋„์ฐฉํ•˜๊ธฐ ์ „๊นŒ์ง€๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋”ฉ๋˜๊ณ  ์žˆ๋‹ค๊ณ  ํ‘œ์‹œํ•ด์ฃผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”. ๊ทธ ํ‘œ์‹œ๋Š” ์–ธ์ œํ•ด์•ผ ํ• ๊นŒ์š”? API๋ฅผ ํ˜ธ์ถœํ•˜์ž๋งˆ์ž ๋ณด์—ฌ์ฃผ๊ณ , API๊ฐ€ ์ž˜ ๋„์ฐฉํ–ˆ๊ฑฐ๋‚˜ ์‹คํŒจํ•œ ์ดํ›„์— ์•Œ๋งž์€ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด ๋˜์ง€ ์•Š์„๊นŒ์š”?
recipeList.innerHTML = '<h3>Loading...</h3>' fetch(`${apiEndPoint}?f=${inputValue}`, { method: 'GET', }) .then(({ meals }) => { recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') }) .catch(() => { recipeList.innerHTML = '<h3>An error has occurred when fetching data...</h3>' })
์ด๋ฏธ API๊ฐ€ ์ž˜ ๋„์ฐฉํ•œ ์ƒํ™ฉ๊ณผ ์‹คํŒจํ•œ ์ƒํ™ฉ ๋ชจ๋‘๋ฅผ then๊ณผ catch์—์„œ ์žก์•„ ์ ์ ˆํ•œ ํ‘œ์‹œ๋ฅผ ํ•ด์ฃผ๊ณ  ์žˆ์–ด์„œ ์šฐ๋ฆฌ๋Š” ๊ทธ๋ƒฅ fetch ํ•˜๊ธฐ ์ง์ „์— ๋ฐ”๋กœ Loading Indicator๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด ๋ผ์š”!
์ž ๊ทธ๋Ÿผ ์ด์ œ Loading Indicator๋„ ์žˆ๊ณ  ์—๋Ÿฌ๋„ ํ•ธ๋“ค๋งํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋” ๊ตฌ์ƒ‰์ด ๊ฐ–์ถ”์–ด์กŒ์–ด์š”. Promise๋ฅผ ๋ง›๋ณด์•˜์œผ๋‹ˆ ์ด์ „ ์„ธ์…˜์—์„œ ๋ณด์•˜๋˜ async await๋ฅผ ์‚ฌ์šฉํ•ด๋ด…์‹œ๋‹ค.
const debouncedInputHandler = debounce(async ({ target: { value: inputValue } }) => { if (!inputValue || previousInputValue === inputValue) { return } recipeList.innerHTML = '<h3>Loading...</h3>' try { const response = await fetch(`${apiEndPoint}?f=${inputValue}`, { method: 'GET', }) const { meals } = await response.json() recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') } catch { recipeList.innerHTML = '<h3>An error has occurred when fetching data...</h3>' } previousInputValue = inputValue }, 1000)
async await๋กœ ๋งŒ๋“  ๋™์ผํ•œ ๋ฒ„์ „์˜ ์ฝ”๋“œ์—์š”. ์ด์ „๊ณผ ๋‹ฌ๋ผ์ง„ ์ ์€ debounce์— ๋„˜๊ธฐ๋Š” ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ async๋กœ ์„ ์–ธ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ, Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ ์•ž์— await๊ฐ€ ์“ฐ์—ฌ ๋งˆ์น˜ ๋™๊ธฐ์ ์ธ ์ฝ”๋“œ๋กœ ๋ณด์ด๋Š” ๊ฒƒ, Promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ ์ „์ฒด๋ฅผ try catch ๊ตฌ๋ฌธ์ด ๊ฐ์‹ธ๊ณ  ์žˆ๋Š” ๊ฒƒ์ด์—์š”. ์—๋Ÿฌ๋Š” ์—ฌ๊ธฐ์„œ ์žก์•„๋„ ๋˜๊ณ  ๋” ์ƒ์œ„ ๊ณต๊ฐ„์—์„œ ์žก์•„๋„ ๊ดœ์ฐฎ์•„์š”. ๊ทธ๊ฒƒ์€ ์•ฑ์„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ•˜๋А๋ƒ์— ๋‹ฌ๋ ค์žˆ๋‹ต๋‹ˆ๋‹ค.
ย 
์ž, ์ด์ œ Vanilla JS๋กœ๋Š” ๋‹ค ๋งŒ๋“ค์–ด ๋ณด์•˜์œผ๋‹ˆ ์ด๋ฒˆ์—” RxJS๋ฅผ ํ™œ์šฉํ•ด์„œ ๋™์ผํ•œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค. ์‹œ์ž‘ํ•˜๊ธฐ์— ์•ž์„œ, ๋จผ์ € RxJS์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ฐœ๋…์— ๋Œ€ํ•ด์„œ ์•„์ฃผ ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณผ๊ฒŒ์š”.
ย 

Observable

์šฐ๋ฆฌ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค๋•Œ ๋ฐ์ดํ„ฐ๋Š” API์—์„œ๋„ ์˜ค๊ณ , ์—ฌ๋Ÿฌ DOM tag์— ๋‹ฌ๋ฆฐ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์—์„œ๋„ ์˜ค๊ฒ ์ฃ ? ํ•˜์ง€๋งŒ ์–ธ์ œ ์˜ฌ์ง€๋Š” ์•„๋ฌด๋„ ๋ชจ๋ฅด์ฃ . ๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ Browser์— ๋“ฑ๋ก ์„ ํ•ด์„œ ์–ด๋–ค ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ Browser๊ฐ€ ๋Œ€์‹  ์šฐ๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ์ž‘์—…(์ฝœ๋ฐฑ ํ•จ์ˆ˜)์„ ์‹คํ–‰ํ•ด ์ฃผ์ฃ . API๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์—์š”. ์–ธ์ œ ์˜ฌ์ง€ ๋ชจ๋ฅด๋Š” ๋น„๋™๊ธฐ์„ฑ ํ˜ธ์ถœ์„ ๋„์ฐฉํ–ˆ์„๋•Œ ์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด Promise๋กœ API๋ฅผ ํ•ธ๋“ค๋ง ํ–ˆ์–ด์š”.
RxJS๋Š” ์ด๋Ÿฐ ๋ชจ๋“  ์ข…๋ฅ˜์˜ ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ํ‘œํ˜„ํ•ด์ฃผ๋Š” ๊ฐ์ฒด๋ž๋‹ˆ๋‹ค. ์ •ํ™•ํžˆ๋Š” ์‹œ๊ฐ„์„ ์ถ•์œผ๋กœ ์—ฐ์†์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š” ๊ฐ์ฒด ์ž…๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ input tag์— awesome์ด๋ผ๊ณ  ํƒ€์ดํ•‘์„ ํ•˜๋ฉด a โ†’ aw โ†’ awe โ†’ awes โ†’ aweso โ†’ awesom โ†’ awesome ์ˆœ์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„˜์–ด์˜ต๋‹ˆ๋‹ค. API๋“ค๋„ ํ˜ธ์ถœํ•œ ์ˆœ์„œ์— ๋”ฐ๋ผ ์ปจํŠธ๋กคํ•ด์•ผ ํ•˜๋Š” ํ•„์š”์„ฑ์ด ์žˆ์ฃ ? Observable์€ ์ด๋ ‡๊ฒŒ ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค๊ณ  ์ดํ•ดํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Operator

๊ฐ€์žฅ ์ค‘์š”ํ•œ Observable์„ ์ดํ•ดํ–ˆ์œผ๋‹ˆ ์ด๊ฑด ์ข€ ๋” ์‰ฝ์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์‹œ๊ฐ„์ˆœ์œผ๋กœ ์—ฐ์†์ ์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์šฐ๋ฆฌ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•˜์ง€๋งŒ ๊ณ„์‚ฐ์„ ํ•˜๊ฑฐ๋‚˜ log๋ฅผ ๋‚จ๊ธฐ๊ธฐ๋„ ํ•˜์ฃ . ์ด๋ ‡๊ฒŒ ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„ ์‚ฌ์ด ์‚ฌ์ด์— ์–ด๋–ค ํŠน์ •ํ•œ ์ž‘์—… ์„ ํ•ด์•ผํ•˜๋Š”๋ฐ, ์ด ์ž‘์—…์„ ํ•ด์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋…€์„์ด ๋ฐ”๋กœ Operator ์—์š”(Operator๋Š” Observable์„ ๋งŒ๋“ค์–ด๋‚ด๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค). ์ข€ ๋” ์ •ํ™•ํžˆ๋Š”, ์ด Operator๋Š” Observable์„ ๋ฐ›์•„ ์–ด๋–ค ์ž‘์—…์„ ํ•˜๊ณ  ์ƒˆ๋กœ์šด Observable์„ ๋’ค๋กœ ๋„˜๊น๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ๋‹ค์Œ Operator๊ฐ€ ๋ฐ›๊ฒ ์ฃ !

Observer

์ด๋ ‡๊ฒŒ Observable๊ณผ Operator๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ˜๋Ÿฌ์„œ ์–ด๋–ป๊ฒŒ ๋ณ€ํ•˜๋Š”์ง€๋ฅผ ํ‘œํ˜„ํ–ˆ์œผ๋‹ˆ, ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒฐ๊ตญ์—” ์–ด๋”˜๊ฐ€์—์„œ ๊บผ๋‚ด์„œ ํ™œ์šฉ์„ ํ•ด์•ผ๊ฒ ์ฃ ? ๊ทธ ํ™œ์šฉ์„ ํ•˜๋Š” ๋…€์„์ด ๋ฐ”๋กœ Observer์—์š”. ๊ทธ๋ž˜์„œ ๊ด€์ฐฐ์ž์ธ๊ฑฐ์ฃ ! ๋’ค์— ์ฝ”๋“œ๋กœ ๋ณด๋ฉด ๋” ์‰ฝ๊ฒŒ ์ดํ•ด๊ฐ€ ๊ฐ€๊ฒ ์ง€๋งŒ, ๋” ์ •ํ™•ํžˆ ์„ค๋ช…ํ•˜๋ฉด Observer๋Š” next, error, complete ํ•จ์ˆ˜๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ–๋Š” ๊ฐ์ฒด๋ฅผ ์˜๋ฏธํ•ด์š”. ์ด ๊ฐ์ฒด๋ฅผ observableInstance.subscribe() ์ด๋ ‡๊ฒŒ subscribe ํ•จ์ˆ˜ ์•ˆ์— ๋„ฃ์–ด์ฃผ๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ด์˜ค๊ธฐ ์‹œ์ž‘ํ•ด์š”.

Subscription

Observer๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ observableInstance.subscribe() ๋กœ ๊บผ๋‚ด์˜ค๊ธฐ ์‹œ์ž‘ํ–ˆ์–ด์š”. ๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ์–ธ์ œ ์˜ฌ์ง€ ๋ชจ๋ฅด๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ด์˜จ๋‹ค๋Š” ๊ฒƒ์€ RxJS๊ฐ€ Browser ์–ด๋”˜๊ฐ€์— ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋‹ฌ์•„๋†จ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๊ฒ ์ฃ ? ์ฆ‰, ๋ฉ”๋ชจ๋ฆฌ ์ž์›์ด ์‚ฌ์šฉ๋˜์—ˆ๋‹ค๋Š” ๊ฑด๋ฐ, ๋” ์ด์ƒ ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•˜์ง€ ์•Š์•„์•ผ ํ•˜๋Š” ์ˆœ๊ฐ„์ด ์˜ค๋ฉด(ํŽ˜์ด์ง€๋ฅผ ์ด๋™ํ•˜๋Š” ๋“ฑ) ์‚ฌ์šฉํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ž์›์„ ๋‹ค์‹œ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ด์š”. ๊ทธ๋Ÿด ๋•Œ๋ฅผ ์œ„ํ•ด Observable ์ธ์Šคํ„ด์Šค์˜ subscribe ํ•จ์ˆ˜๋Š” subscription ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. ์ด ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด subscription.unsubscribe() ์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ž์›์„ ํ•ด์ œํ•  ์ˆ˜ ์žˆ์–ด์š”.
ย 
์ด์™ธ์—๋„ RxJS์—๋Š” Subject, Scheduler ๋“ฑ ๊ฐœ๋…์ด ๋” ์žˆ์ง€๋งŒ ์ง€๊ธˆ์€ ์ด 4๊ฐœ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค! ์ด ์ฝ”๋“œ๋žฉ์„ ํ†ตํ•ด Reactive Programming๊ณผ RxJS์— ๋Œ€ํ•ด ๊ด€์‹ฌ์„ ๊ฐ–๊ฒŒ ๋˜์…จ๋‹ค๋ฉด ์ €๋Š” ๊ธฐ์ฉ๋‹ˆ๋‹ค! ์•ผํ˜ธ
๊ด€์‹ฌ์ด ์žˆ์œผ์‹  ๋ถ„์€ ์•„๋ž˜ ๋‘ ๋งํฌ์—์„œ ๋” ๋งŽ์ด ์•Œ์•„๋ณด์‹œ๋ฉด ์ข‹๊ฒ ์–ด์š”!
ย 
๊ฐœ๋…์„ ์•Œ์•„๋ณด์•˜์œผ๋‹ˆ Vanilla JS๋กœ ๋งŒ๋“ค์—ˆ๋˜ ์ฝ”๋“œ๋ฅผ ์ปจ๋ฒ„ํŒ… ํ•ด๋ด…์‹œ๋‹ค. ๋ฌด์—‡๋ถ€ํ„ฐ ํ•ด์•ผํ• ๊นŒ์š”? ์ด์ œ๋Š” ์œ„์—์„œ ์„ค๋ช…ํ–ˆ๋˜ RxJS์˜ ์ฃผ์š” ๊ฐœ๋…์„ ์ด์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ํ‘œํ˜„ํ•ด์•ผ๊ฒ ์ฃ ?
const inputStream = fromEvent(searchInput, 'input')
fromEvent ๋ผ๋Š” ํ•จ์ˆ˜๋Š” DOM EventTarget๊ณผ ์ด ํƒ€๊ฒŸ์—์„œ ๋ฐ์ดํ„ฐ๋กœ ์‚ผ์„ ์ด๋ฒคํŠธ๋ช…์„ ์ธ์ž๋กœ ๋ฐ›๊ณ , ๊ทธ ์ด๋ฒคํŠธ๋ช…์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” Observable ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด Observable์„ inputStream์ด๋ผ๊ณ  ์ด๋ฆ„์„ ์ง€์—ˆ์–ด์š”. ๊ทธ๋Ÿผ ์ด์ œ ์ด ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์„ ์ ์ ˆํžˆ ๋ณ€๊ฒฝํ•ด์„œ API ํ˜ธ์ถœ์„ ํ•˜๊ณ , ์‘๋‹ต์„ ๋ฐ›์•„ ๊ณ„์‚ฐํ•˜๊ณ , ๊ณ„์‚ฐ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ Œ๋”๋ง์„ ํ•˜๋Š” ์ž‘์—…์„ ํ•ด๋ณผ๊ฒŒ์š”.
const inputStream = fromEvent(searchInput, 'input') .pipe( ... )
fromEvent๊ฐ€ Observable์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ํ–ˆ์ฃ ? Observable ๊ฐ์ฒด์—๋Š” pipe ๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ ์žˆ์–ด์š”. ์ด ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด์„œ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์ด ์–ด๋–ป๊ฒŒ ๋ณ€ํ™”ํ•˜๋Š”์ง€๋ฅผ ํ‘œํ˜„ํ•  ๊ฑฐ์—์š”. ๊ทธ๋ฆฌ๊ณ  ์ด ํ•จ์ˆ˜๋Š” ๋‹ค์‹œ ์ƒˆ๋กœ์šด Observable ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. ๊ทธ๋ž˜์„œ ์ตœ์ข…์ ์œผ๋กœ inputStream์—๋Š” ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์ด ์–ด๋–ป๊ฒŒ ๋ณ€ํ™”ํ•˜๋Š”์ง€๋ฅผ ๋ชจ๋‘ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ๋Š” ์ƒˆ๋กœ์šด Observable ๊ฐ์ฒด๊ฐ€ ํ• ๋‹น๋  ๊ฑฐ์—์š”. ์ด์ œ pipe ์•ˆ์— ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์˜ ๋ณ€ํ™”๋ฅผ ํ‘œํ˜„ํ•ด๋ด…์‹œ๋‹ค.
const inputStream = fromEvent(searchInput, 'input') .pipe( map((event) => event.target.value), debounceTime(1000), distinctUntilChanged(), tap(() => recipeList.innerHTML = '<h3>Loading...</h3>'), switchMap((inputValue) => ajax(`${apiEndPoint}?f=${inputValue}`, { method: 'GET' }) .pipe( map(({ response }) => response ? response.meals : []), ), ), )
์—ฌ๊ธฐ์„œ ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜๋“ค(RxJS์—์„œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๊ธฐ๋„ ํ•˜๋Š”)์— ๋Œ€ํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ์†Œ๊ฐœ๋ฅผ ํ•ด๋“œ๋ฆด๊ฒŒ์š”.
map์€ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์–ด๋–ค ๊ฐ’์„ ๋ฐ›์•„์„œ ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ๋งคํ•‘์„ ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•ด์š”. ์ฆ‰, a๋ผ๋Š” ๊ฐ’์„ ๋ฐ›์•„์„œ ๊ณ„์‚ฐ์„ ํ•œ ๋’ค์— b๋ผ๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์š”.
debounceTime์€ ์šฐ๋ฆฌ๊ฐ€ Vanilla JS๋กœ ๋งŒ๋“ค์—ˆ๋˜ ์ฝ”๋“œ์—์„œ lodash์˜ debounce์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋Š” ํ•จ์ˆ˜์—์š”. ์ฃผ์–ด์ง„ ๋ฐ€๋ฆฌ์„ธ์ปจ๋“œ๋งŒํผ ๋””๋ฐ”์šด์‹ฑ์„ ํ•ฉ๋‹ˆ๋‹ค.
distinctUntilChanged๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ด์ „ ์ฝ”๋“œ์—์„œ previousInputValue๋กœ ์ด์ „ ๊ฐ’๊ณผ ์ƒˆ ๊ฐ’์ด ๊ฐ™์œผ๋ฉด API ํ˜ธ์ถœ์„ ๋ง‰์•˜๋˜ ์—ญํ• ์„ ํ•ด์š”. map, debounceTime ์„ ๊ฑฐ์ณ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ–ˆ๋Š”๋ฐ ์ด์ „ ๊ฐ’๊ณผ ๊ฐ™๋‹ค๋ฉด ๋‹ค์Œ Operator๋กœ ๋„˜์–ด๊ฐ€์ง€ ์•Š๊ฒŒ ๋งŒ๋“ค์–ด์š”!
tap์€ ๊ฐ€๋ณ๊ฒŒ ๋‘๋“œ๋ฆฌ๋‹ค ๋ผ๋Š” ๋œป์ธ๋ฐ, ์—ฌ๊ธฐ์„œ๋„ ๋™์ผํ•œ ์˜๋ฏธ๋กœ ์‚ฌ์šฉ๋ผ์š”. ์ด tap์€ map๊ณผ ๋‹ฌ๋ฆฌ ๋„์ฐฉํ•œ ๋ฐ์ดํ„ฐ์— ์–ด๋– ํ•œ ๋ณ€๊ฒฝ๋„ ํ•˜์ง€ ์•Š์•„์š”. ๊ทธ๋ƒฅ ๋ฐ์ดํ„ฐ๊ฐ€ tap์— ๋„์ฐฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” ์–ด๋–ค ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋‹ค์Œ Operator๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„˜์–ด๊ฐ€์š”. ๊ทธ๋ž˜์„œ ์ด tap์—์„œ log๋ฅผ ๋‚จ๊ธฐ๊ฑฐ๋‚˜ ํ•  ์ˆ˜ ์žˆ์–ด์š”.
์ง€๊ธˆ ์šฐ๋ฆฌ๋Š” searchInput์ด ๋‚ด๋ณด๋‚ด๋Š” input ์ด๋ฒคํŠธ๋ฅผ Observable๋กœ ํ‘œํ˜„ํ•˜๊ณ  ์žˆ์–ด์š”. ๊ทธ๋Ÿฐ๋ฐ ์šฐ๋ฆฌ์˜ ๋ชฉํ‘œ๋Š” ์ด input ๊ฐ’์„ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ์‹œ์ ์— API ํ˜ธ์ถœ์„ ํ•ด์•ผํ•˜์ฃ ? ์•ž์„œ ๋งํ–ˆ๋“ฏ์ด API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๊ทธ ์‘๋‹ต์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์ด์—์š”. RxJS์—์„œ๋Š” Observable ์•ˆ์—์„œ ์ƒˆ๋กœ์šด Observable์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์–ด์š”. ๊ทธ๋Ÿฌ๋‹ˆ๊นŒ [input tag์˜ ๊ฐ’์ด ํ˜๋Ÿฌ์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š”] ํ˜•ํƒœ์ธ๊ฑฐ์ฃ . ๊ทธ๋Ÿฐ๋ฐ ์šฐ๋ฆฌ๋Š” ์ด๋Ÿฐ ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์˜ ๊ตฌ์กฐ์— ์ƒ๊ด€์—†์ด [input tag์˜ ๊ฐ’์ด ํ˜๋Ÿฌ์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›๊ณ ] ์‹ถ์–ด์š”. ๊ทธ๋ž˜์„œ ์ด๋ ‡๊ฒŒ ๋ฐ”๊นฅ Observable์—์„œ ๋‚ด๋ถ€ Observable์˜ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ• ๋•Œ mergeMap์ด๋ผ๋Š” Operator๋ฅผ ์‚ฌ์šฉํ•ด์š”. ๊ทธ๋Ÿฐ๋ฐ ๋งŒ์•ฝ apple pie๋กœ ๊ฒ€์ƒ‰ํ•œ API๊ฐ€ ์•„์ง ๋„์ฐฉํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ peacan pie๋ผ๋Š” ๊ฐ’์œผ๋กœ ์ƒˆ๋กœ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ด์ „ API๋Š” ๋ฌด์‹œํ•˜๊ฑฐ๋‚˜ ์ทจ์†Œ๋ฅผ ํ•ด์•ผ๊ฒ ์ฃ ? mergeMap์˜ ์—ญํ• ์„ ํ•˜๋ฉด์„œ ์ƒˆ ๊ฐ’์ด ๋“ค์–ด์˜ค๋ฉด ์ด์ „ ์ž‘์—…์„ ์ทจ์†Œํ•ด์ฃผ๋Š” Operator๊ฐ€ switchMap์ด์—์š”!
ajax๋Š” ์ด๋ฆ„์—์„œ๋„ ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด API ์š”์ฒญ์„ ํ•˜๋Š” ํ•จ์ˆ˜์—์š”! ๊ทธ๋ฆฌ๊ณ  ์•ž์„œ ๋งํ–ˆ๋“ฏ์ด ์ด ๋…€์„์ด API ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ํ‘œํ˜„ํ•˜๋Š” Observable์„ ๋ฐ˜ํ™˜ํ•ด์š”.
์ž, ์ด์ œ input tag์—์„œ ์˜ค๋Š” ๊ฐ’์„ ๋ฐ›์•„ API๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์‘๋‹ต ๊ฒฐ๊ณผ๋ฅผ ์ ์ ˆํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ๊นŒ์ง€ Observable๊ณผ Operator๋ฅผ ์‚ฌ์šฉํ•ด ํ‘œํ˜„ํ–ˆ์–ด์š”. ์ด์ œ ์ด ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์—์„œ ์‹ค์ œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ๋ Œ๋”๋ง์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
inputStream.subscribe({ ... })
์ด๋ ‡๊ฒŒ inputStream Observable ๊ฐ์ฒด์˜ subscribe ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ธฐ ์‹œ์ž‘ํ•ด์š”! ๊ทธ๋Ÿฐ๋ฐ ์ด subscribe ํ•จ์ˆ˜๋Š” next, error, complete ๋ผ๋Š” ํ•จ์ˆ˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ–๋Š” ๊ฐ์ฒด๋ฅผ ๋ฐ›๊ฑฐ๋‚˜ ์ด ๊ฐ๊ฐ์˜ ํ•จ์ˆ˜ ์„ธ๊ฐœ๋ฅผ ์—ฐ๋‹ฌ์•„ ๋ฐ›์•„์š”. next๋Š” ๋‹ค์Œ ๊ฐ’์ด ์˜ฌ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ์ด ๋˜๊ณ , error๋Š” ์ด ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ ๊ณผ์ •์— ์—๋Ÿฌ๊ฐ€ ์žˆ์„๋•Œ ํ˜ธ์ถœ์ด ๋˜๊ณ , complete๋Š” Observable์˜ ๊ตฌ๋…์ด ์™„๋ฃŒ๋˜์—ˆ์„๋•Œ ํ˜ธ์ถœ์ด ๋ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ๋ ˆ์‹œํ”ผ๋ฅผ next์—์„œ ๋ฐ›์•„ ๋ Œ๋”๋ง ํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— next ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ์š”. ๊ทธ๋ฆฌ๊ณ  ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์„๋•Œ๋„ ์ ์ ˆํ•˜๊ฒŒ ํ‘œํ˜„์„ ํ•ด๋ด…์‹œ๋‹ค.
inputStream.subscribe({ next: (meals) => { recipeList.innerHTML = meals .map((meal) => new Meal(meal)) .map((mealInstance) => mealInstance.renderToString()) .join('') }, error: () => { recipeList.innerHTML = '<h3>An error has occurred when fetching data...</h3>' }, })
next ํ•จ์ˆ˜ ๋ฐ”๋””์˜ ๋‚ด์šฉ์ด ์ด์ „ ์ฝ”๋“œ์™€ ๋™์ผํ•˜์ฃ ? ๋ธŒ๋ผ์šฐ์ €๋กœ ํ™•์ธํ•ด๋ณด๋ฉด ์ด์ „ ์ฝ”๋“œ์™€ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”.
ย 
๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋ฅผ RxJS๋ฅผ ์ด์šฉํ•ด ๋‹ค์‹œ ํ‘œํ˜„์„ ํ•ด๋ณด์•˜๋Š”๋ฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ๋ฅด๊ณ , ์–ด๋–ค ์‹œ์ ์— ์–ด๋–ค ๋™์ž‘์„ ํ•ด์•ผํ•˜๋Š”์ง€ ์ดํ•ดํ•˜๊ณ  ์žˆ๋‹ค๋ฉด RxJS๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ์˜ ํ๋ฆ„์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™์ง€ ์•Š๋‚˜์š”? ์ด๋ฏธ react + redux + redux-observable์„ ์‚ฌ์šฉํ•˜๊ณ  ๊ณ„์‹  ๋ถ„๋“ค์€ ๋” ์ฒด๊ฐํ•˜์‹ค ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ ๊ฐ™๋„ค์š”! ์ด์ƒ, DevFest WebTech 2019 CodeLab ์ด์—ˆ์Šต๋‹ˆ๋‹ค!
ย 

์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ๋ฌธ์„œ

ย