├── .editorconfig ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json └── src ├── anime ├── index.html ├── main.completed.js ├── main.js └── style.css ├── callback-hell ├── assets │ ├── pexels-pixabay-104827.jpg │ ├── pexels-wojciech-kumpicki-2071873.jpg │ └── pexels-александар-цветановић-1440387.jpg ├── index.completed.mjs ├── index.js ├── index.step1.js ├── index.step2.js ├── index.step3.js ├── index.step4.mjs └── resized │ └── .gitkeep ├── sleep ├── index.completed.js └── index.js └── users ├── index.html ├── index.mjs ├── mock-request.mjs ├── users.completed.mjs └── users.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,cjs,mjs,json,yml,css}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | tab_width = 2 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Dependency directories 29 | node_modules/ 30 | 31 | src/callback-hell/resized/*.jpg 32 | .DS_Store 33 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Taegon Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fc-async-js 2 | 3 | 패스트캠퍼스 "쉽게 만나는 자바스크립트 비동기 처리" 세미나의 실습 코드 4 | 5 | ## 설치 6 | 7 | ### 이미지 라이브러리 8 | 9 | `callback-hell` 예제를 실행하려면 그래픽 라이브러리가 필요합니다. 10 | [gm 모듈의 안내](https://www.npmjs.com/package/gm)에 따라 GraphicsMagick 라이브러리를 시스템에 설치하세요. 11 | (ImageMagick도 가능하지만, gm 모듈을 호출하는 부분의 코드를 수정하셔야 에러가 발생하지 않습니다) 12 | 13 | ### 패키지 설치 14 | 15 | 다음 명령어를 실행하여 필요한 패키지를 설치합니다. 16 | 17 | ```shell 18 | npm i 19 | ``` 20 | 21 | ## 설명 22 | 23 | `src/`의 각 폴더에는 예제가 저장되어 있습니다. `index.js`, `users.js`, `main.js`에는 과제의 내용과 수정 전 코드가 있으니 이를 기반으로 직접 작성해보시면 됩니다. 24 | 25 | `*.completed.js`에는 보다 읽기 편하게 작성된 비동기 코드가 있습니다. 당연한 말이지만, 여기에 저장된 코드가 유일한 정답은 아닙니다. 각자의 상황과 방식에 따라 더 적절한 답이 있을 것이니 이를 참고삼아 스스로 고민해보기를 권장합니다. 26 | 27 | 필요한 경우 `*.step1.js`처럼 중간단계를 표현한 코드도 있습니다. 초기 코드부터 완성 코드까지 변화가 많아서 한 번에 이해하기 어려울 때 이해를 돕기 위해 추가한 코드입니다. 바로 앞 단계 코드와 비교해보면 코드 변경의 이유를 알 수 있을 것입니다. 28 | 29 | ## 실행 30 | 31 | `package.json`을 열어보면 몇 가지 스크립트를 확인할 수 있습니다. 학습 순서는 `sleep`, `callback-hell`, `users`, `anime` 순을 권장하지만 각각 독립적이므로 원하는 순서로 보셔도 상관없습니다. 32 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fc-async", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "fc-async", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "gm": "^1.25.0", 13 | "http-server": "^14.1.1" 14 | }, 15 | "devDependencies": { 16 | "@faker-js/faker": "^7.6.0", 17 | "prettier": "^2.8.1" 18 | } 19 | }, 20 | "node_modules/@faker-js/faker": { 21 | "version": "7.6.0", 22 | "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", 23 | "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", 24 | "dev": true, 25 | "engines": { 26 | "node": ">=14.0.0", 27 | "npm": ">=6.0.0" 28 | } 29 | }, 30 | "node_modules/ansi-styles": { 31 | "version": "4.3.0", 32 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 33 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 34 | "dependencies": { 35 | "color-convert": "^2.0.1" 36 | }, 37 | "engines": { 38 | "node": ">=8" 39 | }, 40 | "funding": { 41 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 42 | } 43 | }, 44 | "node_modules/array-parallel": { 45 | "version": "0.1.3", 46 | "resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz", 47 | "integrity": "sha512-TDPTwSWW5E4oiFiKmz6RGJ/a80Y91GuLgUYuLd49+XBS75tYo8PNgaT2K/OxuQYqkoI852MDGBorg9OcUSTQ8w==" 48 | }, 49 | "node_modules/array-series": { 50 | "version": "0.1.5", 51 | "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", 52 | "integrity": "sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==" 53 | }, 54 | "node_modules/async": { 55 | "version": "2.6.4", 56 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", 57 | "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", 58 | "dependencies": { 59 | "lodash": "^4.17.14" 60 | } 61 | }, 62 | "node_modules/basic-auth": { 63 | "version": "2.0.1", 64 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", 65 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 66 | "dependencies": { 67 | "safe-buffer": "5.1.2" 68 | }, 69 | "engines": { 70 | "node": ">= 0.8" 71 | } 72 | }, 73 | "node_modules/call-bind": { 74 | "version": "1.0.2", 75 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 76 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 77 | "dependencies": { 78 | "function-bind": "^1.1.1", 79 | "get-intrinsic": "^1.0.2" 80 | }, 81 | "funding": { 82 | "url": "https://github.com/sponsors/ljharb" 83 | } 84 | }, 85 | "node_modules/chalk": { 86 | "version": "4.1.2", 87 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 88 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 89 | "dependencies": { 90 | "ansi-styles": "^4.1.0", 91 | "supports-color": "^7.1.0" 92 | }, 93 | "engines": { 94 | "node": ">=10" 95 | }, 96 | "funding": { 97 | "url": "https://github.com/chalk/chalk?sponsor=1" 98 | } 99 | }, 100 | "node_modules/color-convert": { 101 | "version": "2.0.1", 102 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 103 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 104 | "dependencies": { 105 | "color-name": "~1.1.4" 106 | }, 107 | "engines": { 108 | "node": ">=7.0.0" 109 | } 110 | }, 111 | "node_modules/color-name": { 112 | "version": "1.1.4", 113 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 114 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 115 | }, 116 | "node_modules/corser": { 117 | "version": "2.0.1", 118 | "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", 119 | "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", 120 | "engines": { 121 | "node": ">= 0.4.0" 122 | } 123 | }, 124 | "node_modules/eventemitter3": { 125 | "version": "4.0.7", 126 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 127 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 128 | }, 129 | "node_modules/follow-redirects": { 130 | "version": "1.15.2", 131 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 132 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", 133 | "funding": [ 134 | { 135 | "type": "individual", 136 | "url": "https://github.com/sponsors/RubenVerborgh" 137 | } 138 | ], 139 | "engines": { 140 | "node": ">=4.0" 141 | }, 142 | "peerDependenciesMeta": { 143 | "debug": { 144 | "optional": true 145 | } 146 | } 147 | }, 148 | "node_modules/function-bind": { 149 | "version": "1.1.1", 150 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 151 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 152 | }, 153 | "node_modules/get-intrinsic": { 154 | "version": "1.1.3", 155 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 156 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 157 | "dependencies": { 158 | "function-bind": "^1.1.1", 159 | "has": "^1.0.3", 160 | "has-symbols": "^1.0.3" 161 | }, 162 | "funding": { 163 | "url": "https://github.com/sponsors/ljharb" 164 | } 165 | }, 166 | "node_modules/gm": { 167 | "version": "1.25.0", 168 | "resolved": "https://registry.npmjs.org/gm/-/gm-1.25.0.tgz", 169 | "integrity": "sha512-4kKdWXTtgQ4biIo7hZA396HT062nDVVHPjQcurNZ3o/voYN+o5FUC5kOwuORbpExp3XbTJ3SU7iRipiIhQtovw==", 170 | "dependencies": { 171 | "array-parallel": "~0.1.3", 172 | "array-series": "~0.1.5", 173 | "cross-spawn": "^4.0.0", 174 | "debug": "^3.1.0" 175 | }, 176 | "engines": { 177 | "node": ">=14" 178 | } 179 | }, 180 | "node_modules/gm/node_modules/cross-spawn": { 181 | "version": "4.0.2", 182 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", 183 | "integrity": "sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==", 184 | "dependencies": { 185 | "lru-cache": "^4.0.1", 186 | "which": "^1.2.9" 187 | } 188 | }, 189 | "node_modules/gm/node_modules/debug": { 190 | "version": "3.2.7", 191 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 192 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 193 | "dependencies": { 194 | "ms": "^2.1.1" 195 | } 196 | }, 197 | "node_modules/gm/node_modules/lru-cache": { 198 | "version": "4.1.5", 199 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", 200 | "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", 201 | "dependencies": { 202 | "pseudomap": "^1.0.2", 203 | "yallist": "^2.1.2" 204 | } 205 | }, 206 | "node_modules/gm/node_modules/which": { 207 | "version": "1.3.1", 208 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 209 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 210 | "dependencies": { 211 | "isexe": "^2.0.0" 212 | }, 213 | "bin": { 214 | "which": "bin/which" 215 | } 216 | }, 217 | "node_modules/gm/node_modules/yallist": { 218 | "version": "2.1.2", 219 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 220 | "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" 221 | }, 222 | "node_modules/has": { 223 | "version": "1.0.3", 224 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 225 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 226 | "dependencies": { 227 | "function-bind": "^1.1.1" 228 | }, 229 | "engines": { 230 | "node": ">= 0.4.0" 231 | } 232 | }, 233 | "node_modules/has-flag": { 234 | "version": "4.0.0", 235 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 236 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 237 | "engines": { 238 | "node": ">=8" 239 | } 240 | }, 241 | "node_modules/has-symbols": { 242 | "version": "1.0.3", 243 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 244 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 245 | "engines": { 246 | "node": ">= 0.4" 247 | }, 248 | "funding": { 249 | "url": "https://github.com/sponsors/ljharb" 250 | } 251 | }, 252 | "node_modules/he": { 253 | "version": "1.2.0", 254 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 255 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 256 | "bin": { 257 | "he": "bin/he" 258 | } 259 | }, 260 | "node_modules/html-encoding-sniffer": { 261 | "version": "3.0.0", 262 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 263 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 264 | "dependencies": { 265 | "whatwg-encoding": "^2.0.0" 266 | }, 267 | "engines": { 268 | "node": ">=12" 269 | } 270 | }, 271 | "node_modules/http-proxy": { 272 | "version": "1.18.1", 273 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", 274 | "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", 275 | "dependencies": { 276 | "eventemitter3": "^4.0.0", 277 | "follow-redirects": "^1.0.0", 278 | "requires-port": "^1.0.0" 279 | }, 280 | "engines": { 281 | "node": ">=8.0.0" 282 | } 283 | }, 284 | "node_modules/http-server": { 285 | "version": "14.1.1", 286 | "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", 287 | "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", 288 | "dependencies": { 289 | "basic-auth": "^2.0.1", 290 | "chalk": "^4.1.2", 291 | "corser": "^2.0.1", 292 | "he": "^1.2.0", 293 | "html-encoding-sniffer": "^3.0.0", 294 | "http-proxy": "^1.18.1", 295 | "mime": "^1.6.0", 296 | "minimist": "^1.2.6", 297 | "opener": "^1.5.1", 298 | "portfinder": "^1.0.28", 299 | "secure-compare": "3.0.1", 300 | "union": "~0.5.0", 301 | "url-join": "^4.0.1" 302 | }, 303 | "bin": { 304 | "http-server": "bin/http-server" 305 | }, 306 | "engines": { 307 | "node": ">=12" 308 | } 309 | }, 310 | "node_modules/iconv-lite": { 311 | "version": "0.6.3", 312 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 313 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 314 | "dependencies": { 315 | "safer-buffer": ">= 2.1.2 < 3.0.0" 316 | }, 317 | "engines": { 318 | "node": ">=0.10.0" 319 | } 320 | }, 321 | "node_modules/isexe": { 322 | "version": "2.0.0", 323 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 324 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 325 | }, 326 | "node_modules/lodash": { 327 | "version": "4.17.21", 328 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 329 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 330 | }, 331 | "node_modules/mime": { 332 | "version": "1.6.0", 333 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 334 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 335 | "bin": { 336 | "mime": "cli.js" 337 | }, 338 | "engines": { 339 | "node": ">=4" 340 | } 341 | }, 342 | "node_modules/minimist": { 343 | "version": "1.2.7", 344 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", 345 | "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", 346 | "funding": { 347 | "url": "https://github.com/sponsors/ljharb" 348 | } 349 | }, 350 | "node_modules/mkdirp": { 351 | "version": "0.5.6", 352 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 353 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 354 | "dependencies": { 355 | "minimist": "^1.2.6" 356 | }, 357 | "bin": { 358 | "mkdirp": "bin/cmd.js" 359 | } 360 | }, 361 | "node_modules/ms": { 362 | "version": "2.1.2", 363 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 364 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 365 | }, 366 | "node_modules/object-inspect": { 367 | "version": "1.12.2", 368 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 369 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", 370 | "funding": { 371 | "url": "https://github.com/sponsors/ljharb" 372 | } 373 | }, 374 | "node_modules/opener": { 375 | "version": "1.5.2", 376 | "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", 377 | "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", 378 | "bin": { 379 | "opener": "bin/opener-bin.js" 380 | } 381 | }, 382 | "node_modules/portfinder": { 383 | "version": "1.0.32", 384 | "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", 385 | "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", 386 | "dependencies": { 387 | "async": "^2.6.4", 388 | "debug": "^3.2.7", 389 | "mkdirp": "^0.5.6" 390 | }, 391 | "engines": { 392 | "node": ">= 0.12.0" 393 | } 394 | }, 395 | "node_modules/portfinder/node_modules/debug": { 396 | "version": "3.2.7", 397 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 398 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 399 | "dependencies": { 400 | "ms": "^2.1.1" 401 | } 402 | }, 403 | "node_modules/prettier": { 404 | "version": "2.8.1", 405 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", 406 | "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", 407 | "dev": true, 408 | "bin": { 409 | "prettier": "bin-prettier.js" 410 | }, 411 | "engines": { 412 | "node": ">=10.13.0" 413 | }, 414 | "funding": { 415 | "url": "https://github.com/prettier/prettier?sponsor=1" 416 | } 417 | }, 418 | "node_modules/pseudomap": { 419 | "version": "1.0.2", 420 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 421 | "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" 422 | }, 423 | "node_modules/qs": { 424 | "version": "6.11.0", 425 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 426 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 427 | "dependencies": { 428 | "side-channel": "^1.0.4" 429 | }, 430 | "engines": { 431 | "node": ">=0.6" 432 | }, 433 | "funding": { 434 | "url": "https://github.com/sponsors/ljharb" 435 | } 436 | }, 437 | "node_modules/requires-port": { 438 | "version": "1.0.0", 439 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 440 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" 441 | }, 442 | "node_modules/safe-buffer": { 443 | "version": "5.1.2", 444 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 445 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 446 | }, 447 | "node_modules/safer-buffer": { 448 | "version": "2.1.2", 449 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 450 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 451 | }, 452 | "node_modules/secure-compare": { 453 | "version": "3.0.1", 454 | "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", 455 | "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" 456 | }, 457 | "node_modules/side-channel": { 458 | "version": "1.0.4", 459 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 460 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 461 | "dependencies": { 462 | "call-bind": "^1.0.0", 463 | "get-intrinsic": "^1.0.2", 464 | "object-inspect": "^1.9.0" 465 | }, 466 | "funding": { 467 | "url": "https://github.com/sponsors/ljharb" 468 | } 469 | }, 470 | "node_modules/supports-color": { 471 | "version": "7.2.0", 472 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 473 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 474 | "dependencies": { 475 | "has-flag": "^4.0.0" 476 | }, 477 | "engines": { 478 | "node": ">=8" 479 | } 480 | }, 481 | "node_modules/union": { 482 | "version": "0.5.0", 483 | "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", 484 | "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", 485 | "dependencies": { 486 | "qs": "^6.4.0" 487 | }, 488 | "engines": { 489 | "node": ">= 0.8.0" 490 | } 491 | }, 492 | "node_modules/url-join": { 493 | "version": "4.0.1", 494 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", 495 | "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" 496 | }, 497 | "node_modules/whatwg-encoding": { 498 | "version": "2.0.0", 499 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 500 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 501 | "dependencies": { 502 | "iconv-lite": "0.6.3" 503 | }, 504 | "engines": { 505 | "node": ">=12" 506 | } 507 | } 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fc-async", 3 | "version": "1.0.0", 4 | "description": "FastCampus 세미나의 실습 코드", 5 | "main": "src/callback-hell/index.js", 6 | "scripts": { 7 | "anime": "http-server ./src/anime", 8 | "callback": "rm -f src/callback-hell/resized/*.* && node ./src/callback-hell", 9 | "users": "node ./src/users/index.mjs", 10 | "users:browser": "http-server ./src/users", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/taggon/fc-async-js.git" 16 | }, 17 | "keywords": [ 18 | "examples" 19 | ], 20 | "author": "Taegon Kim", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/taggon/fc-async-js/issues" 24 | }, 25 | "homepage": "https://github.com/taggon/fc-async-js#readme", 26 | "devDependencies": { 27 | "@faker-js/faker": "^7.6.0", 28 | "prettier": "^2.8.1" 29 | }, 30 | "dependencies": { 31 | "gm": "^1.25.0", 32 | "http-server": "^14.1.1" 33 | }, 34 | "volta": { 35 | "node": "18.12.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/anime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 애니메이션 검색 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | 17 | 20 |
21 |
22 |
23 |
검색된 애니메이션이 없습니다.
24 |
25 |
26 |
27 | 28 | 37 |
38 | 39 |
40 | 41 | -------------------------------------------------------------------------------- /src/anime/main.completed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 애니메이션 검색 3 | * 4 | * 검색어를 입력하면 해당 검색어와 일치하는 애니메이션 목록을 화면에 표시하는 함수를 작성하세요. 5 | * 사용할 API는 아래에 주어져 있습니다. 6 | * 7 | * #result-wrapper에 .loading 또는 .empty 클래스를 추가하면 로딩 애니메이션 또는 빈 화면을 표시할 수 있습니다. 8 | * 9 | * #card에 있는 템플릿을 사용하여 결과를 표시하세요. 10 | * 11 | * 사용할 API: 12 | * - url: `https://api.jikan.moe/v4/anime?q=${query}&page=1&limit=10` 13 | * - query: 검색어 14 | * 15 | * API 문서: https://docs.api.jikan.moe/#tag/anime/operation/getAnimeSearch 16 | */ 17 | 18 | const wrapper = document.querySelector('#result-wrapper'); 19 | const results = document.querySelector('#results'); 20 | const tpl = document.querySelector('#card'); 21 | const form = document.querySelector('form'); 22 | 23 | form.addEventListener('submit', async (event) => { 24 | event.preventDefault(); 25 | 26 | const query = event.target.elements.query.value; 27 | let data = []; 28 | 29 | // 기존 결과는 삭제합니다. 30 | results.innerHTML = ''; 31 | 32 | // 로딩 애니메이션을 표시합니다. 33 | wrapper.classList.add('loading'); 34 | wrapper.classList.remove('empty'); 35 | 36 | try { 37 | const resp = await fetch(`https://api.jikan.moe/v4/anime?q=${query}&sfw=true&page=1&limit=10`); 38 | const result = await resp.json(); 39 | 40 | data = result.data; 41 | 42 | if (data.length === 0) { 43 | wrapper.classList.add('empty'); 44 | return; 45 | } 46 | } catch (err) { 47 | console.error(`데이터를 가져오는 도중 에러가 발생했습니다: ${err}`); 48 | return; 49 | } finally { 50 | // 로딩 애니메이션을 제거합니다. 51 | wrapper.classList.remove('loading'); 52 | } 53 | 54 | data.forEach(render); 55 | }, false); 56 | 57 | function render(item) { 58 | const tpl = template.content.cloneNode(true); 59 | 60 | tpl.querySelector('img').src = item.images.jpg.image_url; 61 | tpl.querySelector('.card-title').innerText = item.title; 62 | tpl.querySelector('.desc').innerText = item.synopsis; 63 | 64 | results.appendChild(tpl); 65 | } 66 | -------------------------------------------------------------------------------- /src/anime/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 애니메이션 검색 3 | * 4 | * 검색어를 입력하면 해당 검색어와 일치하는 애니메이션 목록을 화면에 표시하는 함수를 작성하세요. 5 | * 사용할 API는 아래에 주어져 있습니다. 6 | * 7 | * #result-wrapper에 .loading 또는 .empty 클래스를 추가하면 로딩 애니메이션 또는 빈 화면을 표시할 수 있습니다. 8 | * 9 | * #card에 있는 템플릿을 사용하여 결과를 표시하세요. 10 | * 11 | * 사용할 API: 12 | * - url: `https://api.jikan.moe/v4/anime?q=${query}&sfw=true&page=1&limit=10` 13 | * - query: 검색어 14 | * 15 | * API 문서: https://docs.api.jikan.moe/#tag/anime/operation/getAnimeSearch 16 | */ 17 | 18 | const wrapper = document.querySelector('#result-wrapper'); 19 | const results = document.querySelector('#results'); 20 | const template = document.querySelector('#card'); 21 | const form = document.querySelector('form'); 22 | 23 | form.addEventListener('submit', async (event) => { 24 | event.preventDefault(); 25 | 26 | // TODO 27 | }, false); 28 | -------------------------------------------------------------------------------- /src/anime/style.css: -------------------------------------------------------------------------------- 1 | .btn[type="submit"]:not(:hover) { 2 | background-color: hsl(var(--p)/var(--tw-bg-opacity)); 3 | } 4 | 5 | .container .empty, 6 | .container .loading { 7 | display: none; 8 | } 9 | 10 | .container.loading, 11 | .container.empty { 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | .container.loading .loading { 17 | display: block; 18 | } 19 | 20 | .container.empty .empty { 21 | display: block; 22 | } 23 | 24 | .container:is(.empty, .loading) #results { 25 | display: none; 26 | } 27 | 28 | .card-body .desc { 29 | display: -webkit-box; 30 | -webkit-line-clamp: 5; 31 | -webkit-box-orient: vertical; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | flex-grow: unset; 35 | } 36 | 37 | .lds-facebook { 38 | display: inline-block; 39 | position: relative; 40 | width: 80px; 41 | height: 80px; 42 | } 43 | .lds-facebook div { 44 | display: inline-block; 45 | position: absolute; 46 | left: 8px; 47 | width: 16px; 48 | background: rgb(64, 6, 136); 49 | animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; 50 | } 51 | .lds-facebook div:nth-child(1) { 52 | left: 8px; 53 | animation-delay: -0.24s; 54 | } 55 | .lds-facebook div:nth-child(2) { 56 | left: 32px; 57 | animation-delay: -0.12s; 58 | } 59 | .lds-facebook div:nth-child(3) { 60 | left: 56px; 61 | animation-delay: 0; 62 | } 63 | @keyframes lds-facebook { 64 | 0% { 65 | top: 8px; 66 | height: 64px; 67 | } 68 | 50%, 100% { 69 | top: 24px; 70 | height: 32px; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/callback-hell/assets/pexels-pixabay-104827.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taggon/fc-async-js/4aacae7a464caeff8ac2e496b0e4f1d82db86b26/src/callback-hell/assets/pexels-pixabay-104827.jpg -------------------------------------------------------------------------------- /src/callback-hell/assets/pexels-wojciech-kumpicki-2071873.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taggon/fc-async-js/4aacae7a464caeff8ac2e496b0e4f1d82db86b26/src/callback-hell/assets/pexels-wojciech-kumpicki-2071873.jpg -------------------------------------------------------------------------------- /src/callback-hell/assets/pexels-александар-цветановић-1440387.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taggon/fc-async-js/4aacae7a464caeff8ac2e496b0e4f1d82db86b26/src/callback-hell/assets/pexels-александар-цветановић-1440387.jpg -------------------------------------------------------------------------------- /src/callback-hell/index.completed.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성합니다. 3 | */ 4 | import fs from 'node:fs/promises'; 5 | import path from 'node:path'; 6 | import { fileURLToPath } from 'node:url'; 7 | import gm from 'gm'; 8 | 9 | const source = path.join(path.dirname(fileURLToPath(import.meta.url)), 'assets'); 10 | const dest = path.join(path.dirname(fileURLToPath(import.meta.url)), 'resized'); 11 | const WIDTHS = [1024, 640, 320]; 12 | 13 | /** 14 | * 완료: 이미지를 처리하는 부분에도 async/await을 적용합니다. 15 | * 원본 코드와 비교해서 코드를 읽어보면 어떤 느낌이 드시나요? 16 | */ 17 | try { 18 | const files = await fs.readdir(source); 19 | 20 | for (const filename of files) { 21 | console.log(filename); 22 | if (filename.startsWith('.')) { 23 | continue; 24 | } 25 | 26 | const sourceFile = path.join(source, filename); 27 | const img = gm(sourceFile); 28 | let aspect = 1; 29 | 30 | try { 31 | const size = await gmReadSize(img); 32 | aspect = size.width / size.height; 33 | } catch(err) { 34 | console.log('Error identifying file size: ' + err); 35 | continue; 36 | } 37 | 38 | for (const width of WIDTHS) { 39 | const height = Math.round(width / aspect); 40 | const resizedPath = path.join(dest, 'w' + width + '_' + filename); 41 | console.log('resizing ' + filename + ' to ' + height + 'x' + height); 42 | 43 | try { 44 | await gmSaveResize(img, resizedPath, width, height); 45 | } catch(err) { 46 | console.log('Error writing file: ' + err); 47 | } 48 | } 49 | } 50 | } catch(err) { 51 | console.log('Error finding files: ' + err); 52 | } 53 | 54 | function gmReadSize(img) { 55 | return new Promise((resolve, reject) => { 56 | img.size((err, size) => { 57 | if (err) { 58 | reject(err); 59 | } else { 60 | resolve(size); 61 | } 62 | }); 63 | }); 64 | } 65 | 66 | function gmSaveResize(img, resizedPath, width, height) { 67 | return new Promise((resolve, reject) => { 68 | img.resize(width, height).write(resizedPath, (err) => { 69 | if (err) { 70 | reject(err); 71 | } else { 72 | resolve(); 73 | } 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/callback-hell/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성한다. 3 | */ 4 | const fs = require('node:fs'); 5 | const path = require('node:path'); 6 | const gm = require('gm'); 7 | 8 | const source = path.join(__dirname, 'assets'); 9 | const dest = path.join(__dirname, 'resized'); 10 | const WIDTHS = [1024, 640, 320]; 11 | 12 | fs.readdir(source, function (err, files) { 13 | if (err) { 14 | console.log('Error finding files: ' + err); 15 | } else { 16 | files.forEach(function (filename, fileIndex) { 17 | console.log(filename); 18 | if (!filename.startsWith('.')) { 19 | gm(path.join(source, filename)).size(function (err, size) { 20 | if (err) { 21 | console.log('Error identifying file size: ' + err); 22 | } else { 23 | console.log(filename + ' : ' + size); 24 | const aspect = size.width / size.height; 25 | WIDTHS.forEach( 26 | function (width, widthIndex) { 27 | let height = Math.round(width / aspect); 28 | console.log( 29 | 'resizing ' + filename + ' to ' + height + 'x' + height 30 | ); 31 | this.resize(width, height).write( 32 | path.join(dest, 'w' + width + '_' + filename), 33 | function (err) { 34 | if (err) console.log('Error writing file: ' + err); 35 | } 36 | ); 37 | }.bind(this) 38 | ); 39 | } 40 | }); 41 | } 42 | }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/callback-hell/index.step1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성합니다. 3 | */ 4 | const fs = require('node:fs'); 5 | const path = require('node:path'); 6 | const gm = require('gm'); 7 | 8 | const source = path.join(__dirname, 'assets'); 9 | const dest = path.join(__dirname, 'resized'); 10 | const WIDTHS = [1024, 640, 320]; 11 | 12 | /** 13 | * Step 1: 불필요한 if-else 문을 정리하여 단계를 줄입니다. 14 | */ 15 | fs.readdir(source, function (err, files) { 16 | if (err) { 17 | console.log('Error finding files: ' + err); 18 | return; 19 | } 20 | 21 | files.forEach(function (filename, fileIndex) { 22 | console.log(filename); 23 | if (filename.startsWith('.')) { 24 | return; 25 | } 26 | 27 | gm(path.join(source, filename)).size(function (err, size) { 28 | if (err) { 29 | console.log('Error identifying file size: ' + err); 30 | return; 31 | } 32 | 33 | console.log(filename + ' : ' + size); 34 | const aspect = size.width / size.height; 35 | 36 | WIDTHS.forEach( 37 | function (width, widthIndex) { 38 | let height = Math.round(width / aspect); 39 | console.log('resizing ' + filename + ' to ' + height + 'x' + height); 40 | this.resize(width, height).write( 41 | path.join(dest, 'w' + width + '_' + filename), 42 | function (err) { 43 | if (err) console.log('Error writing file: ' + err); 44 | } 45 | ); 46 | }.bind(this) 47 | ); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/callback-hell/index.step2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성합니다. 3 | */ 4 | const fs = require('node:fs'); 5 | const path = require('node:path'); 6 | const gm = require('gm'); 7 | 8 | const source = path.join(__dirname, 'assets'); 9 | const dest = path.join(__dirname, 'resized'); 10 | const WIDTHS = [1024, 640, 320]; 11 | 12 | /** 13 | * Step 2: this 대신 img 변수를 사용하여 코드를 조금 더 명확하게 보이도록 수정합니다. 14 | * 추가로 몇 가지 변수를 도입하여 코드를 더 읽기 쉽게 만듭니다. 15 | */ 16 | fs.readdir(source, function (err, files) { 17 | if (err) { 18 | console.log('Error finding files: ' + err); 19 | return; 20 | } 21 | 22 | files.forEach(function (filename, fileIndex) { 23 | console.log(filename); 24 | if (filename.startsWith('.')) { 25 | return; 26 | } 27 | 28 | const sourceFile = path.join(source, filename); 29 | gm(sourceFile).size(function (err, size) { 30 | if (err) { 31 | console.log('Error identifying file size: ' + err); 32 | return; 33 | } 34 | 35 | console.log(filename + ' : ' + size); 36 | const aspect = size.width / size.height; 37 | const img = this; 38 | 39 | WIDTHS.forEach((width) => { 40 | const height = Math.round(width / aspect); 41 | const resizedPath = path.join(dest, 'w' + width + '_' + filename); 42 | 43 | console.log('resizing ' + filename + ' to ' + height + 'x' + height); 44 | 45 | img.resize(width, height).write(resizedPath, (err) => { 46 | if (err) console.log('Error writing file: ' + err); 47 | }); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/callback-hell/index.step3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성합니다. 3 | */ 4 | const fs = require('node:fs'); 5 | const path = require('node:path'); 6 | const gm = require('gm'); 7 | 8 | const source = path.join(__dirname, 'assets'); 9 | const dest = path.join(__dirname, 'resized'); 10 | const WIDTHS = [1024, 640, 320]; 11 | 12 | /** 13 | * Step 3: GM의 resize 함수를 Promise를 사용하도록 변경합니다. 14 | */ 15 | fs.readdir(source, function (err, files) { 16 | if (err) { 17 | console.log('Error finding files: ' + err); 18 | return; 19 | } 20 | 21 | files.forEach(function (filename, fileIndex) { 22 | console.log(filename); 23 | if (filename.startsWith('.')) { 24 | return; 25 | } 26 | 27 | const sourceFile = path.join(source, filename); 28 | const img = gm(sourceFile); 29 | 30 | gmReadSize(img) 31 | .then(size => { 32 | console.log(filename + ' : ' + size); 33 | const aspect = size.width / size.height; 34 | return aspect; 35 | }) 36 | .catch(err => { 37 | console.log('Error identifying file size: ' + err); 38 | }) 39 | .then(aspect => { 40 | return Promise.all( 41 | WIDTHS.map(width => { 42 | const height = Math.round(width / aspect); 43 | const resizedPath = path.join(dest, 'w' + width + '_' + filename); 44 | console.log('resizing ' + filename + ' to ' + height + 'x' + height); 45 | 46 | return gmSaveResize(img, resizedPath, width, height); 47 | }) 48 | ); 49 | }) 50 | .catch(err => { 51 | console.log('Error writing file: ' + err); 52 | }); 53 | }); 54 | }); 55 | 56 | function gmReadSize(img) { 57 | return new Promise((resolve, reject) => { 58 | img.size((err, size) => { 59 | if (err) { 60 | reject(err); 61 | } else { 62 | resolve(size); 63 | } 64 | }); 65 | }); 66 | } 67 | 68 | function gmSaveResize(img, resizedPath, width, height) { 69 | return new Promise((resolve, reject) => { 70 | img.resize(width, height).write(resizedPath, (err) => { 71 | if (err) { 72 | reject(err); 73 | } else { 74 | resolve(); 75 | } 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/callback-hell/index.step4.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 폴더 내의 이미지 파일을 모두 읽어서 widths에 설정된 너비에 맞게 리사이징 한 이미지를 생성합니다. 3 | */ 4 | import fs from 'node:fs/promises'; 5 | import path from 'node:path'; 6 | import { fileURLToPath } from 'node:url'; 7 | import gm from 'gm'; 8 | 9 | const source = path.join(path.dirname(fileURLToPath(import.meta.url)), 'assets'); 10 | const dest = path.join(path.dirname(fileURLToPath(import.meta.url)), 'resized'); 11 | const WIDTHS = [1024, 640, 320]; 12 | 13 | /** 14 | * Step 4: fs.readdir도 Promise 버전으로 바꾸고, top-level await을 사용하여 코드를 더 간결하게 만듭니다. 15 | * top-level await은 모듈에서만 사용할 수 있기 때문에 현재 파일을 모듈로 바꾸었습니다. (확장자도 변경됐습니다) 16 | */ 17 | try { 18 | const files = await fs.readdir(source); 19 | 20 | for (const filename of files) { 21 | console.log(filename); 22 | if (filename.startsWith('.')) { 23 | continue; 24 | } 25 | 26 | const sourceFile = path.join(source, filename); 27 | const img = gm(sourceFile); 28 | 29 | gmReadSize(img) 30 | .then(size => { 31 | console.log(filename + ' : ' + size); 32 | const aspect = size.width / size.height; 33 | return aspect; 34 | }) 35 | .catch(err => { 36 | console.log('Error identifying file size: ' + err); 37 | }) 38 | .then(aspect => { 39 | return Promise.all( 40 | WIDTHS.map(width => { 41 | const height = Math.round(width / aspect); 42 | const resizedPath = path.join(dest, 'w' + width + '_' + filename); 43 | console.log('resizing ' + filename + ' to ' + height + 'x' + height); 44 | 45 | return gmSaveResize(img, resizedPath, width, height); 46 | }) 47 | ); 48 | }) 49 | .catch(err => { 50 | console.log('Error writing file: ' + err); 51 | }); 52 | } 53 | } catch(err) { 54 | console.log('Error finding files: ' + err); 55 | } 56 | 57 | function gmReadSize(img) { 58 | return new Promise((resolve, reject) => { 59 | img.size((err, size) => { 60 | if (err) { 61 | reject(err); 62 | } else { 63 | resolve(size); 64 | } 65 | }); 66 | }); 67 | } 68 | 69 | function gmSaveResize(img, resizedPath, width, height) { 70 | return new Promise((resolve, reject) => { 71 | img.resize(width, height).write(resizedPath, (err) => { 72 | if (err) { 73 | reject(err); 74 | } else { 75 | resolve(); 76 | } 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/callback-hell/resized/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taggon/fc-async-js/4aacae7a464caeff8ac2e496b0e4f1d82db86b26/src/callback-hell/resized/.gitkeep -------------------------------------------------------------------------------- /src/sleep/index.completed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * setTimeout 대신 사용할 Promise 기반의 sleep 함수 3 | */ 4 | function sleep(ms) { 5 | return new Promise(resolve => setTimeout(resolve, ms)); 6 | } 7 | -------------------------------------------------------------------------------- /src/sleep/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * setTimeout 대신 사용할 Promise 기반의 sleep 함수 3 | */ 4 | -------------------------------------------------------------------------------- /src/users/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 사용자 검색 6 | 7 | 8 |

사용자 검색

9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/users/index.mjs: -------------------------------------------------------------------------------- 1 | import { getUsers } from './users.mjs'; 2 | import { getUsers as getUsersCompleted } from './users.completed.mjs'; 3 | 4 | if (typeof document === 'undefined') { 5 | getUsers(); 6 | } else { 7 | // DOM 이벤트 핸들러에 비동기 코드를 추가해도 괜찮습니다. 8 | document.querySelector('#btn').addEventListener('click', getUsersCompleted, false); 9 | } 10 | -------------------------------------------------------------------------------- /src/users/mock-request.mjs: -------------------------------------------------------------------------------- 1 | let fakerMod; 2 | 3 | if (typeof process !== 'undefined') { 4 | fakerMod = await import('@faker-js/faker'); 5 | } else { 6 | fakerMod = await import('https://cdn.skypack.dev/@faker-js/faker'); 7 | } 8 | 9 | const { faker } = fakerMod; 10 | const seed = (new Date().getTime() / 1000 / 3600 / 24)|0; // daily 11 | faker.seed(seed); 12 | 13 | async function sleep(ms) { 14 | return new Promise((resolve) => setTimeout(resolve, ms)); 15 | } 16 | 17 | export async function request(url) { 18 | const shouldFail = Math.random() < 0.1; 19 | const data = { 20 | userId: faker.datatype.uuid(), 21 | username: faker.name.fullName(), 22 | age: +faker.random.numeric(2), 23 | job: faker.name.jobTitle(), 24 | registeredAt: faker.date.past(), 25 | requestedUrl: url, 26 | }; 27 | 28 | if (shouldFail) { 29 | throw new Error('Something went wrong on the server!'); 30 | } 31 | 32 | await sleep((Math.random() * 1000)|0); 33 | 34 | return data; 35 | } 36 | -------------------------------------------------------------------------------- /src/users/users.completed.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 서버에 사용자 정보를 요청하고 콘솔에 출력하는 함수 3 | * async/await을 사용하여 코드를 개선해보세요. 4 | */ 5 | import { request } from './mock-request.mjs'; 6 | 7 | export async function getUsers() { 8 | for (let i = 0; i < 10; i++) { 9 | // 디버깅이 쉽지 않은 비동기 동작의 특성상, 가능한 한 에러를 감지하는 범위를 좁게 가져가는 것이 좋습니다. 10 | try { 11 | const user = await request('api/users/' + i); 12 | console.log(user); 13 | } catch(err) { 14 | console.error(err); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/users/users.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 서버에 사용자 정보를 요청하고 콘솔에 출력하는 함수 3 | * async/await을 사용하여 코드를 개선해보세요. 4 | */ 5 | import { request } from './mock-request.mjs'; 6 | 7 | export function getUsers() { 8 | for (let i = 0; i < 10; i++) { 9 | request('api/users/' + i).then((user) => console.log(user)).catch(err => console.error(err)); 10 | } 11 | } 12 | --------------------------------------------------------------------------------