├── .github └── workflows │ └── documentation.yml ├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── blog ├── 2021-08-26-welcome │ ├── imfp.jpeg │ ├── index.mdx │ └── logo.png └── authors.yml ├── docs ├── 01-chapter_3.mdx ├── 02-chapter_4.mdx ├── 03-chapter_5.mdx ├── 04-chapter_6.mdx ├── 05-chapter_7.mdx ├── 06-chapter_8.mdx ├── 07-chapter_9.mdx ├── 08-chapter_10.mdx ├── 09-chapter_11.mdx ├── 10-chapter_12.mdx ├── 11-chapter_13.mdx ├── 12-chapter_14.mdx ├── 13-chapter_15.mdx ├── 14-chapter_16.mdx ├── 15-chapter_17.mdx ├── 16-chapter_18.mdx ├── Authors.module.css ├── Authors.tsx ├── chap6.png └── images │ ├── 7-1.jpeg │ ├── 8-1.jpeg │ ├── 8-2.jpeg │ ├── 8-3.jpeg │ ├── 8-4.jpeg │ └── 8-5.jpeg ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src ├── components │ ├── HomepageFeatures.js │ └── HomepageFeatures.module.css ├── css │ └── custom.css └── pages │ ├── index.js │ ├── index.module.css │ └── markdown-page.md ├── static ├── .nojekyll └── img │ ├── docusaurus.png │ ├── favicon.ico │ ├── imfp.jpeg │ ├── logo.svg │ ├── tutorial │ ├── docsVersionDropdown.png │ └── localeDropdown.png │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── yarn.lock /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Review gh actions docs if you want to further define triggers, paths, etc 8 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 9 | 10 | jobs: 11 | deploy: 12 | name: Deploy to GitHub Pages 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | cache: yarn 20 | 21 | - name: Install dependencies 22 | run: yarn install --frozen-lockfile 23 | - name: Build website 24 | run: yarn build 25 | 26 | # Popular action to deploy to GitHub Pages: 27 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 28 | - name: Deploy to GitHub Pages 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.AUTH_TOKEN }} 32 | # Build output to publish to the `gh-pages` branch: 33 | publish_dir: ./build 34 | # The following lines assign commit authorship to the official 35 | # GH-Actions bot for deploys to `gh-pages` branch: 36 | # https://github.com/actions/checkout/issues/13#issuecomment-724415212 37 | # The GH actions bot is used by default if you didn't specify the two fields. 38 | # You can swap them out with your own user credentials. 39 | user_name: github-actions[bot] 40 | user_email: 41898282+github-actions[bot]@users.noreply.github.com 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 프로젝트 명 2 | 3 | ## 부제 4 | 5 | ## 👨‍👩‍👧‍👦 참여자 6 | 7 | - [positiveko](https://github.com/positiveko) 8 | - [saengmotmi](https://github.com/saengmotmi) 9 | - [Jtree03](https://github.com/jtree03) 10 | - [yongsk0066](https://github.com/yongsk0066) 11 | 12 | ## 📅 진행 기간 13 | 2022.09 ~ 2022.12 14 | ## 사이트 15 | 16 | - 주소: [https://dev-in-book.github.io/imfp/](https://dev-in-book.github.io/imfp/) (Docusaurus를 기반으로 제작하였습니다.) 17 | 18 | ### Installation 19 | 20 | ``` 21 | $ yarn install 22 | ``` 23 | 24 | ### Local Development 25 | 26 | ``` 27 | $ yarn start 28 | ``` 29 | 30 | ### Build 31 | 32 | ``` 33 | $ yarn build 34 | ``` 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /blog/2021-08-26-welcome/imfp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/blog/2021-08-26-welcome/imfp.jpeg -------------------------------------------------------------------------------- /blog/2021-08-26-welcome/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: member 3 | title: Member 4 | authors: [positiveko, saengmotmi, Jtree03, yongsk0066] 5 | tags: [member, introduce] 6 | --- 7 | 8 | import logo from './imfp.jpeg'; 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /blog/2021-08-26-welcome/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/blog/2021-08-26-welcome/logo.png -------------------------------------------------------------------------------- /blog/authors.yml: -------------------------------------------------------------------------------- 1 | positiveko: 2 | name: Positiveko 3 | title: Front End Engineer 4 | url: https://github.com/positiveko 5 | image_url: https://github.com/positiveko.png 6 | 7 | saengmotmi: 8 | name: saengmotmi 9 | title: Front End Engineer 10 | url: https://github.com/saengmotmi 11 | image_url: https://github.com/saengmotmi.png 12 | 13 | Jtree03: 14 | name: Jtree03 15 | title: Software Engineer 16 | url: https://github.com/jtree03 17 | image_url: https://github.com/jtree03.png 18 | 19 | yongsk0066: 20 | name: yongsk0066 21 | title: Front End Engineer 22 | url: https://github.com/yongsk0066 23 | image_url: https://github.com/yongsk0066.png 24 | 25 | undefined: 26 | name: undefined 27 | -------------------------------------------------------------------------------- /docs/01-chapter_3.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 3장 액션과 계산, 데이터의 차이를 알기 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 액션과 계산, 데이터가 어떻게 다른지 배웁니다. 10 | - 문제에 대해 생각하거나 코드를 작성할 때 또는 코드를 읽을 때 액션과 계산, 데이터를 구분해서 적용해봅니다. 11 | - 액션이 코드 전체로 퍼질 수 있다는 것을 이해합니다. 12 | - 이미 있는 코드에서 어떤 부분이 액션인지 찾아봅니다. 13 | 14 | ## 액션과 계산, 데이터 15 | 16 | 우선, 액션과 계산, 데이터에 대한 정의를 보겠습니다. 17 | 18 | **액션**: 실행 시점과 횟수에 의존합니다. 19 | 20 | **계산**: 입력으로 출력을 계산합니다. 21 | 22 | **데이터**: 이벤트에 대한 사실을 나타냅니다. 23 | 24 | ### 액션과 계산, 데이터를 적용하여 보는 개발 과정 25 | 26 | 1. 문제에 대해 생각하기 27 | 28 | 아직 코딩을 하기 전 단계이지만 액션과 계산, 데이터로 나눠서 생각하여 설계합니다. 29 | 30 | 2. 코딩하기 31 | 32 | 최대한 액션에서 계산을 빼냅니다. 33 | 또 계산에서 데이터를 분리할 수 있는지 생각합니다. 34 | 더 나아가 액션이 계산이 될 수 있는지, 계산은 데이터가 될 수 있는지 고민합니다. 35 | 36 | 3. (유지보수를 위해) 코드 읽기 37 | 38 | 액션과 계산, 데이터 중 어디에 속하는지 잘 봐야합니다. 39 | 특히, 액션은 실행하는 시점에 따라 결과가 달라지니 유의해서 봐야합니다. 40 | 41 | ## 액션과 계산, 데이터는 어디에나 적용할 수 있습니다 42 | 43 | ### 장보기 과정 44 | 45 | | A,C,D | 장보기 과정 | 설명 | 46 | | -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | 47 | | `Action` | 냉장고 확인하기 | 냉장고의 문을 열기전 까지 고양이가 있는 상태와 없는 상태가 동시에 존재하므로 액션입니다. | 48 | | `Action` | 운전해서 상점으로 가기 | 운전하는 것은 연료라는 데이터(혹은 상태)를 소모하므로 액션입니다. | 49 | | `Action` | 필요한 것 구입하기 | 필요하다라는 것은 시간에 따라 달라집니다. 지금은 카레가 먹고 싶어서 카레를 사지만 나중에는 아닐 수도 있기 때문에 액션입니다. | 50 | | `Action` | 운전해서 집으로 오기 | 운전해서 상점으로 가기랑 비슷하므로 액션입니다. | 51 | 52 | 모든 것이 액션이다를 결론으로 chapter 3 정리를 종료하겠습니다. 53 | 54 | --- 55 | 56 | 는 장난이고 이어서 작성하겠습니다. 57 | 58 | 각 단계 별로 세부적인 부분은 계산과 데이터로 나눌 수 있습니다. 59 | 60 | ### 냉장고 확인하기 61 | 62 | 냉장고란 무엇일까요? 냉장고는 우리의 중요한 식량을 신선하게 유지하는 곳 입니다. 63 | 64 | 그리고 우리는 때에 따라 냉장고의 식료품을 꺼내서 먹습니다. 65 | 66 | 여기서 식료품은 데이터이고 냉장고는 데이터베이스라고 생각하면 되겠습니다. 67 | 68 | 현재 재고(데이터) - ... 69 | 70 | 냉장고의 상태(식료품)는 시간에 따라 달라집니다. 71 | 72 | 그러므로 액션입니다. 73 | 74 | ### 운전해서 상점으로 가기 75 | 76 | 데이터가 숨어 있긴 하나 다른 기술적인 작업이 들어가지 않을 거라 생략하겠습니다. 77 | 78 | ### 필요한 것 구입하기 79 | 80 | 필요한 것 구입하기에서는 단계를 다음과 같이 더 나눌 수 있습니다. 81 | 82 | ... - 필요한 재고(데이터) - 재고 빼기(계산) - 장보기 목록(데이터) - 목록에 있는 것 구입하기(액션) 83 | 84 | ### 운전해서 집으로 오기 85 | 86 | 여기서 다루는 범위가 아니므로 이것도 생략하겠습니다. 87 | 88 | p.35 사진 추가 89 | 90 | ## 장보기 과정에서 배운 것 91 | 92 | 1. 액션과 계산, 데이터는 어디에나 적용할 수 있습니다. 93 | 94 | 2. 액션 안에는 계산과 데이터, 또 다른 액션이 숨어 있을지도 모릅니다. 95 | 96 | 3. 계산은 더 작은 계산과 데이터로 나누고 연결할 수 있습니다. 97 | 98 | 4. 데이터는 데이터만 조합할 수 있습니다. 99 | 100 | 5. 계산은 때로 '우리 머릿속에서' 일어납니다. 101 | 102 | ## 새로 만드는 코드에 함수형 사고 적용하기 103 | 104 | ### 쿠폰독의 새로운 마케팅 전략 105 | 106 | - 쿠폰에 관심 있는 구독자들에게 이메일로 쿠폰을 매주 보내주는 서비스입니다. 107 | 108 | - 친구 10명을 추천하면 더 좋은 쿠폰을 보내주려고 합니다. 109 | 110 | - 이메일과 추천 친구수를 가지는 테이블이 있습니다. 111 | 112 | | email | rec_count | 113 | | ----------------- | --------- | 114 | | john@coldmain.com | 2 | 115 | | sam@pmail.co | 16 | 116 | | linda1989@oal.com | 1 | 117 | | jan1940@ahoy.com | 0 | 118 | | mrbig@pmail.co | 25 | 119 | | lol@lol.lol | 0 | 120 | 121 | - 등급에 따라 부여되는 쿠폰 데이터를 가지는 테이블이 있습니다. 122 | 123 | | code | rank | 124 | | -------------- | ---- | 125 | | MAYDISCOUNT | good | 126 | | 10PERCENT | bad | 127 | | PROMOTION45 | best | 128 | | IHEARTYOU | bad | 129 | | GETADEAL | best | 130 | | ILIKEDISCOUNTS | good | 131 | 132 | - best 쿠폰은 추천을 많이 한 사용자를 위한 쿠폰입니다. 133 | 134 | - good 쿠폰은 모든 사용자들에게 전달되는 쿠폰입니다. 135 | 136 | - bad 쿠폰은 사용하지 않는 쿠폰입니다. 137 | 138 | ## 쿠폰 보내는 과정을 그려보기 139 | 140 | 1. 데이터베이스에서 구독자 목록을 가져오기 141 | 142 | 2. 데이터베이스에서 쿠폰 목록 가져오기 143 | 144 | 3. 보내야할 이메일 목록 만들기 145 | 146 | 4. 이메일 전송하기 147 | 148 | ## 쿠폰 보내는 과정 구현하기 149 | 150 | ### 데이터베이스에서 가져온 구독자 데이터 151 | 152 | ```typescript 153 | interface Subscriber { 154 | email: string; 155 | recommendCount: number; 156 | } 157 | 158 | // ex 159 | const subscriber: Subscriber = { 160 | email: 'sam@pmail.com', 161 | recommendCount: 16, 162 | }; 163 | ``` 164 | 165 | ### 쿠폰 등급을 결정하는 것은 함수(계산)입니다. 166 | 167 | ```typescript 168 | type Rank = 'best' | 'good' | 'bad'; 169 | 170 | function getRankBySubscriber(subscriber: Subscriber): Rank { 171 | if (subscriber.recommendCount >= 10) return 'best'; 172 | else return 'good'; 173 | } 174 | ``` 175 | 176 | ### 데이터베이스에서 가져온 쿠폰 데이터 177 | 178 | ```typescript 179 | interface Coupon { 180 | code: string; 181 | rank: Rank; 182 | } 183 | // ex 184 | const coupon: Coupon = { 185 | code: '10PERCENT', 186 | rank: 'bad', 187 | }; 188 | ``` 189 | 190 | ### 특정 등급의 쿠폰 목록을 선택하는 것은 함수(계산)입니다. 191 | 192 | ```typescript 193 | function getCouponsByRank(coupons: Coupon[], rank: Rank): Coupon[] { 194 | return coupons.filter((coupon) => coupon.rank === rank); 195 | } 196 | ``` 197 | 198 | ### 이메일은 그냥 데이터 입니다. 199 | 200 | ```typescript 201 | interface Email { 202 | from: string; 203 | to: string; 204 | subject: string; 205 | body: string; 206 | } 207 | // ex 208 | const email: Email = { 209 | from: 'newsletter@coupondog.co', 210 | to: 'sam@pmail.com', 211 | subject: 'Your weekly coupons inside', 212 | body: 'Here are your coupons ...', 213 | }; 214 | ``` 215 | 216 | ### 구독자가 받을 이메일을 계획하는 계산 217 | 218 | ```typescript 219 | function writeEmailForSubscriber( 220 | subscriber: Subscriber, 221 | goodCoupons: Coupon[], 222 | bestCoupons: Coupon[] 223 | ): Email { 224 | const subscriberRank = getRankBySubscriber(subscriber); 225 | switch (subsriberRank) { 226 | case 'best': 227 | return { 228 | from: 'newsletter@coupondog.co', 229 | to: subscriber.email, 230 | subject: 'Your best weekly coupons inside', 231 | body: `Here are the best coupons: ${bestCoupons.join(', ')}`, 232 | }; 233 | case 'good': 234 | return { 235 | from: 'newsletter@coupondog.co', 236 | to: subscriber.email, 237 | subject: 'Your best weekly coupons inside', 238 | body: `Here are the good coupons: ${goodCoupons.join(', ')}`, 239 | }; 240 | default: 241 | throw new Error('rank not provided'); 242 | } 243 | } 244 | ``` 245 | 246 | ### 보낼 이메일 목록을 준비하기 247 | 248 | ```typescript 249 | function writeEmailsForSubscribers( 250 | subscribers: Subscriber[], 251 | goodCoupons: Coupon[], 252 | bestCoupons: Coupon[] 253 | ): Email[] { 254 | return subscribers.map((subsriber) => 255 | writeEmailForSubsriber(subscriber, goodCoupons, bestCoupons) 256 | ); 257 | } 258 | ``` 259 | 260 | ### 이메일 보내기는 액션입니다. 261 | 262 | ```typescript 263 | interface EmailSystem { 264 | send: (email: Email) => void; 265 | } 266 | 267 | async function sendWeeklyEmail(emailSystem: EmailSystem) { 268 | const [subscribers, coupons] = await Promise.all([ 269 | readSubscribers(), 270 | readCoupons(), 271 | ]); 272 | const goodCoupons = getCouponsByRank(coupons, 'good'); 273 | const bestCoupons = getCouponsByRank(coupons, 'best'); 274 | const emails = writeEmailsForSubscribers( 275 | subscribers, 276 | goodCoupons, 277 | bestCoupons 278 | ); 279 | emails.forEach((email) => emailSystem.send(email)); 280 | } 281 | ``` 282 | 283 | ## 액션은 코드 전체로 퍼집니다. 284 | 285 | 액션을 호출하면 액션이 됩니다. 286 | 287 | ## 액션은 다양한 형태로 나타납니다. 288 | 289 | - 팝업창 띄우기 290 | 291 | - 콘솔 띄우기 292 | 293 | - new Date() 294 | 295 | - 공유 가변값 296 | 297 | 언제 부르는지 또는 얼마나 부르는지에 따라 다른 결과를 나타내므로 액션입니다. 298 | 어느 곳에서나 사용할 수 있기 때문에 코드 전체로 퍼지기 쉽습니다. 299 | 300 | ## 결론 301 | 302 | 계산은 계획이나 결정을 할 때 적용했고 데이터는 계획하거나 결정한 결과였습니다. 마지막으로, 액션을 통해 계산으로 만든 계획을 실행할 수 있었습니다. 303 | 304 | ## 요점정리 305 | 306 | - 함수형 프로그래머는 액션과 계산, 데이터를 구분합니다. 307 | 308 | - 액션은 실행 시점이나 횟수에 의존합니다. 309 | 310 | - 계산은 입력값으로 출력값을 만드는 것(순수 함수)입니다. 311 | 312 | - 데이터는 이벤트에 대한 사실입니다. 313 | 314 | - 함수형 프로그래머는 액션보다 계산을 좋아하고 계산보다 데이터를 좋아합니다. 315 | 316 | - 계산은 테스트하기 쉽습니다. 317 | -------------------------------------------------------------------------------- /docs/02-chapter_4.mdx: -------------------------------------------------------------------------------- 1 | # 4장 액션에서 계산 빼내기 2 | 3 | ## 이번 장에서 살펴볼 내용 4 | 5 | - 어떻게 함수로 정보가 들어가고 나오는지 살펴봅니다. 6 | 7 | - 테스트하기 쉽고 재사용성이 좋은 코드를 만들기 위한 함수형 기술에 대해 알아봅니다. 8 | 9 | - 액션에서 계산을 빼내는 방법을 배웁니다. 10 | 11 | ## MegaMart.com에 오신 것을 환영합니다 12 | 13 | 이번 장은 리팩토링 장이라서 코드 위주로 설명하겠습니다. 14 | 15 | ```typescript 16 | interface Item { 17 | name: string; 18 | price: number; 19 | } 20 | 21 | const shoppingCart: Item[] = []; 22 | let shoppingCartTotal = 0; 23 | 24 | function addItemToCart(name: string, price: number) { 25 | shoppingCart.push({ name, price }); 26 | calcCartTotal(); 27 | } 28 | 29 | function calcCartTotal() { 30 | shoppingCartTotal = 0; 31 | shoppingCart.forEach((item) => { 32 | shoppingCartTotal += item.price; 33 | }); 34 | setCartTotalDom(); 35 | } 36 | ``` 37 | 38 | ## 무료 배송비 계산하기 39 | 40 | ### 새로운 요구사항 41 | 42 | MegaMart는 구매 합계가 20달러 이상이면 무료 배송을 해주려고 합니다. (비즈니스 로직의 변화) 43 | 44 | 그래서 해당 제품을 카트에 담았을 때 20달러가 넘으면 무료 배송 아이콘을 표시해주려고 합니다. 45 | 46 | ### 절차적인 방법으로 구현하기 47 | 48 | ```typescript 49 | function updateShippingIcons() { 50 | const buyButtons = getBuyButtonsDom(); 51 | buyButtons.forEach((buyButton) => { 52 | if (buyButton.item.price + shoppingCartTotal >= 20) { 53 | buyButton.showFreeShippingIcon(); 54 | } else { 55 | buyButton.hideFreeShippingIcon(); 56 | } 57 | }); 58 | } 59 | 60 | function calcCartTotal() { 61 | shoppingCartTotal = 0; 62 | shoppingCart.forEach((item) => { 63 | shoppingCartTotal += item.price; 64 | }); 65 | setCartTotalDom(); 66 | updateShippingIcons(); 67 | } 68 | ``` 69 | 70 | ## 세금 계산하기 71 | 72 | ### 다음 요구사항 73 | 74 | 장바구니의 금액 합계가 바뀔 때마다 세금을 다시 계산해야 합니다. 75 | 76 | ```typescript 77 | function updateTaxDom() { 78 | setTaxDom(shoppingCartTotal * 0.1); 79 | } 80 | 81 | function calcCartTotal() { 82 | shoppingCartTotal = 0; 83 | shoppingCart.forEach((item) => { 84 | shoppingCartTotal += item.price; 85 | }); 86 | setCartTotalDom(); 87 | updateShippingIcons(); 88 | updateTaxDom(); 89 | } 90 | ``` 91 | 92 | ## 테스트하기 쉽게 만들기 93 | 94 | ### 지금 코드는 비즈니스 규칙을 테스트하기 어렵습니다. 95 | 96 | 코드가 바뀔 때마다 조지는 아래와 같은 테스트를 만들어야 합니다. 97 | 98 | 1. 브라우저 설정하기 99 | 2. 페이지 로드하기 100 | 3. 장바구니에 제품 담기 버튼 클릭 101 | 4. DOM 이 업데이트될 때까지 기다리기 102 | 5. DOM 에서 값 가져오기 103 | 6. 가져온 문자열 값을 숫자로 바꾸기 104 | 7. 예상하는 값과 비교하기 105 | 106 | ### 조지의 코드 설명 107 | 108 | ```typescript 109 | function updateTaxDom() { 110 | setTaxDom(shoppingCartTotal * 0.1); 111 | } 112 | ``` 113 | 114 | shoppingCartTotal: 테스트하기 전에 전역변수를 설정해야 합니다. 115 | setTaxDom: 결괏값을 얻을 방법은 DOM에서 값을 가져오는 방법뿐입니다. 116 | shoppingCartTotal \* 0.1: 최종적으로 테스트 해야하는 비즈니스 로직입니다. 117 | 118 | ### 테스트 개선을 위한 조지의 제안 119 | 120 | - DOM 업데이트와 비즈니스 규칙은 분리되어야 합니다. 121 | - 전역변수가 없어야 합니다! 122 | 123 | 위 제안은 함수형 프로그래밍과 잘 맞습니다. 124 | 125 | ## 재사용하기 쉽게 만들기 126 | 127 | ### 결제팀과 배송팀이 우리 코드를 사용하려고 합니다. 128 | 129 | 하지만 다음과 같은 이유로 재사용할 수 없었습니다. 130 | 131 | - 장바구니 정보를 전역변수에서 읽어오고 있지만, 결제팀과 배송팀은 데이터베이스에서 장바구니 정보를 읽어 와야 합니다. 132 | - 결과를 보여주기 위해 DOM을 직접 바꾸고 있지만, 결제팀은 영수증을, 배송팀은 운송장을 출력해야 합니다. 133 | 134 | ### 개발팀 제나의 코드 설명 135 | 136 | ```typescript 137 | function updateShippingIcons() { 138 | const buyButtons = getBuyButtonsDom(); 139 | buyButtons.forEach((buyButton) => { 140 | // 결제팀, 배송팀이 사용하려는 비즈니스 규칙 141 | // but, 전역 변수가 있음 142 | if (buyButton.item.price + shoppingCartTotal >= 20) { 143 | buyButton.showFreeShippingIcon(); // DOM이 있어야함 144 | } else { 145 | buyButton.hideFreeShippingIcon(); // DOM이 있어야함 146 | } 147 | }); 148 | } // 리턴값 없음 149 | ``` 150 | 151 | ### 개발팀 제나의 제안 152 | 153 | - 전역 변수에 의존하지 않아야 합니다. (부수효과 제거) 154 | - DOM을 사용할 수 있는 곳에서 실행된다고 가정하면 안 됩니다. (클린 아키텍처, 선택지는 나중에 선택할 수 있게 만들기) 155 | - 함수가 결괏값을 리턴해야 합니다. (이 책에서 말하는 '계산'으로 만드려는 것 같습니다.) 156 | 157 | ## 액션과 계산, 데이터를 구분하기 158 | 159 | ```typescript 160 | interface Item { 161 | name: string; 162 | price: number; 163 | } 164 | 165 | const shoppingCart: Item[] = []; // A 166 | let shoppingCartTotal = 0; // A 167 | 168 | // A 169 | function addItemToCart(name: string, price: number) { 170 | shoppingCart.push({ name, price }); 171 | calcCartTotal(); 172 | } 173 | 174 | // A 175 | function updateShippingIcons() { 176 | const buyButtons = getBuyButtonsDom(); 177 | buyButtons.forEach((buyButton) => { 178 | if (buyButton.item.price + shoppingCartTotal >= 20) { 179 | buyButton.showFreeShippingIcon(); 180 | } else { 181 | buyButton.hideFreeShippingIcon(); 182 | } 183 | }); 184 | } 185 | 186 | // A 187 | function updateTaxDom() { 188 | setTaxDom(shoppingCartTotal * 0.1); 189 | } 190 | 191 | // A 192 | function calcCartTotal() { 193 | shoppingCartTotal = 0; 194 | shoppingCart.forEach((item) => { 195 | shoppingCartTotal += item.price; 196 | }); 197 | setCartTotalDom(); 198 | updateShippingIcons(); 199 | updateTaxDom(); 200 | } 201 | ``` 202 | 203 | ## 함수에는 입력과 출력이 있습니다. 204 | 205 | ```typescript 206 | let total = 0; 207 | function addToTotal(amount: number) { 208 | // 인자는 명시적 입력입니다. 209 | console.log(`Old total: ${total}`); // 전역변수는 암묵적 입력입니다. 210 | // console.log 는 암묵적 출력입니다. 211 | total += amount; // 전역 변수를 변경하는 것도 암묵적 출력입니다. 212 | return total; // 리턴 값은 명시적 출력입니다. 213 | } 214 | ``` 215 | 216 | - 암묵적인 뭐시기가 있으면 액션이 됩니다. (부수효과를 조금더 체계적으로 정리하는 듯 합니다.) 217 | 218 | ## 테스트와 재사용성은 입출력과 관련 있습니다. 219 | 220 | - DOM 업데이트와 비즈니스 규칙은 분리되어야 합니다. 221 | - 전역변수가 없어야 합니다. 222 | - 전역변수에 의존하지 않아야 합니다. 223 | - DOM 을 사용할 수 있는 곳에서 실행된다고 가정하면 안 됩니다. 224 | - 함수가 결괏값을 리턴해야 합니다. 225 | 226 | ## 액션에서 계산 빼내기 227 | 228 | ### 서브루틴 추출하기 229 | 230 | ```typescript 231 | function calcTotal() { 232 | shoppingCartTotal = 0; 233 | shoppingCart.forEach((item) => { 234 | shoppingCartTotal += item.price; 235 | }); 236 | } 237 | 238 | function calcCartTotal() { 239 | calcTotal(); 240 | setCartTotalDom(); 241 | updateShippingIcons(); 242 | updateTaxDom(); 243 | } 244 | ``` 245 | 246 | ### 암묵적 입출력을 없앤 코드 247 | 248 | ```typescript 249 | function calcCartTotal() { 250 | shoppingCartTotal = 0; 251 | shoppingCart.forEach((item) => { 252 | shoppingCartTotal += item.price; 253 | }); 254 | setCartTotalDom(); 255 | updateShippingIcons(); 256 | updateTaxDom(); 257 | } 258 | ``` 259 | 260 | ```typescript 261 | function calcTotal(cart: Item[]) { 262 | let total = 0; 263 | cart.forEach((item) => { 264 | total += item.price; 265 | }); 266 | return total; 267 | } 268 | 269 | function calcCartTotal() { 270 | shoppingCartTotal = calcTotal(shoppingCart); 271 | setCartTotalDom(); 272 | updateShippingIcons(); 273 | updateTaxDom(); 274 | } 275 | ``` 276 | 277 | ### 조지와 제나의 모든 고민은 해결되었습니다. 278 | 279 | - DOM 업데이트와 비즈니스 규칙은 분리되어야 합니다. 280 | - 전역변수가 없어야 합니다. 281 | - 전역변수에 의존하지 않아야 합니다. 282 | - DOM 을 사용할 수 있는 곳에서 실행된다고 가정하면 안 됩니다. 283 | - 함수가 결괏값을 리턴해야 합니다. 284 | 285 | ## 액션에서 또 다른 계산 빼내기 286 | 287 | ```typescript 288 | function addItemToCart(name: string, price: number) { 289 | shoppingCart.push({ name, price }); 290 | calcCartTotal(); 291 | } 292 | ``` 293 | 294 | ```typescript 295 | function addItem(cart: Item[], name: string, price: number) { 296 | const newCart = [...cart]; 297 | newCart.push({ name, price }); 298 | return newCart; 299 | } 300 | 301 | function addItemToCart(name: string, price: number) { 302 | addItem(name, price); 303 | calcCartTotal(); 304 | } 305 | ``` 306 | 307 | ## 전체 코드를 봅시다 308 | 309 | ```typescript 310 | interface Item { 311 | name: string; 312 | price: number; 313 | } 314 | 315 | let shoppingCart: Item[] = []; // A 316 | let shoppingCartTotal = 0; // A 317 | 318 | // A 319 | function calcCartTotal() { 320 | shoppingCartTotal = calcTotal(shoppingCart); 321 | setCartTotalDom(); 322 | updateShippingIcons(); 323 | updateTaxDom(); 324 | } 325 | 326 | // C 327 | function calcTotal(cart: Item[]) { 328 | let total = 0; 329 | cart.forEach((item) => { 330 | total += item.price; 331 | }); 332 | return total; 333 | } 334 | 335 | function addItemToCart(name: string, price: number) { 336 | shoppingCart = addItem(shoppingCart, name, price); 337 | calcCartTotal(); 338 | } 339 | 340 | // C 341 | function addItem(cart: Item[], name: string, price: number) { 342 | const newCart = [...cart]; 343 | newCart.push({ name, price }); 344 | return newCart; 345 | } 346 | 347 | // A 348 | function updateShippingIcons() { 349 | const buyButtons = getBuyButtonsDom(); 350 | buyButtons.forEach((buyButton) => { 351 | if (getFreeShipping(shoppingCartTotal, buyButton.item.price)) { 352 | buyButton.showFreeShippingIcon(); 353 | } else { 354 | buyButton.hideFreeShippingIcon(); 355 | } 356 | }); 357 | } 358 | 359 | // C 360 | function getFreeShipping(total: number, itemPrice: number) { 361 | return itemPrice + total >= 20; 362 | } 363 | 364 | // A 365 | function updateTaxDom() { 366 | setTaxDom(calcTax(shoppingCartTotal)); 367 | } 368 | 369 | // C 370 | function calcTax(amount: number) { 371 | return amount * 0.1; 372 | } 373 | ``` 374 | 375 | ## 결론 376 | 377 | 슬픈 이야기라 생략하겠읍니다. 378 | 379 | ## 요점정리 380 | 381 | - 액션은 암묵적인 입력 또는 출력을 가지고 있습니다. 382 | - 계산의 정의에 따르면 계산은 암묵적인 입력이나 출력이 없어야 합니다. 383 | - 공유 변수(전역변수 같은)는 일반적으로 암묵적 입력 또는 출력이 됩니다. 384 | - 암묵적 입력은 인자로 바꿀 수 있씁니다. 385 | - 암묵적 출력은 리턴값으로 바꿀 수 있습니다. 386 | - 함수형 원칙을 적용하면 액션은 줄어들고 계산은 늘어난다는 것을 확인했습니다. 387 | -------------------------------------------------------------------------------- /docs/03-chapter_5.mdx: -------------------------------------------------------------------------------- 1 | # 5장 더 좋은 액션 만들기 2 | 3 | ## 이번 장에서 살펴볼 내용 4 | 5 | - 암묵적 입력과 출력을 제거, 재사용 좋은 코드를 만드는 법 6 | - 복잡하게 엉킨 코드를 풀어, 좋은 구조 만들기 7 | 8 | ## 비지니스 요구 사항과 설계, 함수를 맞추기 9 | 10 | 4장의 함수들을 보았을 때, 요구사항에 맞지 않는 것들이 있습니다. 11 | 12 | 요구사항은 장바구니에 담긴 제품을 주문할때, 무료인지 확인 하는 것입니다. 그러나 제품의 합계와 가격으로 판단하고 있습니다. 13 | 14 | 또한 중복되는 코드가 존재합니다. 15 | 16 | 이는 나쁜 것은 아니지만, 냄새가 좀 나네요. 책에서는 **코드의 냄새** 라고 부릅니다. 17 | 18 | ```typescript 19 | function getFreeShipping(total: number, itemPrice: number) { 20 | return itemPrice + total >= 20; 21 | } 22 | 23 | function calcTotal(cart: Item[]) { 24 | let total = 0; 25 | cart.forEach((item) => { 26 | total += item.price; 27 | }); 28 | return total; 29 | } 30 | 31 | // 변경 32 | function getFreeShipping(cart: item[]) { 33 | return calcTotal(cart) >= 20; 34 | } 35 | 36 | function updateShippingIcons() { 37 | const buyButtons = getBuyButtonsDom(); 38 | buyButtons.forEach((buyButton) => { 39 | const newCart = addItem( 40 | shippingCart, 41 | buyButton.item.name, 42 | buyButton.item.price 43 | ); 44 | if (getFreeShipping(newCart)) { 45 | buyButton.showFreeShippingIcon(); 46 | } else { 47 | buyButton.hideFreeShippingIcon(); 48 | } 49 | }); 50 | } 51 | ``` 52 | 53 | 위에서는 동작을 바꿨기 때문에 리팩터링이라고 부를 수 없습니다. 54 | 55 | ## 원칙 : 암묵적 입력과 출력은 적을 수록 좋습니다 56 | 57 | - 암묵적 입력 : 인자가 아닌 모든 입력 58 | - 암묵적 출력 : 리턴값이 아닌 모든 출력 59 | 60 | `암묵적 입력,출력`이 있다면 다른 컴포넌트와 `강하게 연결된 컴포넌트`라고 볼 수 있습니다. 모듈이라 부를 수 없다. 61 | 62 | 반면, 명시적 입출력은 모듈에 있는 커넥터와 같다. 63 | 64 | 액션은 예측하기 어렵기 때문에 줄이는 것이 좋다! 65 | 66 | ## 암묵적 입력과 출력 줄이기 67 | 68 | 암묵적 입력인 전역변수 shippingCart 를 명시적 입력으로 바꿔서 제거 69 | 70 | ```typescript 71 | function updateShippingIcons(cart: item[]) { 72 | const buyButtons = getBuyButtonsDom(); 73 | buyButtons.forEach((buyButton) => { 74 | const newCart = addItem(cart, buyButton.item.name, buyButton.item.price); 75 | if (getFreeShipping(newCart)) { 76 | buyButton.showFreeShippingIcon(); 77 | } else { 78 | buyButton.hideFreeShippingIcon(); 79 | } 80 | }); 81 | } 82 | ``` 83 | 84 | ## 코드 다시 살펴보기 85 | 86 | ```typescript 87 | function addItemToCart(name: string, price: number) { 88 | shoppingCart = addItem(shoppingCart, name, price); 89 | calcCartTotal(); 90 | } 91 | 92 | function calcCartTotal(cart: item[]) { 93 | const total = calcTotal(cart); 94 | setCartTotalDom(total); 95 | updateShippingIcons(cart); 96 | updateTaxDom(total); 97 | } 98 | 99 | // 변경 100 | function addItemToCart(name: string, price: number) { 101 | shoppingCart = addItem(shoppingCart, name, price); 102 | const total = calcTotal(shoppingCart); 103 | setCartTotalDom(total); 104 | updateShippingIcons(shoppingCart); 105 | updateTaxDom(total); 106 | } 107 | ``` 108 | 109 | ## 계산 분류하기 110 | 111 | 의미 있는 계층에 알기 위해 계산을 분류 112 | 113 | - C : cart 에 대한 동작 114 | - I : Item에 대한 동작 115 | - B : 비지니스 로직 116 | 117 | ```typescript 118 | // C I 119 | function addItem(cart: Item[], name: string, price: number) { 120 | const newCart = [...cart]; 121 | newCart.push({ name, price }); 122 | return newCart; 123 | } 124 | 125 | // C I B 126 | function calcTotal(cart: Item[]) { 127 | let total = 0; 128 | cart.forEach((item) => { 129 | total += item.price; 130 | }); 131 | return total; 132 | } 133 | 134 | // B 135 | function getFreeShipping(cart: item[]) { 136 | return calcTotal(cart) >= 20; 137 | } 138 | 139 | // B 140 | function calcTax(amount: number) { 141 | return amount * 0.1; 142 | } 143 | ``` 144 | 145 | ## 원칙 : 설계는 엉켜있는 코드를 푸는 것이다 146 | 147 | - 재사용하기 쉽다. 148 | - 유지보수가 쉽다. 149 | - 테스트하기 쉽다. 150 | 151 | ## addItem 개선 152 | 153 | ### 분리해내기 154 | 155 | ```typescript 156 | function addItem(cart: Item[], name: string, price: number) { 157 | const newCart = [...cart]; // 1 배열을 복사 158 | newCart.push({ name, price }); // 2 객체를 만들고, 3 복사본에 추가 159 | return newCart; // 4 복사본 리턴 160 | } 161 | addItem(shoppingCart, 'shoe', 3.45); 162 | ``` 163 | 164 | ```typescript 165 | // 생성자 함수 166 | function makeCartItem(name: string, price: number) { 167 | return { 168 | // 2 객체 생성 169 | name: name, 170 | price: price, 171 | }; 172 | } 173 | 174 | function addItem(cart: Item[], item: Item) { 175 | const newCart = [...cart]; // 1 배열을 복사 176 | newCart.push(item); // 3 복사본에 추가 177 | return newCart; // 4 복사본 리턴 178 | } 179 | 180 | addItem(shoppingCart, makeCartItem('shoe', 3.45)); 181 | ``` 182 | 183 | cart 의 구조를 바꿀 때 용이해졌다. 184 | 185 | ### 카피-온-라이트 패턴 빼내기 186 | 187 | 일반적인 이름으로 교체 188 | 189 | ```typescript 190 | function addElementLast(array: T[], elem: T): T[] { 191 | const newArr = [...array]; 192 | newArr.push(elem); 193 | return newArr; 194 | } 195 | ``` 196 | 197 | ## 계산 분류하기 2 198 | 199 | - C : cart 에 대한 동작 200 | - I : Item에 대한 동작 201 | - B : 비지니스 로직 202 | - **A** : 배열 유틸리티 203 | 204 | ```typescript 205 | // A 206 | function addElementLast(array: T[], elem: T): T[] { 207 | const newArr = [...array]; 208 | newArr.push(elem); 209 | return newArr; 210 | } 211 | 212 | // C 213 | function addItem(cart: Item[], item: Item) { 214 | const newCart = [...cart]; 215 | newCart.push(item); 216 | return newCart; 217 | } 218 | 219 | // I 220 | function makeCartItem(name: string, price: number) { 221 | return { 222 | name: name, 223 | price: price, 224 | }; 225 | } 226 | 227 | // C I B 228 | function calcTotal(cart: Item[]) { 229 | let total = 0; 230 | cart.forEach((item) => { 231 | total += item.price; 232 | }); 233 | return total; 234 | } 235 | 236 | // B 237 | function getFreeShipping(cart: item[]) { 238 | return calcTotal(cart) >= 20; 239 | } 240 | 241 | // B 242 | function calcTax(amount: number) { 243 | return amount * 0.1; 244 | } 245 | ``` 246 | 247 | ## 요점 정리 248 | 249 | - 암묵적 입출력은 `인자`와 `리턴값`으로 바꿔 없애자 250 | - 설계는 엉켜있는 것을 푸는 것, 풀린 것은 다시 합칠 수 있다. 251 | - 함수가 하나의 일만 하도록하면, 개념 중심으로 쉽게 구성 가능! 252 | -------------------------------------------------------------------------------- /docs/04-chapter_6.mdx: -------------------------------------------------------------------------------- 1 | # 6장 변경 가능한 데이터 구조를 가진 언어에서 불변성 유지하기 2 | 3 | ![chap6](./chap6.png) 4 | 5 | ## 이번 장에서 살펴볼 내용 6 | 7 | - 데이터가 바뀌지 않도록 카피-온-라이트 적용 8 | - 배열과 객체를 데이터에 쓸 수 있는 카피-온-라이트 만들기 9 | - 깊이 중첩된 데이터도 카피-온-라이트 잘 동작하게 만들기 10 | 11 | ## 카피-온-라이트 원칙 세 단계 12 | 13 | 1. 복사본 만들기 14 | 2. 복사본 변경하기 15 | 3. 복사본 리턴하기 16 | 17 | ```typescript 18 | function addElementLast(array: T[], elem: T): T[] { 19 | const newArr = [...array]; 20 | newArr.push(elem); 21 | return newArr; 22 | } 23 | ``` 24 | 25 | > 🤔 쓰기 일까... 읽기 일까... 26 | > 27 | > 데이터를 바꾸지 않고, 정보를 리턴했기 때문에 **"읽기"**입니다!!!! 28 | 29 | ## 카피-온-라이트로 쓰기를 읽기로 바꾸기! 30 | 31 | ```typescript 32 | function removeItemByName(cart: Item[], name: string) { 33 | let idx = null; 34 | cart.forEach((item) => { 35 | if (item.name === name) { 36 | idx = i; 37 | } 38 | }); 39 | if (idx !== null) { 40 | cart.splice(idx, 1); 41 | } 42 | } 43 | ``` 44 | 45 | ```typescript 46 | cart.splice(idx, 1); 47 | ``` 48 | 49 | shoppingCart가 들어가게 되면 전역변수가 변경되게 된다. 50 | 51 | 우리는 shoppingCart을 변경 불가능한 데이터로 쓰고 싶다. 불변성을 유지하고 싶다. 52 | 53 | ```typescript 54 | function removeItemByName(cart: Item[], name: string) { 55 | const newCart = [...cart]; // 1 복사본 만들기 56 | let idx = null; 57 | newCart.forEach((item) => { 58 | if (item.name === name) { 59 | idx = i; 60 | } 61 | }); 62 | if (idx !== null) { 63 | newCart.splice(idx, 1); // 2 복사본 변경하기 64 | } 65 | return newCart; // 3 복사본 리턴하기 66 | } 67 | ``` 68 | 69 | ## 일반화 하기 70 | 71 | ```typescript 72 | function removeItems(arr: T[], idx: number, count: number) { 73 | const copy = [...arr]; 74 | copy.splice(idx, count); 75 | return copy; 76 | } 77 | 78 | function removeItemByName(cart: Item[], name: string) { 79 | let idx = null; 80 | cart.forEach((item) => { 81 | if (item.name === name) { 82 | idx = i; 83 | } 84 | }); 85 | if (idx !== null) { 86 | return removeItems(cart, idx, 1); 87 | } 88 | return cart; 89 | } 90 | ``` 91 | 92 | ## 자바스크립트 배열 훑어보기 93 | 94 | - array는 자바스크립트에서 기본적인 collection 이다. 95 | - 배열은 다른 타입의 항목을 동시에 가질 수 있다. 96 | - 인덱스로 접근 할 수 있다. 97 | - 크기를 늘리거나 줄일 수 있다. 98 | 99 | ```typescript 100 | // 인덱싱 101 | const arr = [1, 2, 3, 4]; 102 | arr[2]; // 3 103 | 104 | // 할당 105 | arr[2] = 'abc'; 106 | arr; // [1,2, "abc", 4] 107 | 108 | // 길이 109 | arr.length; // 4 110 | 111 | // 끝에 추가, 지우기, 앞에 추가, 지우기 112 | arr.push(10); // 5, 길이 리턴 113 | arr.pop(); // 4, 지운 값 리턴 114 | arr.unshift(10); // 5, 길이 리턴 115 | arr.shift(); // 1, 지운 값 리턴 116 | 117 | // 복사 (얇게) 118 | arr.slice(); // [1,2,3,4] 119 | 120 | // 항목 삭제 121 | const arr = [1, 2, 3, 4]; 122 | arr.splice(1, 2); // [2,3], 지운 값 리턴 123 | ``` 124 | 125 | ## 쓰기, 읽기 같이 하는 함수 분리하기 126 | 127 | ### .shift() 128 | 129 | ```typescript 130 | const a = [1, 2, 3, 4]; 131 | const b = a.shift(); 132 | console.log(b); // 1 값을 리턴 133 | console.log(a); // [2,3,4] 값이 바뀌었다! 134 | ``` 135 | 136 | 1. 읽기와 쓰기 함수로 각각 분리한다. 137 | 2. 함수에서 값을 두개 리턴한다. 138 | 139 | ### 읽기와 쓰기 동작으로 분리 140 | 141 | **.shift() 의 읽기** : 첫번째 값을 리턴하는 동작 142 | 143 | ```typescript 144 | function firstElement(array: T[]) { 145 | return array[0]; 146 | } 147 | ``` 148 | 149 | **.shift() 의 쓰기** : 새로 만들 필요가 없다. ( 그대로 감싸면 된다. ) 150 | 151 | ```typescript 152 | function dropFirst(array: T[]) { 153 | array.shift(); 154 | } 155 | ``` 156 | 157 | 쓰기를 카피-온-라이트로 바꾸기 158 | 159 | - 인자로 들어온 값을 변경하기 때문 160 | 161 | ```typescript 162 | function dropFirst(array: T[]) { 163 | const arrayCopy = [...array]; 164 | arrayCopy.shift(); 165 | return arrayCopy; 166 | } 167 | ``` 168 | 169 | ### 값을 두개 리턴하는 함수로 만들기 170 | 171 | ```typescript 172 | function shift(array: T[]) { 173 | const arrayCopy = [...array]; 174 | const first = arrayCopy.shift(); 175 | return { 176 | first: first, 177 | array: arrayCopy, 178 | }; 179 | } 180 | 181 | // 다른 방법 (조합) 182 | function shift(array: T[]) { 183 | return { 184 | first: firstElement(array), 185 | array: dropFirst(array), 186 | }; 187 | } 188 | ``` 189 | 190 | ## 불변 데이터 구조를 읽는 것은 계산이다. 191 | 192 | - 변경 가능한 데이터를 읽는 것은 액션 193 | - 쓰기는 데이터를 변경 가능한 구조로 만든다 194 | - 어떤 데이터에 쓰기가 없다면, 데이터는 변경 불가능한 데이터이다. 195 | - 불변 데이터 구조를 읽는 것은 계산 196 | - 쓰기를 읽기로 바꾸면 코드에 계산이 많아진다. 197 | 198 | ## 어플리케이션에는 시간에 따라 변하는 상태가 있다. 199 | 200 | - shippingCart는 전역변수로 장바구니가 바뀔 때마다 새 값이 할당된다. 201 | - 이는 교체 Swapping (읽기,바꾸기,쓰기) 된다고 할 수 있다. 202 | 203 | ## 불변 데이터 구조는 충분히 빠릅니다! 204 | 205 | - 언제든 최적화 할 수 있다. 206 | - 가비지 콜렉터는 매우 빠르다! 207 | - 생각보다 많이 복사하지 않는다. 208 | - 구조적 공유를 하는 얕은 복사를 통해 참조만 복사한다. 209 | - 함수형 프로그래밍 언어엔 빠른 구현체가 있다. 210 | 211 | ## 자바스크립트 객체 훑어 보기 212 | 213 | ```typescript 214 | // 키 값으로 찾기 [key], .key 215 | const obj = {a:1,b:2}; 216 | obj["a"] // 1 217 | obj.key // 1 218 | 219 | // 키 값으로 설정 220 | obj["a"] = 7 221 | obj.c = 10 222 | 223 | // 키/값 쌍 지우기 224 | delete obj["a"] // true 225 | 226 | // 객체 복사하기 227 | Object.assign({}, obj); 228 | {...obj} 229 | 230 | // 키 목록 가져오기 231 | Object.keys(obj) // ["a", "b"] 232 | ``` 233 | 234 | ## 객체의 카피-온-라이트 235 | 236 | ```typescript 237 | function setPrice(item: Item, newPrice: number) { 238 | item.price = newPrice; 239 | } 240 | 241 | // 변경 242 | function setPrice(item: Item, newPrice: number) { 243 | const itemCopy = { ...item }; // 1 복사본 만들기 244 | itemCopy['price'] = newPrice; // 2 복사본 변경하기 245 | return itemCopy; // 3 복사본 리턴하기 246 | } 247 | ``` 248 | 249 | ## 중첩된 쓰기 읽기로 바꾸기 250 | 251 | ```typescript 252 | function setPriceByName(cart: Item[], name: string, price: number) { 253 | cart.forEach((item) => { 254 | if (item.name === name) { 255 | item.price = price; 256 | } 257 | }); 258 | } 259 | ``` 260 | 261 | ```typescript 262 | function setPriceByName(cart: Item[], name: string, price: number) { 263 | const cartCopy = [...cart]; // 배열의 카피-온-라이트 264 | cartCopy.forEach((item) => { 265 | if (item.name === name) { 266 | item = setPrice(item, price); // 객체의 카피-온-라이트 267 | } 268 | return cartCopy; 269 | }); 270 | } 271 | ``` 272 | 273 | ## 어떤 복사본이 생겼을까요? 274 | 275 | ```typescript 276 | shoppingCart = setPriceByName(shoppingCart, 't-shirt', 18564); // 12일 환율 기준 13달러 277 | ``` 278 | 279 | - 배열 하나 ( 장바구니 ) `const cartCopy = [...cart]` 280 | 281 | - 객체 하나 ( 티셔츠 ) `const itemCopy = {...item};` 282 | 283 | 나머지 두 객체(장바구니속 상품)는 복사 되지 않았다. 그 이유는 중첩 데이터의 얕은 복사를 했기 때문에, 구조적 공유가 되었다. 284 | 285 | ```typescript 286 | // 배열은 객체 세 개를 가리킨다 287 | [{티셔츠}, {양말}, {신발}] 288 | 289 | // 복사본도 동일한 객체를 가리킨다 290 | const cartCopy = [...cart]; 291 | [{티셔츠}, {양말}, {신발}], [{티셔츠}, {양말}, {신발}] 292 | 293 | // 티셔츠 객체의 복사본을 만들고 가격을 바꾼다 294 | item = setPrice(item, price); 295 | [{티셔츠}, {양말}, {신발}], 복사본 [{티셔츠}, {양말}, {신발}], 복사본 {뉴 티셔츠} 296 | 297 | // 복사된 티셔츠 객체를 가르키게 한다. 298 | [{티셔츠}, {양말}, {신발}], 복사본 [복사본 {뉴티셔츠}, {양말}, {신발}] 299 | ``` 300 | 301 | 결과적으로 `{양말}, {신발}` 객체는 `구조적 공유`가 된걸로 볼 수 있다. 302 | 303 | ## 요점 정리 304 | 305 | - 함수형 프로그래밍에는 불변 데이터가 필요하다 306 | - 카피-온-라이트는 데이터의 불변성을 유지할 수 있는 원칙이다 307 | - 카피-온-라이트는 값을 변경하기 전 얕은 복사를 한다 그리고 리턴한다 308 | - 보일러플레이트 코드를 줄이기 위해, 기본적인 유틸성 카피-온-라이트 함수를 만들어 두자 309 | -------------------------------------------------------------------------------- /docs/05-chapter_7.mdx: -------------------------------------------------------------------------------- 1 | # 7장 신뢰할 수 없는 코드를 쓰면서 불변성 지키기 2 | 3 | ## 이번 장에서 살펴볼 내용 4 | 5 | - 레거시 코드나 신뢰할 수 없는 코드로부터 내 코드를 보호하기 위해 방어적 복사를 만듭니다 6 | - 앝은 복사와 깊은 복사를 비교합니다 7 | - 카피-온-라이트와 방어적 복사를 언제 사용하면 좋은지 알 수 있습니다 8 | 9 | ## 레거시 코드와 불변성 10 | 11 | 저번 장 까지 어쩐지 깊은 복사 이야기 없이 얕은 복사만으로 불변성을 구현하더라니 이번 장에는 레거시 코드 이야기와 함께 깊은 복사 얘기가 나오네요. 12 | 13 | 아래 코드는 기존 레거시 코드에 행사를 위한 함수를 추가하고 있습니다만, 다만 `blackFridayPromotion()` 함수는 내부적을 상태를 mutate한다는 문제가 있습니다. 어떻게 해결해야 할까요? 14 | 15 | ```ts 16 | function addItemToCart(name: string, price: number) { 17 | // 레거시 코드 18 | const item = makeCartItem(name, price); 19 | shoppingCart = addItem(shoppingCart, item); 20 | const total = calcTotal(shoppingCart); 21 | setCartTotalDom(total); 22 | updateShippingIcons(shoppingCart); 23 | updateTaxDom(total); 24 | // highlight-next-line 25 | blackFridayPromotion(shoppingCart); // 블랙 프라이데이 행사를 위한 코드 추가 26 | } 27 | ``` 28 | 29 | 이때 `방어적 복사(defensive copy)`를 사용하면 레거시 코드를 수정하지 않고도 내 코드를 보호할 수 있습니다. 30 | 31 | ## 우리가 만든 카피-온-라이트 코드는 신뢰할 수 없는 코드와 상호작용해야 합니다 32 | 33 | 위에 말했듯 새로 추가되는 함수는 상태를 mutate하기 때문에 불변성을 지킬 수 없습니다. 불변성을 깨트리는 신뢰할 수 없는 코드(안전지대 바깥)와 우리의 코드 베이스가 상호작용할 때가 있을겁니다. 34 | 35 | 그간 우리가 만든 카피-온-라이트 코드는 불변성을 지키기 위해 얕은 복사를 사용했지만, 이번에는 깊은 복사를 사용해야 합니다. 36 | 37 | ![안개, 시야](./images/7-1.jpeg) 38 | 39 | 깊은 복사를 사용하면 참조를 끊을 수 있습니다. 즉 신뢰할 수 없는 코드가 참조에 접근하여 데이터를 손상시킬 수 있는 수단을 제거할 수 있습니다. 40 | 41 | 카피 온 라이트와 깊은 복사의 차이는, 카피 온 라이트는 무엇이 바뀌는지 알기 때문에 무엇을 복사해야 할지 결정할 수 있습니다. 반면 깊은 복사는 무엇이 바뀌는지 모르기 때문에 모든 것을 복사해야 합니다. 물론 복사본을 만든다는 점에 있어서는 동일합니다. 42 | 43 | ## 방어적 복사는 원본이 바뀌는 것을 막아줍니다 44 | 45 | 우리가 안전하게 잘 가꿔놓은 영역을 `안전지대`라고 부른다면, 안전지대 안팎으로 통행하는 데이터들은 모두 깊은 복사를 사용하여 복사본을 사용하도록 해야합니다. 참조를 끊어야 하기 때문입니다. 46 | 47 | ## 방어적 복사 구현하기 48 | 49 | 방어적 복사를 구현하기 위한 두 가지 규칙이 있습니다. 위에 언급한 `안전지대` 경계를 통과할 때 복사하는 것입니다. 50 | 51 | 1. 데이터가 안전한 코드에서 나갈 때 복사하기 52 | 2. 안전한 코드로 데이터가 들어올 때 복사하기 53 | 54 | 위 규칙을 준수하여 예제를 개선해보겠습니다. 방어적 복사와 관련된 코드를 감싸면 더 좋은 코드가 될 것 같습니다. 55 | 56 | ```ts 57 | function addItemToCart(name: string, price: number) { 58 | // ... 59 | updateTaxDom(total); 60 | const cartCopy = deepCopy(shoppingCart); 61 | blackFridayPromotion(cartCopy); // 블랙 프라이데이 행사를 위한 코드 추가 62 | // highlight-next-line 63 | shoppingCart = blackFridayPromotionSafe(cartCopy); 64 | } 65 | 66 | // 함수가 하는 일이 명확해보이도록 함수를 분리 - 세부 구현은 은닉 67 | function blackFridayPromotionSafe(cart: Cart) { 68 | const cartCopy = deepCopy(cart); 69 | blackFridayPromotion(cartCopy); 70 | return deepCopy(cartCopy); 71 | } 72 | ``` 73 | 74 | ## 방어적 복사가 익숙할 수도 있습니다 75 | 76 | - API를 호출할 때 방어적 복사 개념이 자연스럽게 구현되고 있습니다. HTTP 통신을 할 때 직렬화를 하니까요. 77 | - 얼랭(Erlang)과 엘릭서(Elixir) 같은 함수형 언어는 데이터를 주고 받거나 프로세스에서 데이터가 나갈 때 등등의 상황에서 데이터를 복사합니다. 78 | 79 | ## 깊은 복사는 얕은 복사보다 비쌉니다 80 | 81 | 깊은 복사는 원본과 어떤 데이터 구조도 공유하지 않습니다. 중첩된 모든 객체와 배열을 재사용하지 않고 복사본을 떠서 사용햡니다. 얕은 복사는 구조적 공유가 일어나므로 참조가 유지됩니다. 82 | 83 | 이러한 특징은 깊은 복사에 신뢰성을 더하는 요소이기도 하지만, 한편으로는 연산의 비용이 더 들어가는 요소가 됩니다. 깊은 복사는 얕은 복사보다 비쌉니다. 84 | 85 | ## 자바스크립트에서 깊은 복사를 구현하는 것은 어렵습니다 86 | 87 | 자바스크립트에서 깊은 복사를 구현하는 것은 어렵습니다. 자바스크립트는 객체를 참조로 전달하기 때문에 객체를 복사하려면 객체를 재귀적으로 복사해야 합니다. 이는 깊은 복사를 구현하는 데 많은 시간이 걸립니다. 88 | 89 | 아래는 완벽하지는 않지만 간단한 예제 코드입니다 90 | 91 | ```ts 92 | function deepCopy(obj: any) { 93 | // null이나 원시 값은 그대로 반환 94 | if (typeof obj !== 'object' || obj === null) { 95 | return obj; 96 | } 97 | // 배열인 경우 98 | const copy = Array.isArray(obj) ? [] : {}; 99 | 100 | // 모든 키를 재귀적으로 복사 101 | for (const key in obj) { 102 | copy[key] = deepCopy(obj[key]); 103 | } 104 | // 모든 연산 후에는 복사본을 반환 105 | return copy; 106 | } 107 | ``` 108 | 109 | ## 카피-온-라이트와 방어적 복사의 대화 110 | 111 | ... 이상하니 넘어가자. 112 | 113 | ## 요점 정리 114 | 115 | - 방어적 복사는 불변성을 구현하는 원칙입니다. 데이터가 들어오고 나갈 때 복사본을 만듭니다 116 | - 방어적 복사는 깊은 복사를 합니다. 그래서 카피 온 라이트보다 비용이 더 듭니다 117 | - 카피 온 라이트와 다르게 방어적 복사는 불변성 원칙을 구현하지 않은 코드로부터 데이터를 보호해 줍니다 118 | - 복사본이 많이 필요하지 않기 때문에 카피 온 라이트를 더 많이 사용합니다. 방어적 복사는 신뢰할 수 없는 코드와 함께 사용할 때만 사용합니다 119 | - 깊은 복사는 위에서 아래로 중첩된 데이터 전체를 복사합니다. 얕은 복사는 필요한 부분만 최소한으로 복사합니다 120 | -------------------------------------------------------------------------------- /docs/06-chapter_8.mdx: -------------------------------------------------------------------------------- 1 | # 8장 계층형 설계 1 2 | 3 | ## 이번 장에서 살펴볼 내용 4 | 5 | - 소프트웨어 설계에 대한 실용적인 정의를 소개합니다 6 | - 계층형 설계를 이해하고 어떤 도움이 되는지 알아봅니다 7 | - 깨끗한 코드를 만들기 위해 함수를 추출하는 방법을 배웁니다 8 | - 계층을 나눠서 소프트웨어를 설계하면 왜 더 나은 생각을 할 수 있는지 알아봅니다 9 | 10 | ## 소프트웨어 설계란 무엇입니까? 11 | 12 | > 제나 : "장바구니는 잘 만들지 못한 것 같아! 장바구니 관련 코드가 전체에 퍼져 있어. 대부분의 시간을 장바구니 관련 작업을 해야 하는데 뭔가 잘못될 거 같아 불안해." 13 | 14 | 설계가 잘된 코드는 개발 프로세스 전반을 편안하게 만들어줍니다. 아이디어를 코드로 구현하고 테스트하고 유지보수하기 쉽습니다. 그만큼 `소프트웨어 설계`는 중요합니다. 15 | 16 | 간단히 정의해보겠습니다. 17 | 18 | > 소프트웨어 설계 19 | > 20 | > 코드를 만들고, 테스트하고, 유지보수하기 쉬운 프로그래밍 방법을 선택하기 위해 미적 감각을 사용하는 것 21 | 22 | 이 장에서 이야기해볼 `계층형 설계(stratified design)`은 소프트웨어 설계에 도움이 되는 한 가지 방법입니다. 23 | 24 | ## 계층형 설계란 무엇인가요? 25 | 26 | 계층형 설계란 소프트웨어를 계층으로 구성하는 기술입니다. 각 계층에 있는 함수는 바로 아래 계층에 있는 함수를 이용해 정의합니다. 27 | 28 | 바로 아래 계층에 있는 함수를 이용해 정의한다는 것은 무슨 뜻일까요? 아래 도식을 보세요. 29 | 30 | ![계층형 설계](./images/8-1.jpeg) 31 | 32 | 각 계층은 목적(비즈니스 규칙, 장바구니, 카피-온-라이트, 언어 자체 기능)을 가지고 있습니다. 이 부분에 대하여 책에서는 다음과 같이 여러 패턴으로 나누어 설명하고 있습니다. 33 | 34 | - 직접 구현 (이번 챕터에서 다룸!) 35 | - 추상화 벽 36 | - 작은 인터페이스 37 | - 편리한 계층 38 | 39 | 이런 기법들의 핵심은 `코드의 추상화 단계를 맞추기`에 있다고 생각합니다. 하지만 왜 이런 분류를 하는걸까요? 40 | 41 | 계층을 나누고, 코드의 추상화 단계를 맞추는 것 자체가 절대 선이라서는 아닙니다. 당연히 읽기 좋고 고치기 쉬운 코드를 작성하기 위함입니다. 유지보수하기 좋은 코드는 서비스의 내구성을 높이고 더 빠르게 많은 기능을 구현할 수 있는 기초체력이 됩니다. 42 | 43 | 책에서는 `직접 구현(Straightforward Implementation)` 단계부터 시작해볼 것을 권하고 있습니다. 직접 구현이라길래 무슨 소리인가 했는데, 코드베이스의 모든 코드를 완전히 직접 구현하고, 직접 함수를 분리하고, 인터페이스를 만드는 등의 작업 방식을 말하는 것이었습니다. 44 | 45 | 직접 구현하다보면 설계 요령이 생긴다며... 암튼 이후 챕터에서 `추상화 벽` 등을 설명하면서 코드의 모듈화 등을 다루는 것으로 미루어 보았을 때 직접 구현 방식의 한계를 언급하면서 저자가 하고 싶은 말을 하게 될 것 같네요. 46 | 47 | 예컨대 아래 코드는 책에 제시된 것을 가져온 것인데, 저수준의 로직이 전부 드러나 있습니다. 기능은 구현되어 있지만 설계는 없는 코드입니다. 48 | 49 | ```ts 50 | function freeTieClip(cart: Cart) { 51 | let hasTie = false; 52 | let hasTieClip = false; 53 | for (let i = 0; i < cart.length; i++) { 54 | let item = cart[i]; 55 | if (item.name === 'tie') hasTie = true; 56 | if (item.name === 'tie clip') hasTieClip = true; 57 | } 58 | if (hasTie && !hasTieClip) { 59 | let tieClip = makeItem('tie clip', 0); 60 | return addItem(cart, tieClip); 61 | } 62 | return cart; 63 | } 64 | ``` 65 | 66 | 추상화를 하는 이유는 무엇일까요? `추상화의 벽`을 세워서 코드를 분리하면 세부 구현사항을 모두 살펴보지 않고서도 코드를 이해할 수 있게 됩니다. 이는 인간의 머리가 한계를 가지고 있기 때문입니다. 추상화를 통해 코드를 분리하면 코드를 이해하는데 필요한 노력을 줄일 수 있습니다. 67 | 68 | 이를 거꾸로 뒤집어보자면 머리가 충분히 좋다면 추상화는 불필요합니다. 디자인 패턴은 그 개발팀의 가장 개발 능력이 떨어지는 사람을 위해서 필요하다는 말도 있더라고요. 하지만 그보다 더 구체와 계산에 뛰어난 컴퓨터는 추상화가 필요 없습니다. 69 | 70 | 추상화에 앞서 아래와 같이 코드를 다이어그램으로 그려 옮겨보면 구조를 개선하기 위한 준비를 보다 효과적으로 할 수 있습니다. 71 | 72 | ![다이어그램](./images/8-2.jpeg) 73 | 74 | 위 다이어그램은 책에서 `호출 그래프`라고 부르는 시각화 방법입니다. 호출 그래프에서 화살표는 아래로 그리며, 각 화살표는 함수 호출을 나타냅니다. 사각형 상자에는 언어 기능이나 함수 호출 등이 담기게 됩니다. 75 | 76 | 지금 상태로는 `freeTieClip()` 이라는 함수의 바로 아랫 수준에 모든 로직이 같은 수준으로 드러나있습니다. 77 | 78 | 첫 번째 보여드렸던 그림처럼 호출 그래프에 그려진 각 노드가 동일한 목적과 수준을 가지고 있는지를 살펴서 같은 레이어에 배치하면 아래와 같이 각 노드가 여러 층에 배치될 것이고, 그에 따라 기존과 달리 노드와 노드를 이어주는 화살표의 길이가 서로 달라지게 됩니다. 79 | 80 | ![다이어그램](./images/8-4.jpeg) 81 | 82 | - 각 노드가 서로 특징에 따라 다른 레이어로 분류되었으나 실제 호출 시점의 코드를 떠올려보면 여전히 고수준과 저수준의 로직이 동시에 호출됩니다. 화살표의 길이도 짧아지는 것이 좋습니다. 83 | 84 | ![다이어그램](./images/8-5.jpeg) 85 | 86 | - 추상화를 통해 이전에 비해 고수준과 저수준의 로직이 분리되고, 같은 수준의 추상화 로직이 함께 실행됩니다. 최종적으로 분류한 그래프는 다음과 같습니다. 87 | 88 | ![다이어그램](./images/8-3.jpeg) 89 | 90 | ```ts 91 | function setPriceByName(cart, name, price) { 92 | const i = indexOfItem(cart, name); 93 | if (i !== null) { 94 | const item = arrayGet(cart, i); 95 | return arraySet(cart, i, setPrice(item, price)); 96 | } 97 | return cart; 98 | } 99 | 100 | function indexOfItem(cart, name) { 101 | for (let i = 0; i < cart.length; i++) { 102 | if (arrayGet(cart, i).name === name) return i; 103 | } 104 | return null; 105 | } 106 | 107 | function arrayGet(array, idx) { 108 | return array[idx]; 109 | } 110 | 111 | function arraySet(array, idx, value) { 112 | const copy = array.slice(); 113 | copy[idx] = value; 114 | return copy; 115 | } 116 | ``` 117 | 118 | 아래 목록은 이 책에서 언급한 `직접 구현`의 장점인데요, 119 | 120 | - 직접 구현한 코드는 한 단계의 구체화 수준에 관한 문제만 해결합니다 121 | - 계층형 설계는 특정 구체화 단계에 집중할 수 있게 도와줍니다 122 | - 호출 그래프는 구체화 단계에 대한 풍부한 단서를 보여줍니다 123 | - 함수를 추출하면 더 일반적인 함수로 만들 수 있습니다 124 | - 일반적인 함수가 많을 수록 재사용하기 좋습니다 125 | - 복잡성을 감추지 않습니다 126 | 127 | 저는 이 중 가장 마지막 '복잡성을 감추지 않습니다'에 대한 코멘트가 가장 마음에 들었습니다. 그간 업무를 하며 만들었던 수많은 헬퍼 함수들은 설계가 아닌 그저 분리에 지나지 않았나 반성하게 되네요. 128 | 129 | > 직접 구현 패턴을 적용한 코드처럼 보이게 만드는 것은 쉽습니다. 명확하지 않은 코드를 감추기 위해 '도우미 함수(helper function)'를 만들면 됩니다. 하지만 이렇게 하는 것은 계층형 설계가 아닙니다. 계층형 설계에서 모든 계층은 바로 아래 계층에 의존해야 합니다. 복잡한 코드를 같은 계층으로 옮기면 안됩니다. 더 낮은 구체화 수준을 가진 일반적인 함수를 만들어 소프트웨어에 직접 구현 패널을 적용해야 합니다. - 198p 130 | 131 | 다음 장에서 모듈화의 원칙에 대해서 조금 더 알아보면 실마리가 잡히지 않을까 기대해봅니다. 오늘은 개념의 구분과 필요성의 공감 정도로 충분할 것 같습니다. 132 | 133 | ## 요점 정리 134 | 135 | - 계층형 설계는 코드를 추상화 계층으로 구성합니다. 각 계층을 볼 때 다른 계층에 구체적인 내용을 몰라도 됩니다. 136 | - 문제 해결을 위한 함수를 구현할 때 어떤 구체화 단계로 쓸지 걸졍하는 것이 중요합니다. 그래야 함수가 어떤 계층에 속할지 알 수 있습니다. 137 | - 함수가 어떤 계층에 속할지 알려주는 요소는 함수 이름, 본문, 호출 그래프 등 많이 있습니다. 138 | - 함수 이름은 의도를 알려주므로 의도를 함께하는 유사한 함수들을 묶을 수 있습니다. 139 | - 함수 본문은 중요한 세부사항을 알려줍니다. 함수 본문을 통해 함수가 속해야 할 계층 구조를 알 수 있습니다. 140 | - 호출 그래프로 구현이 직접적이지 않다는 것을 알 수 있습니다. 함수를 호출하는 화살표가 다양한 길이를 가질 경우 직접 구현되어 있지 않다는 뜻입니다 (?) 141 | - 직접 구현 패턴은 함수를 명확하고 아름답게 구현해 계층을 구성할 수 있도록 알려줍니다. 142 | -------------------------------------------------------------------------------- /docs/07-chapter_9.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 9장 계층형 설계 2 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 코드를 모듈화하기 위해 추상화 벽을 만드는 법을 배웁니다. 10 | - 좋은 인터페이스가 어떤 것이고, 어떻게 찾는지 알아봅니다. 11 | - 설계가 이만하면 되었다고 할 수 있는 시점을 압니다. 12 | - 왜 계층형 설계가 유지보수와 테스트, 재사용에 도움이 되는지 이해합니다. 13 | 14 | ## 계층형 설계 패턴 15 | 16 | - 패턴 1: 직접 구현 17 | 직접 구현은 계충형 설계 구조를 만드는 데 도움이 됩니다. 직접 구현된 함수를 읽을 때, 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절한 구체화 수준으로 해결해야 합니다. 만약 너무 구체적이라면 코드에서 나는 냄새입니다. 18 | - 패턴 2: 추상화 벽 19 | 호출 그래프에 어떤 계층은 중요한 세부 구현을 감추고 인터페이스를 사용하여 코드를 만들면 높은 차원으로 생각할 수 있습니다. 고수준의 추상화 단계만 생각하면 되기 때문에, 두뇌 용량의 한계를 극복할 수 있습니다. 20 | - 패턴 3: 작은 인터페이스 21 | 시스템이 커질수록 비즈니스 개념을 나타내는 중요한 인터페이스는 작고 강력한 동작으로 구성하는 것이 좋습니다. 다른 동작도 직간접적으로 최소한의 인터페이스를 유지하면서 정의해야 합니다. 22 | - 패턴 4: 편리한 계층 23 | 계충형 설계 패턴과 실천 방법은 개발자의 요구를 만족시키면서 비즈니스 문제를 잘 풀 수 있어야 합니다. 소프트웨어를 더 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 합니다. 그냥 좋아서 계층을 추가하면 안 됩니다. 코드와 그 코드가 속한 추상화 계층은 작업할 때 편리해야 합니다. 24 | 25 | ## 패턴 2: 추상화 벽 26 | 27 | 추상화 벽은 여러가지 문제를 해결하는데, 그중 하나는 **팀 간 책임을 명확하게 나누는 것**이다. 28 | 추상화 벽은 세부 구현을 감춘 함수로 이루어진 계층이며, 추상화 벽에 있는 함수를 사용할 때에는 구현을 전혀 몰라도 함수를 쓸 수 있다는 것. 29 | 30 | ![abstraction barrier](https://drek4537l1klr.cloudfront.net/normand/Figures/f0206-02.jpg) 31 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1 32 | 33 | 추상화 벽을 통해 점선 위 아래로 함수가 나뉘는데, 마케팅팀과 개발팀은 추상화 벽 너머에 있는 함수를 어떻게 쓸지 신경 쓰지 않고 독립적으로 일할 수 있게 된다. 34 | 이건 마치 우리가 종종 사용하는 라이브러리나 API와 비슷하다고 말한다. 우리 라이브러리나 API를 사용할 때 그 내부가 어떻게 구현되었는지 신경쓸 필요 없이 가져다가 쓸 수 있는 것처럼, 추상화 벽은 책임을 명확하게 나눠준다. 35 | 36 | 배열이었던 장바구니 데이터 구조를 객체로 만드는 코드를 작성해보자. 37 | 38 | ```ts 39 | function add_item(cart, item) { 40 | // return add_element_last(cart, item); 41 | return objectSet(cart, item.name, item); 42 | } 43 | function calc_total(cart) { 44 | var total = 0; // for(var i = 0; i < cart.length; i++) { //     var item = cart[i]; //     total += item.price; // } 45 | var names = Object.keys(cart); 46 | for (var i = 0; i < names.length; i++) { 47 | var item = cart[names[i]]; 48 | total += item.price; 49 | } 50 | return total; 51 | } 52 | function setPriceByName(cart, name, price) { 53 | // var cartCopy = cart.slice(); 54 | // for(var i = 0; i < cartCopy.length; i++) { 55 | //   if(cartCopy[i].name === name) 56 | //     cartCopy[i] = setPrice(cartCopy[i], price); 57 | // } 58 | // return cartCopy; 59 | if (isInCart(cart, name)) { 60 | var item = cart[name]; 61 | var copy = setPrice(item, price); 62 | return objectSet(cart, name, copy); 63 | } else { 64 | var item = make_item(name, price); 65 | return objectSet(cart, name, item); 66 | } 67 | } 68 | function remove_item_by_name(cart, name) { 69 | // var idx = indexOfItem(cart, name); 70 | // if(idx !== null) 71 | //   return splice(cart, idx, 1); 72 | // return cart; 73 | return objectDelete(cart, name); 74 | } // for(var i = 0; i < cart.length; i++) { //  if(cart[i].name === name) //    return i; // } // return null; 75 | 76 | // function indexOfItem(cart, name) { 77 | // } 78 | function isInCart(cart, name) { 79 | // return indexOfItem(cart, name) !== null; 80 | return cart.hasOwnProperty(name); 81 | } 82 | ``` 83 | 84 | 추상화 벽이 있기 때문에 배열이었던 장바구니 데이터 구조를 객체로 완전히 바꿀 수 있었다. 85 | 이처럼 추상화 벽은 필요하지 않은 것은 무시할 수 있도록 돕는다. 86 | 87 | 리팩터링의 결과로 장바구니 함수들은 아래와 같이 한 줄짜리 코드가 되었다. 88 | 89 | ```ts 90 | function add_item(cart, item) { 91 | return objectSet(cart, item.name, item); 92 | } 93 | function gets_free_shipping(cart) { 94 | return calc_total(cart) >= 20; 95 | } 96 | function cartTax(cart) { 97 | return calc_tax(calc_total(cart)); 98 | } 99 | function remove_item_by_name(cart, name) { 100 | return objectDelete(cart, name); 101 | } 102 | function isInCart(cart, name) { 103 | return cart.hasOwnProperty(name); 104 | } 105 | ``` 106 | 107 | ![abstraction barrier](https://drek4537l1klr.cloudfront.net/normand/Figures/f0209-01.jpg) 108 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1 109 | 110 | 이미지를 보면 점선을 가로지르는 함수는 없다. 111 | 112 | 그렇다면 완전하지 않은 추상화 벽을 완전한 추상화 벽으로 만드려면 어떻게 하면 될까..? 바로 **추상화 벽에 새로운 함수를 만들면 된다**. 113 | 114 | > 추상화 벽을 사용하면 좋은 상황 115 | > 116 | > 1. 쉽게 구현을 바꾸기 위해: 구현에 대한 확신이 없는 경우 추상화 벽을 사용해 구현을 간접적으로 사용할 수 있어서 나중에 구현을 바꾸기가 쉬워진다. 117 | > 2. 코드를 읽고 쓰기 쉽게 만들기 위해: 세부적인 것을 신경 쓸 필요가 없어진다. 118 | > 3. 팀 간에 조율해야 할 것을 줄이기 위해 119 | > 4. 주어진 문제에 집중하기 위해: 해결하려는 문제의 구체적인 부분을 신경쓰지 않을 수 있다. 120 | 121 | 추상화 벽은 **팀 간의 커뮤니케이션 비용을 줄이고, 복잡한 코드를 명확하게 하기 위해** 전략적으로 사용해야 한다. 122 | 신경 쓰지 않아도 되는 것을 다루는 것이 추상화 벽의 핵심..! 123 | 124 | 125 | 126 | ## 패턴 3: 작은 인터페이스 127 | 128 | 인터페이스를 최소화하면 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있는데.. 129 | 130 | 가령 시계 할인 마케팅을 구현한다고 생각해보자. 하나는 추상화 벽에 구현하는 방법, 다른 하나는 추상화 벽 위에 있는 계층에 구현하는 방법이 있다. 131 | 어떤 방법을 선택하는 것이 좋을까? 132 | 133 | ![abstraction barrier](https://drek4537l1klr.cloudfront.net/normand/Figures/f0214-01.jpg) 134 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1 135 | 136 | 저자는 두 번째 방법으로 **추상화 벽 위에 있는 계층에** 만드는 것이 좋다고 말한다. 그 이유는.. 더 직접 구현에 가까운 방식이기 때문! 137 | 왜 첫 번째 방법을 사용하지 않는 것이 좋을까? 138 | - 첫 번째 방법으로 구현할 경우 시스템 하위 계층 코드가 늘어나기 때문에 좋지 않다. 139 | - 추상화 벽을 유지하긴 하지만 코드 자체가 개발팀을 위한 코드기 때문이다. 140 | - 추상화 벽에 새로운 함수가 늘어난다는 것은 계약이 하나 더 늘어난다는 뜻이기 때문이다. 141 | 142 | 따라서 새로운 기능을 만들 때 하위 계층에 기능을 추가하는 것보다 상위 계층에 만드는 것이 **작은 인터페이스** 패턴이라고 할 수 있다. 143 | 144 | 145 | ```ts 146 | // 1. 147 | function getsWatchDiscount(cart) { 148 | var total = 0; 149 | var names = Object.keys(cart); 150 | for(var i = 0; i < names.length; i++) { 151 | var item = cart[names[i]]; 152 | total += item.price; 153 | } 154 | return total > 100 && 155 | cart.hasOwnProperty("watch"); 156 | } 157 | // 2. 158 | function getsWatchDiscount(cart) { 159 |     var total  = calcTotal(cart); 160 |     var hasWatch = isInCart("watch"); 161 |     return total > 100 && hasWatch; 162 | } 163 | ``` 164 | 165 | > 추상화 벽을 작게 만들어야 하는 이유는? 166 | > 167 | > 1. 추상화 벽에 코드가 많을수록 구현이 변경되었을 때 고쳐야 할 것이 많습니다. 168 | > 2. 추상화 벽에 있는 코드는 낮은 수준의 코드이기 때문에 더 많은 버그가 있을 수 있습니다. 169 | > 3. 낮은 수준의 코드는 이해하기 더 어렵습니다. 170 | > 4. 추상화 벽에 코드가 많을수록 팀 간 조율해야 할 것도 많아집니다. 171 | > 5. 추상화 벽에 인터페이스가 많으면 알아야 할 것이 많아 사용하기 어렵습니다. 172 | 173 | 상위 계층에 어떤 함수를 만들 때 **가능한 현재 계층에 있는 함수로 구현하는 것**이 작은 인터페이스를 실천하는 방법. 174 | 175 | 176 | ## 패턴 4: 편리한 계층 177 | 178 | 이 패턴은 보다 현실적이고 실용적인 측면을 다룬다. 179 | 180 | 추상화는 가능한 일과 불가능한 일의 차이를 나타내기도 한다. (마치 자바스크립트 언어가 기계어에 대한 추상화 벽을 제공하는 것처럼..) 181 | 하지만 비즈니스를 생각하는 개발자에게 완벽한 추상화는 비효율적일 수 있다. 182 | 따라서 지금 당장 편리하다면 설계는 잠시 놓아두고 그대로 두어보자..! 개발자로서 비즈니스 요구사항과 훌륭한 아키텍처를 쌓아나가는 건 어느 하나 놓칠 수 없기에. 183 | 184 | ![abstraction barrier](https://drek4537l1klr.cloudfront.net/normand/Figures/f0222-01.jpg) 185 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1 186 | 187 | **호출 그래프**: 기능적 요구사항과 비기능적 요구사항을 잘 나타냄 188 | 특히 비기능적 요구사항 3가지는 189 | 1. 유지보수성 (maintainabilty): 요구 사항이 바뀌었을 때 가장 쉽게 고칠 수 있는 코드는 어떤 코드인가요? 190 | - 규칙: 위로 연결된 것이 적은 함수가 바꾸기 쉽습니다. 191 | - 핵심: 자주 바뀌는 코드는 가능한 위쪽에 있어야 합니다. 192 | 2. 테스트성 (testablity): 어떤 것을 테스트하는 것이 가장 중요한가요? 193 | - 규칙: 위쪽으로 많이 연결된 함수를 테스트하는 것이 더 가치 있습니다. 194 | - 핵심: 아래쪽에 있는 함수를 테스트하는 것이 위쪽에 있는 함수를 테스트하는 것보다 가치 있습니다. 195 | 3. 재사용성 (reusability): 어떤 함수가 재사용하기 좋나요? 196 | - 규칙: 아래쪽에 함수가 적을수록 더 재사용하기 좋습니다. 197 | - 핵심: 낮은 수준의 단계로 함수를 빼내면 재사용성이 더 높아집니다. 198 | 199 | ![abstraction barrier](https://drek4537l1klr.cloudfront.net/normand/Figures/f0227-01.jpg) 200 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-9/1 201 | 202 | - 코드를 적절한 위치에 두면 **유지보수** 비용을 많이 줄일 수 있다. 203 | - 그래프의 가장 위에 있는 코드, 즉 가장 높은 계층에 있는 코드가 가장 고치기 쉬움 204 | - 가장 낮은 계층에 있는 함수는 상위 계층에 영향을 줌. 205 | - 자주 바뀌는 코드는 그래프 위에 있을수록 좋다. 따라서 수정 사항이 잦은 상위 계층은 적게 유지하는 것이 유리. 206 | - 패턴을 사용하면 **테스트** 가능성에 맞춰 코드를 계층화할 수 있다. 207 | - 아래에 있는 코드는 테스트가 중요: 많은 코드가 가장 하위 계층 코드에 의존하기 때문. 208 | - 아래에 있는 것은 자주 바뀌지 않기 때문에 테스트 코드도 자주 바뀌지 않음. 209 | - 계층형 구조를 만들면 **재사용성**이 좋아진다. 210 | - 낮은 계층으로 함수를 추출하면 재사용할 가능성이 많아짐. 211 | - 아래쪽에 함수가 적을수록 더 재사용하기 좋음. 212 | 213 | 214 | 215 | 216 | ## 요점 정리 217 | 218 | - 추상화 벽 패턴을 사용하면 세부적인 것을 완벽하게 감출 수 있어서 더 높은 차원에서 생각할 수 있게 된다. 219 | - 작은 인터페이스 패턴을 사용하면 완성된 인터페이스에 가깝게 계층을 만들 수 있다. 220 | - 편리한 계층 패턴을 이용하면 다른 패턴을 요구 사항에 맞게 사용할 수 있다. 221 | - 호출 그래프 구조에서 규칙을 얻을 수 있다. 222 | -------------------------------------------------------------------------------- /docs/08-chapter_10.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 10장 일급 함수 1 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 왜 일급 값이 좋은지 알아봅니다. 10 | - 문법을 일급 함수로 만드는 방법에 대해 알아봅니다. 11 | - 고차 함수로 문법을 감싸는 방법을 알아봅니다. 12 | - 일급 함수와 고차 함수를 사용한 리팩터링 두 개를 살펴봅니다. 13 | 14 | ## 마케팅팀은 여전히 개발팀과 협의해야 합니다. 15 | 16 | 추상화 벽은 마케팅티미 사용하기 좋은 API 였습니다. 17 | 하지만 예상만큼 잘 안 되었습니다. 18 | 대부분은 개발팀과 협의 없이 일할 수 있었지만, 주어진 API 로는 할 수 없는 일이 있어서 새로운 것은 개발팀에 요청해야 합니다. 19 | 20 | 요구 사항은 다음과 같습니다: 장바구니에 있는 제품 값을 설정하는 기능, 장바구니에 있는 제품 개수를 설정하는 기능, 장바구니에 있는 제품에 배송을 설정하는 기능 21 | 22 | ## 코드의 냄새: 함수 이름에 있는 암묵적 인자 23 | 24 | ```typescript 25 | interface setPriceByName { 26 | (cart: Cart, name: string, price: number): Cart; 27 | } 28 | interface setQuantityByName { 29 | (cart: Cart, name: string, quantity: number): Cart; 30 | } 31 | interface setShippingByName { 32 | (cart: Cart, name: string, shipping: number): Cart; 33 | } 34 | interface setTaxByName { 35 | (cart: Cart, name: string, tax: number): Cart; 36 | } 37 | ``` 38 | 39 | 자세히 보면 함수 이름에 따라 비슷한 일을 하는 것으로 보입니다. 40 | 41 | ### 냄새를 맡는 법 42 | 43 | **함수 이름에 있는 암묵적 인자**(implicit argument in function name) 냄새는 두 가지 특징을 보입니다. 44 | 45 | 1. 함수 구현이 거의 똑같습니다. 46 | 2. 함수 이름이 구현의 차이를 만듭니다. 47 | 48 | 함수 이름에서 서로 다른 부분이 암묵적 인자입니다. 49 | 50 | ## 리팩터링: 암묵적 인자를 드러내기 51 | 52 | 1. 함수 이름에 있는 암묵적 인자를 확인합니다. 53 | 2. 명시적인 인자를 추가합니다. 54 | 3. 함수 본문에 하드 코딩된 값을 새로운 인자로 바꿉니다. 55 | 4. 함수를 부르는 곳을 고칩니다. 56 | 57 | ### 리팩터링 전 58 | 59 | ```typescript 60 | interface SetPriceByName { 61 | (cart: Cart, name: string, price: number): Cart; 62 | } 63 | interface SetQuantityByName { 64 | (cart: Cart, name: string, quantity: number): Cart; 65 | } 66 | interface SetShippingByName { 67 | (cart: Cart, name: string, shipping: number): Cart; 68 | } 69 | interface SetTaxByName { 70 | (cart: Cart, name: string, tax: number): Cart; 71 | } 72 | ``` 73 | 74 | ### 리팩터링 후 75 | 76 | ```typescript 77 | interface SetFieldByName { 78 | (cart: Cart, name: string, field: string, value: string | number): Cart; 79 | } 80 | ``` 81 | 82 | ## 일급인 것과 일급이 아닌 것을 구별하기 83 | 84 | ### 자바스크립트에서 일급이 아닌 것 85 | 86 | - 수식 연산자 87 | - 반복문 88 | - 조건문 89 | - try/catch 블록 90 | 91 | ### 일급으로 할 수 있는 것 92 | 93 | - 변수에 할당 94 | - 함수의 인자로 넘기기 95 | - 함수의 리턴값으로 받기 96 | - 배열이나 객체에 담기 97 | 98 | ## 필드명을 문자열로 사용하면 버그가 생기지 않을까요? 99 | 100 | 타입스크립트를 이용하면 해결 완료! 101 | 102 | ## 일급 필드를 사용하면 API를 바꾸기 더 어렵나요? 103 | 104 | 기존의 API 를 삭제하지 않아도 맵핑을 이용하여 추가하면 해결이 됩니다. (개방 폐쇄 원칙) 105 | 106 | ```typescript 107 | const validItemFields = ['price', 'quantity', 'shipping', 'tax', 'number']; 108 | const translations = { quantity: 'number' }; 109 | 110 | const setFieldByName: SetFieldByName = (cart, name, field, value) => { 111 | if (!validItemFields.includes(field)) 112 | throw new Error(`Not a valid item field: '${field}'.`); 113 | if (translations.hasOwnProperty(field)) field = translations[field]; 114 | const item = cart[name]; 115 | const newItem = objectSet(item, field, value); 116 | const newCart = objectSet(cart, name, newItem); 117 | 118 | return newCart; 119 | }; 120 | ``` 121 | 122 | ## 객체와 배열을 너무 많이 쓰게 됩니다. 123 | 124 | **장바구니와 제품처럼 일반적인 엔티티는 객체와 배열처럼 일반적인 데이터 구조를 사용해야 합니다.** 125 | 126 | ## 정적 타입 vs 동적 타입 127 | 128 | ``` 129 | ... 130 | 예를 들어 어떤 연구에서는 정적 타입 언어와 동적 타입 언어를 구분하는 것보다 131 | 소프트웨어 품질을 위해 숙면을 하는 것이 더 중요하다고 합니다. 132 | ... 133 | ``` 134 | 135 | ## 모두 문자열로 통신합니다. 136 | 137 | ``` 138 | ... 139 | 그럼 정적 타입 언어는 쓰지 않아야 하나요? 그것은 아닙니다. 그럼 써야 하나요? 그것도 아닙니다. 140 | 다만 동적 타입 언어가 이런 문제를 만드는 것이 아니고 정적 타입 언어가 없어져야할 대상이 아니라는 것을 잘 알아야 합니다. 141 | 그리고 데이터의 단점 하나를 발견할 수 있었습니다. 142 | 그것은 바로 데이터는 항상 해석이 필요하다는 것입니다. (JSON.parse?) 143 | ``` 144 | 145 | ## 어떤 문법이든 일급 함수로 바꿀 수 있습니다. 146 | 147 | ```typescript 148 | function plus(a: number, b: number) { 149 | return a + b; 150 | } 151 | ``` 152 | 153 | ```typescript 154 | interface ARGS { 155 | tryFunction: VoidFunction; 156 | catchFunction?: VoidFunction; 157 | finallyFunction?: VoidFunction; 158 | } 159 | 160 | function tryCatchFinally({ 161 | tryFunction, 162 | catchFunction, 163 | finallyFunction, 164 | }: ARGS) { 165 | try { 166 | tryFunction(); 167 | } catch { 168 | catchFunction?.(); 169 | } finally { 170 | finallyFunction?.(); 171 | } 172 | } 173 | ``` 174 | 175 | ## 반복문 예제: 먹고 치우기 176 | 177 | ```typescript 178 | for (let i = 0; i < foods.lenghth; i++) { 179 | const food = foods[i]; 180 | const cookedFood = cook(food); 181 | eat(cookedFood); 182 | } 183 | 184 | for (let i = 0; i < dishes.lenghth; i++) { 185 | const dish = dishes[i]; 186 | const washedDish = wash(dish); 187 | const driedDish = dry(washedDish); 188 | putAway(driedDish); 189 | } 190 | ``` 191 | 192 | 위의 배열 동작을 추상화할 수 있는데 그것을 forEach 라고 부릅니다. 193 | 194 | ```typescript 195 | function cookAndEat(food: Food) { 196 | const cookedFood = cook(food); 197 | const dish = eat(cookedFood); 198 | 199 | return dish; 200 | } 201 | 202 | function clean(dish: Dish) { 203 | const washedDish = wash(dish); 204 | const driedDish = dry(dish); 205 | putAway(driedDish); 206 | } 207 | 208 | const foods = await getFoods(); 209 | const dishes = foods.map(cookAndEat); 210 | dishes.forEach(clean); 211 | ``` 212 | 213 | ## 리팩터링: 함수 본문을 콜백으로 바꾸기 214 | 215 | ```typescript 216 | try { 217 | saveUserData(user); 218 | } catch (error) { 219 | logToSnapErrors(error); 220 | } 221 | ``` 222 | 223 | ```typescript 224 | function withLogging(callback: voidFunction) { 225 | try { 226 | callback(); 227 | } catch (error) { 228 | logToSnapErrors(error); 229 | } 230 | } 231 | withLogging(() => saveUserData(user)); 232 | ``` 233 | 234 | ## 이것은 무슨 문법인가요? 235 | 236 | 1. 전역으로 정의하기 237 | 238 | ```typescript 239 | function saveCurrentUserData() { 240 | saveUserData(user); 241 | } 242 | withLogging(saveCurrentUserData); 243 | ``` 244 | 245 | 2. 지역적으로 정의하기 246 | 247 | ```typescript 248 | function someFunction() { 249 | const saveCurrentUserData = function () { 250 | saveUserData(user); 251 | }; 252 | withLogging(saveCurrentUserData); 253 | } 254 | ``` 255 | 256 | 3. 인라인으로 정의하기 257 | 258 | ```typescript 259 | withLogging(function () { 260 | saveUserData(user); 261 | }); 262 | ``` 263 | 264 | ## 왜 본문을 함수로 감싸서 넘기나요? 265 | 266 | 감싼 함수를 호출하기 전까지 실행되지 않습니다. (지연 평가, Lazy evaluation) 267 | 268 | ### 이름 붙이기 269 | 270 | ```typescript 271 | const saveCurrentUserData = () => saveUserData(user); 272 | ``` 273 | 274 | ### 컬렉션에 저장하기 275 | 276 | ```typescript 277 | array.push(() => saveUserData(user)); 278 | ``` 279 | 280 | ### 그냥 넘기기 281 | 282 | ```typescript 283 | withLogging(() => saveUserData(user)); 284 | ``` 285 | 286 | ### 선택적으로 호출하기 287 | 288 | ```typescript 289 | function callOnThursday(voidFunction: VoidFunction) { 290 | if (today === 'Thursday') voidFunction(); 291 | } 292 | ``` 293 | 294 | ### 나중에 호출하기 295 | 296 | ```typescript 297 | function callTomorrow(voidFunction: VoidFunction) { 298 | sleep(oneDay); 299 | voidFunction(); 300 | } 301 | ``` 302 | 303 | ### 새로운 문맥 안에서 호출하기 304 | 305 | ```typescript 306 | function withLogging(voidFunction: VoidFunction) { 307 | try { 308 | voidFunction(); 309 | } catch (error) { 310 | logToSnapErrors(error); 311 | } 312 | } 313 | ``` 314 | 315 | ## 요점 정리 316 | 317 | - 일급 값은 변수에 저장할 수 있고 인자로 전달하거나 함수의 리턴값으로 사용할 수 있습니다. 318 | - 언어에는 일급이 아닌 기능(if, for, +, - 등)이 있는데 함수로 감싸 일급으로 만들 수 있습니다. 319 | - 어떤 언어는 함수를 일급 값처럼 쓸 수 있는 일급 함수가 있습니다. 320 | - 이는 어떤 단계 이상의 함수형 프로그래밍을 하는데 필요합니다. 321 | - 고차 함수로 다양한 동작을 추상화할 수 있습니다. 322 | - 함수 이름에 있는 암묵적 인자는 함수의 이름으로 구분하는 코드의 냄새입니다. 323 | - 동작을 추상화하기 위해 본문을 콜백으로 바꾸기 리팩터링을 사용할 수 있습니다. 324 | -------------------------------------------------------------------------------- /docs/09-chapter_11.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 11장 일급 함수 2 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 함수 본문을 콜백으로 바꾸기 리팩터링에 대해 더 알아봅니다. 10 | - 함수를 리턴하는 함수가 가진 강력한 힘을 이해합니다. 11 | - 고차 함수에 익숙해지기 위해 여러 고차 함수를 만들어 봅니다. 12 | 13 | ## 카피-온-라이트 리팩터링하기 14 | 15 | ### 함수 본문을 콜백으로 바꾸기 단계 16 | 17 | 1. 본문과 앞부분, 뒷부분을 확인하기 18 | 2. 함수 빼내기 19 | 3. 콜백 빼내기 20 | 21 | ### 카피-온-라이트 단계 22 | 23 | 1. 복사본을 만듭니다. 24 | 2. 복사본을 변경합니다. 25 | 3. 복사본을 리턴합니다. 26 | 27 | ## 배열에 대한 카피-온-라이트 리팩터링 28 | 29 | ```typescript 30 | function arraySet(array: Array, idx: number, value: T) { 31 | const copy = array.slice(); 32 | copy[idx] = value; 33 | return copy; 34 | } 35 | ``` 36 | 37 | ```typescript 38 | function arraySet(array: Array, idx: number, value: T) { 39 | return withArrayCopy(array, (copy) => { 40 | copy[idx] = value; 41 | }); 42 | } 43 | 44 | function withArrayCopy(array: Array, modify: (t: Array) => Array) { 45 | const copy = array.slice(); 46 | modify(copy); 47 | return copy; 48 | } 49 | // = Array.map 50 | ``` 51 | 52 | ### 리팩터링으로 얻은 것 53 | 54 | 1. 표준화된 원칙 55 | 2. 새로운 동작에 원칙을 적용할 수 있음 56 | 3. 여러 개를 변경할 때 최적화 57 | 58 | ```typescript 59 | const sortedArray = withArrayCopy(array, (copy) => SuperSorter.sort(copy)); 60 | ``` 61 | 62 | ## 함수를 리턴하는 함수 63 | 64 | ```typescript 65 | function saveUserDataWithLogging(args) { 66 | try { 67 | saveUserData(args); 68 | } catch (error) { 69 | logToSnapErrors(error); 70 | } 71 | } 72 | 73 | function fetchProductWithLogging(args) { 74 | try { 75 | fetchProduct(args); 76 | } catch (error) { 77 | logToSnapErrors(error); 78 | } 79 | } 80 | ``` 81 | 82 | ```typescript 83 | function wrapLogging(voidFunction: VoidFunction) { 84 | return (...args) => { 85 | try { 86 | voidFunction(args); 87 | } catch (error) { 88 | logToSnapErrors(error); 89 | } 90 | }; 91 | } 92 | 93 | const saveUserDataWithLogging = wrapLogging(saveUserData); 94 | const fetchProductWithLogging = wrapLogging(fetchProduct); 95 | ``` 96 | 97 | ## 결론 98 | 99 | - 일급 값: 변수에 저장하거나 인자로 전달하거나 함수의 리턴값으로 사용할 수 있습니다. 100 | - 일급 함수: 함수를 일급 값으로써 취급할 수 있습니다. 101 | - 고차 함수: 함수를 인자로 받거나 함수를 리턴하는 함수 입니다. (입력과 출력 중 함수가 포함됨) 102 | 103 | ## 요점 정리 104 | 105 | - 고차 함수로 패턴이나 원칙을 코드로 만들 수 있습니다. 106 | - 고차 함수로 함수를 리턴하는 함수를 만들 수 있습니다. 107 | - 고차 함수는 가독성을 해칠 수 있으므로 적절하게 써야합니다. 108 | -------------------------------------------------------------------------------- /docs/10-chapter_12.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 12장 함수형 반복 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 함수형 도구 map, filter, reduce 에 대해 탐구 10 | - 반복문을 함수형 도구로 바꿔보기 11 | 12 | ## 쿠폰 이메일 처리 로직 바꿔보기 13 | 14 | ```typescript 15 | const emailsForCustomers(customers, goods, bests) => { 16 | const emails = []; 17 | for(let i = 0; i < customers.length; i++){ 18 | const customer = customer[i]; 19 | const email = emailForCustomer(customer, good, best); 20 | emails.push(email); 21 | } 22 | return emails 23 | } 24 | ``` 25 | 26 | ### forEach 를 이용해서 바꿔보기 (불필요한 부분 제거) 27 | 28 | ```typescript 29 | const emailsForCustomers(customers, goods, bests) => { 30 | const emails = []; 31 | customers.forEach(customer => { 32 | const email = emailForCustomer(customer, good, best); 33 | emails.push(email); 34 | }) 35 | return emails 36 | } 37 | ``` 38 | 39 | ### map 을 이용해서 바꿔보기 40 | 41 | ```typescript 42 | const map = (array, fn) => { 43 | const newArray = []; 44 | forEach(array, item => { 45 | newArray.push(fn(item)); 46 | }) 47 | return newArray; 48 | } 49 | 50 | 51 | const emailsForCustomers(customers, goods, bests) => { 52 | return map(customers, customer => { 53 | return emailForCustomer(customer, good, best); 54 | }) 55 | } 56 | ``` 57 | 58 | ## map() 59 | 60 | map() 은 X 값이 있는 배열을 받아서 Y 값이 있는 배열을 반환한다. 61 | X->Y 로 변환하는 함수 xToY() 를 필요로 한다. 62 | 63 | map에 계산을 넘기면 map을 사용하는 코드도 계산이 되고, 액션을 넘기면 액션이 된다. 64 | 65 | ## 함수를 전달하는 세가지 방법 66 | 67 | ### 전역으로 정의하기 68 | 69 | ```typescript 70 | const greet(name) => { 71 | return `Hello, ${name}!`; 72 | } 73 | 74 | const friendGreetings = map(friends, greet); 75 | ``` 76 | 77 | ### 지역적으로 정의하기 78 | 79 | ```typescript 80 | const greetEveryBody = (friends) => { 81 | const greet = (name) => { 82 | return `Hello, ${name}!`; 83 | }; 84 | return map(friends, greet); 85 | }; 86 | ``` 87 | 88 | ### 인라인으로 정의하기 89 | 90 | ```typescript 91 | const friendGreetings = map(friends, (name) => { 92 | return `Hello, ${name}!`; 93 | }); 94 | ``` 95 | 96 | ## filter() 97 | 98 | filter() 는 X 값이 있는 배열을 받아서 순서를 유지하면서 통과하는 Y 값이 있는 배열을 반환한다. 99 | 100 | ```typescript 101 | const selectBestCustomers = (customers) => { 102 | const newArray = []; 103 | forEach(customers, (customer) => { 104 | if (customer.purchases.length >= 3) { 105 | newArray.push(customer); 106 | } 107 | }); 108 | return newArray; 109 | }; 110 | ``` 111 | 112 | ```typescript 113 | const selectBestCustomers = (customers) => { 114 | return filter(customers, (customer) => { 115 | return customer.purchases.length >= 3; 116 | }); 117 | }; 118 | 119 | const filter = (array, fn) => { 120 | const newArray = []; 121 | forEach(array, (item) => { 122 | if (fn(item)) { 123 | newArray.push(item); 124 | } 125 | }); 126 | return newArray; 127 | }; 128 | ``` 129 | 130 | ## reduce() 131 | 132 | reduce() 는 배열을 순회하면서 값을 누적하는 함수이다. 133 | 134 | ```typescript 135 | const countAllPurchases = (customers) => { 136 | let total = 0; 137 | forEach(customers, (customer) => { 138 | total += customer.purchases.length; 139 | }); 140 | return total; 141 | }; 142 | ``` 143 | 144 | ```typescript 145 | const countAllPurchases = (customers) => { 146 | return reduce( 147 | customers, 148 | (total, customer) => { 149 | return total + customer.purchases.length; 150 | }, 151 | 0 152 | ); 153 | }; 154 | 155 | const reduce = (array, fn, initial) => { 156 | let acc = initial; 157 | forEach(array, (item) => { 158 | acc = fn(acc, item); 159 | }); 160 | return acc; 161 | }; 162 | ``` 163 | 164 | ### reduce()로 할 수 있는 것들 165 | 166 | - 실행 취소 / 실행 복귀 167 | - 테스트 할 때 사용자 입력을 다시 실행하기 168 | - 시간여행 디버깅 169 | - 회계감사 추적 170 | -------------------------------------------------------------------------------- /docs/11-chapter_13.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 13장 함수형 도구 체이닝 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 함수형 도구를 조합하는 법을 배운다. 10 | - 복잡한 반복문을 함수형 도구 체인으로 바꾸는 법을 배운다. 11 | - 데이터 변환 파이프라인을 만들어 작업을 수행하는 법을 배운다. 12 | 13 | ## 함수형 도구 체이닝 14 | 15 | ### 우수고객의 가장 비싼 주문을 찾는 함수 16 | 17 | ```typescript 18 | const biggestPurchasesBestCustomers = (customers: Customer[]) => { 19 | // 1 단계 20 | const bestCustomers: Customer[] = filter( 21 | customers, 22 | (customer) => customer.purchases > 3 23 | ); 24 | 25 | // 2 단계 26 | const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => { 27 | return reduce( 28 | customer.purchases, 29 | (biggest, purchase) => { 30 | if (biggest.total > purchase.total) { 31 | return biggest; 32 | } 33 | return purchase; 34 | }, 35 | { total: 0 } 36 | ); 37 | }); 38 | 39 | return biggestPurchases; 40 | }; 41 | ``` 42 | 43 | ### 콜백 분리 44 | 45 | ```typescript 46 | maxKey(customer.purchases, (purchase) => purchase.total, { total: 0 }); 47 | 48 | const maxKey = ( 49 | array: T[], 50 | keySelector: (item: T) => K, 51 | defaultValue: K 52 | ): K => { 53 | return reduce( 54 | array, 55 | (biggest, item) => { 56 | if (keySelector(biggest) > keySelector(item)) { 57 | return biggest; 58 | } 59 | return item; 60 | }, 61 | defaultValue 62 | ); 63 | }; 64 | ``` 65 | 66 | ```typescript 67 | const biggestPurchasesBestCustomers = (customers: Customer[]) => { 68 | // 1 단계 69 | const bestCustomers: Customer[] = filter( 70 | customers, 71 | (customer) => customer.purchases > 3 72 | ); 73 | 74 | // 2 단계 75 | const biggestPurchases: Purchases[] = map(bestCustomers, (customer) => { 76 | return maxKey(customer.purchases, (purchase) => purchase.total, { 77 | total: 0, 78 | }); 79 | }); 80 | 81 | return biggestPurchases; 82 | }; 83 | ``` 84 | 85 | ### 단계에 이름 붙이기 (방법 1) 86 | 87 | ```typescript 88 | const biggestPurchasesBestCustomers = (customers: Customer[]) => { 89 | const bestCustomers: Customer[] = selectBestCustomers(customers); 90 | const biggestPurchases: Purchases[] = getBiggestPurchases(bestCustomers); 91 | return biggestPurchases; 92 | }; 93 | 94 | const selectBestCustomers = (customers: Customer[]) => { 95 | return filter(customers, (customer) => customer.purchases.length >= 3); 96 | }; 97 | 98 | const getBiggerPurchases = (customers: Customer[]) => { 99 | return map(customers, getBiggestPurchase); 100 | }; 101 | 102 | const getBiggestPurchase = (customer: Customer) => { 103 | return maxKey(customer.purchases, (purchase) => purchase.total, { 104 | total: 0, 105 | }); 106 | }; 107 | ``` 108 | 109 | ### 콜백에 이름 붙이기 (방법 2) 110 | 111 | ```typescript 112 | const biggestPurchasesBestCustomers = (customers: Customer[]) => { 113 | const bestCustomers: Customer[] = filter(customers, isGoodCustomer); 114 | const biggestPurchases: Purchases[] = map(bestCustomers, getBiggestPurchase); 115 | return biggestPurchases; 116 | }; 117 | 118 | const isGoodCustomer = (customer: Customer) => customer.purchases.length >= 3; 119 | 120 | const getBiggestPurchase = (customer: Customer) => { 121 | return maxKey(customer.purchases, getPurchaseTotal, { total: 0 }); 122 | }; 123 | 124 | const getPurchaseTotal = (purchase: Purchase) => purchase.total; 125 | ``` 126 | 127 | ### 두 방법을 비교 128 | 129 | - 두번째 방법이 더 명확하다. (재사용성, 단계 중첩 방지) 130 | 131 | ### 효율성 132 | 133 | - filter(), map()은 매번 새 배열을 만든다 134 | - 비효율적이라고 생각 할 수 있으나 GC가 빠르게 처리하게 때문에 걱정 No! 135 | - 체인을 최적화 하는 것을 스트림 결합이라고 한다. 136 | 137 | ```typescript 138 | // 하나의 값에 map() 두번 사용 139 | const names = map(customers, getFullName); 140 | const nameLengths = map(names, StringLength); 141 | 142 | // map() 한번 사용 143 | const nameLengths = map(customers, (customer) => 144 | StringLength(getFullName(customer)) 145 | ); 146 | ``` 147 | 148 | 한 번 사용하는 곳은 GC가 필요 없다. 149 | 150 | ### 반복문 함수형도구로 리팩토링 151 | 152 | - 이해하고 다시 만들기 153 | - 단서찾기 154 | 155 | ```typescript 156 | // 결과의 배열 157 | const answer = []; 158 | const window = 5; 159 | 160 | // 배열 개수만큼 반복 161 | for (let i = 0; i < array.length; i++) { 162 | const sum = 0; 163 | const count = 0; 164 | 165 | // 0~5작은 구간 반복 166 | for (let w = 0; w < window; w++) { 167 | const idx = i + w; 168 | if (idx < array.length) { 169 | sum += array[idx]; 170 | // 값의 누적 171 | count += 1; 172 | } 173 | } 174 | // 배열의 추가 175 | answer.push(sum / count); 176 | } 177 | ``` 178 | 179 | ### 팁 1 데이터 만들기 180 | 181 | ```typescript 182 | const answer = []; 183 | const window = 5; 184 | 185 | for (let i = 0; i < array.length; i++) { 186 | const sum = 0; 187 | const count = 0; 188 | 189 | // 데이터를 배열에 넣어 함수형 도구 쓸수 있게 만들기 190 | const subArray = array.slice(i, i + window); 191 | for (let w = 0; w < subArray.length; w++) { 192 | sum += subArray[w]; 193 | count += 1; 194 | } 195 | answer.push(sum / count); 196 | } 197 | ``` 198 | 199 | ### 팁 2 전체 배열 한번에 조작하기 200 | 201 | ```typescript 202 | const answer = []; 203 | const window = 5; 204 | 205 | for (let i = 0; i < array.length; i++) { 206 | // 하위 배열을 만들기 위해 반복문의 인덱스를 사용 207 | const subArray = array.slice(i, i + window); 208 | answer.push(average(subArray)); 209 | } 210 | ``` 211 | 212 | ### 팁 3 작은 단계로 나누기 213 | 214 | ```typescript 215 | const indices = []; 216 | 217 | for (let i = 0; i < array.length; i++) { 218 | indices.push(i); 219 | } 220 | 221 | const window = 5; 222 | const answer = map(indices, (i) => { 223 | const subArray = array.slice(i, i + window); 224 | return average(subArray); 225 | }); 226 | ``` 227 | 228 | ```typescript 229 | const indices = []; 230 | 231 | for (let i = 0; i < array.length; i++) { 232 | indices.push(i); 233 | } 234 | 235 | const window = 5; 236 | const windows = map(indices, (i) => array.slice(i, i + window)); 237 | 238 | const answer = map(windows, average); 239 | ``` 240 | 241 | ```typescript 242 | const range = (start: number, end: number) => { 243 | const result = []; 244 | for (let i = start; i < end; i++) { 245 | result.push(i); 246 | } 247 | return result; 248 | }; 249 | 250 | const window = 5; 251 | 252 | const indices = range(0, array.length); 253 | const windows = map(indices, (i) => array.slice(i, i + window)); 254 | const answer = map(windows, average); 255 | ``` 256 | 257 | ### 절차적 코드와 함수형 코드 비교 258 | 259 | ```typescript 260 | const answer = []; 261 | const window = 5; 262 | 263 | for (let i = 0; i < array.length; i++) { 264 | const sum = 0; 265 | const count = 0; 266 | 267 | for (let w = 0; w < window; w++) { 268 | const idx = i + w; 269 | if (idx < array.length) { 270 | sum += array[idx]; 271 | count += 1; 272 | } 273 | } 274 | answer.push(sum / count); 275 | } 276 | ``` 277 | 278 | ```typescript 279 | const window = 5; 280 | 281 | const indices = range(0, array.length); 282 | const windows = map(indices, (i) => array.slice(i, i + window)); 283 | 284 | const answer = map(windows, average); 285 | 286 | // 유틸 함수 287 | const range = (start: number, end: number) => { 288 | const result = []; 289 | for (let i = start; i < end; i++) { 290 | result.push(i); 291 | } 292 | return result; 293 | }; 294 | ``` 295 | 296 | ### 체이닝 팁 요약 297 | 298 | - 데이터 만들기 299 | - 배열 전체를 다루기 300 | - 작은 단계로 나누기 301 | - 조건문을 filter 로 바꾸기 302 | - 유용한 함수 추출하기 303 | - 개선을 위해 실험하기(노력) 304 | 305 | ### 체이닝 디버깅을 위한 팁 306 | 307 | - 구체적인 것을 유지 308 | - 출력해보기 309 | - 타입 따라가기 310 | 311 | ### 다양한 함수형 도구 312 | 313 | pluck() 314 | 315 | ```typescript 316 | const pluck = (array, field) => { 317 | return map(array, (obj) => obj[field]); 318 | }; 319 | 320 | const prices = pluck(products, 'price'); 321 | ``` 322 | 323 | concat() 324 | 325 | ```typescript 326 | const concat = (arrays) => { 327 | const ret = []; 328 | forEach(arrays, (array) => { 329 | forEach(array, (item) => { 330 | ret.push(item); 331 | }); 332 | }); 333 | return ret; 334 | }; 335 | 336 | const purchaseArrays = pluck(customers, 'purchases'); 337 | const allPurchases = concat(purchaseArrays); 338 | ``` 339 | 340 | frequenciesBy(), groupBy() 341 | 342 | ```typescript 343 | const frequenciesBy = (array, f) => { 344 | const ret = {}; 345 | forEach(array, (item) => { 346 | const key = f(item); 347 | if (ret[key]) ret[key] += 1; 348 | else ret[key] = 1; 349 | }); 350 | return ret; 351 | }; 352 | 353 | const groupBy = (array, f) => { 354 | const ret = {}; 355 | forEach(array, (item) => { 356 | const key = f(item); 357 | if (ret[key]) ret[key].push(item); 358 | else ret[key] = [item]; 359 | }); 360 | return ret; 361 | }; 362 | ``` 363 | 364 | ### 값을 만들기 위한 reduce() 365 | 366 | ```typescript 367 | const itemsAdded = ['shirt', 'shoes', 'socks', 'hat']; 368 | 369 | const shippingCart = reduce( 370 | itemsAdded, 371 | (cart, item) => { 372 | if (!cart[item]) { 373 | return addItem(cart, { 374 | name: item, 375 | quantity: 1, 376 | price: priceLookup(item), 377 | }); 378 | } else { 379 | const quantity = cart[item].quantity; 380 | return setFieldByName(cart, item, 'quantity', quantity + 1); 381 | } 382 | }, 383 | {} 384 | ); 385 | ``` 386 | 387 | ```typescript 388 | const shippingCart = reduce(itemsAdded, addOne, {}); 389 | 390 | const addOne = (cart, item) => { 391 | if (!cart[item]) { 392 | return addItem(cart, { 393 | name: item, 394 | quantity: 1, 395 | price: priceLookup(item), 396 | }); 397 | } else { 398 | const quantity = cart[item].quantity; 399 | return setFieldByName(cart, item, 'quantity', quantity + 1); 400 | } 401 | }; 402 | ``` 403 | 404 | ### 데이터를 사용해 창의적으로 만들기 405 | 406 | ```typescript 407 | const itemOps = [ 408 | ['add', 'shirt'], 409 | ['add', 'shoes'], 410 | ['remove', 'socks'], 411 | ['remove', 'hat'], 412 | ]; 413 | 414 | const shippingCart = reduce( 415 | itemOps, 416 | (cart, itemOp) => { 417 | const op = itemOp[0]; 418 | const item = itemOp[1]; 419 | 420 | if (op === 'add') return addOne(cart, item); 421 | if (op === 'remove') return removeOne(cart, item); 422 | }, 423 | {} 424 | ); 425 | ``` 426 | -------------------------------------------------------------------------------- /docs/12-chapter_14.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 14장 중첩된 데이터에 함수형 도구 사용하기 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 해시 맵에 저장된 값을 다루기 위한 고차 함수를 만듭니다. 10 | - 중첩된 데이터를 고차 함수로 쉽게 다루는 방법을 배웁니다. 11 | - 재귀를 이해하고 안전하게 재귀를 사용하는 방법을 살펴봅니다. 12 | - 깊이 중첩된 엔티티에 추상화 벽을 적용해서 얻을 수 있는 장점을 이해합니다. 13 | 14 | 카피-온-라이트 기법에 대해 알아보면서 이야기하기도 했지만 중첩된 데이터를 불변값으로 다루기란 쉬운 일이 아닙니다. 중첩된 객체에 참조로 접근할 수 있다는 사실은 언제든 그 객체가 개발자 모르게 혹은 부주의로 인하여 쉽게 손상될 수 있음을 뜻하기 때문이죠. 15 | 16 | 이러한 중첩된 객체들을 함수형 프로그래밍의 방식으로 안전하게 다루는 또 다른 기법에 대해 알아보겠습니다. 먼저 객체를 다루기 위한 고차 함수를 직접 만들고 리팩터링하며 이야기해보겠습니다. 17 | 18 | ## 필드명을 명시적으로 만들기 19 | 20 | ### 주어진 함수들을 리팩터링 해보자 21 | 22 | ```ts 23 | // 수량 필드명이 함수에 하드코딩 되어있음 24 | function incrementQuantity(item: Item) { 25 | const quantity = item.quantity; 26 | const newQuantity = quantity + 1; 27 | const newItem = objectSet(item, 'quantity', newQuantity); 28 | return newItem; 29 | } 30 | ``` 31 | 32 | ```ts 33 | // 크기 필드명이 함수에 하드코딩 되어있음 34 | function incrementSize(item: Item) { 35 | const size = item.size; 36 | const newSize = size + 1; 37 | const newItem = objectSet(item, 'size', newSize); 38 | return newItem; 39 | } 40 | ``` 41 | 42 | 두 함수는 함수 이름에 암묵적 인자가 들어 있습니다. 이전 장에서 배운 방식대로 `암묵적 인자를 드러내기` 리팩터링 해보겠습니다. 43 | 44 | ```ts 45 | function incrementField(item: Item, field: string) { 46 | const value = item[field]; 47 | const newValue = value + 1; 48 | const newItem = objectSet(item, field, newValue); 49 | return newItem; 50 | } 51 | ``` 52 | 53 | 하지만 더하기, 빼기, 곱하기, 나누기, ... 등의 연산을 수행하는 함수를 만들려면 어떻게 해야 할까요? 이런 함수들은 모두 `업데이트`를 하기 위한 목적을 가지고 있습니다. 또 한겹의 중복이 눈에 띕니다. `업데이트`를 하기 위한 목적을 가지고 있는 함수들을 `업데이트` 함수로 만들어보겠습니다. 54 | 55 | 인자로 객체를 받고, 어떤 키값에 접근할지에 대한 단서를 함께 제공하면 원하는 인터페이스를 구성할 수 있을 것 같습니다. 56 | 57 | ```ts 58 | function incrementField(item: Item, field: string) { 59 | return updateField(item, field, function (value) { 60 | return value + 1; 61 | }); 62 | } 63 | 64 | function updateField(item: Item, field: string, modify: (value: any) => any) { 65 | const value = item[field]; 66 | const newValue = modify(value); 67 | const newItem = objectSet(item, field, newValue); // objectSet은 카피-온-라이트를 수행하는 함수 68 | return newItem; 69 | } 70 | ``` 71 | 72 | `함수 본문을 콜백으로 바꾸기` 리팩터링을 통해 `updateField` 함수에 콜백을 전달하였고, 제어권을 전달 받은 본래 함수는 콜백을 호출하는 모습을 볼 수 있습니다. 73 | 74 | ## 함수형 도구 : update() 75 | 76 | `update()`는 객체(해시 맵 대신 쓰고 있는)를 다루는 함수형 도구로, 값 하나를 인자로 받아 객체에 적용합니다. 하나의 키에 하나의 값을 변경합니다. 따라서 1) 객체, 2) 변경할 값이 어디 있는지 알려주는 키, 3) 값을 변경하는 동작이 서술된 함수, 총 3가지 요소가 필요합니다. 77 | 78 | 앞선 코드에서 `update()`만 추출해보겠습니다. 79 | 80 | ```ts 81 | function update( 82 | object: Record, 83 | key: string, 84 | modify: (value: any) => any 85 | ) { 86 | const value = object[key]; 87 | const newValue = modify(value); 88 | const newObject = objectSet(item, key, newValue); 89 | return newObject; 90 | } 91 | ``` 92 | 93 | `update()`에 전달하는 함수는 값을 돌려주는 `계산`이어야 하고, 현재 값을 인자로 받아 새로운 계산 값을 리턴합니다. 94 | 95 | 이제 앞선 예제에서의 `incrementField()`를 `update()`를 사용하여 다시 작성해보겠습니다. 96 | 97 | ```ts 98 | function incrementField(item: Item, field: string) { 99 | return update(item, field, function (value) { 100 | return value + 1; 101 | }); 102 | } 103 | ``` 104 | 105 | 사실 이름 말고는 달라질 건 없습니다만, Item이라는 맥락을 벗어나서 어떤 객체에서든 사용할 수 있는 함수가 되었습니다. 여기에서 update의 세 번째 인자로 넘겨준 콜백 함수 덕분에 update 함수는 동작을 개발자에게 위임하고, 다양한 동작을 할 수 있게 되었습니다. 106 | 107 | ## 중첩된 update 시각화하기 108 | 109 | update 함수를 작성하여 객체를 다룰 수 있게 되었습니다. 하지만 객체가 꼭 한 단계만 있을 수 있는 것은 아닙니다. 객체 안에 객체가 있을 수도 있고, 객체 안에 배열이 있을 수도 있습니다. 중첩된 객체를 다룰 수 있도록 조금 더 손봐주도록 하겠습니다. 110 | 111 | ## 중첩된 데이터에 update() 사용하기 112 | 113 | ```ts 114 | function incrementSize(item: Item) { 115 | return update(item, 'size', function (value) { 116 | return update(value, 'width', function (value) { 117 | return value + 1; 118 | }); 119 | }); 120 | } 121 | ``` 122 | 123 | 앞서 작성했던 incrementSize 함수를 update를 이용하여 한번 리팩터링 해보았습니다. 하지만 이렇게 작성하면 코드가 너무 길어지고, 가독성이 떨어집니다. 124 | 125 | 우리는 'size' -> 'width' 순서대로 객체를 탐색할 계획입니다. 이처럼 순서에 의존하는 자료형으로는 배열이 가장 적합하겠습니다. 즉, 다음과 같은 형태의 코드가 되면 좋겠네요. 126 | 127 | ```ts 128 | function incrementSize(item: Item) { 129 | return update(item, ['size', 'width'], function (value) { 130 | return value + 1; 131 | }); 132 | } 133 | ``` 134 | 135 | 이제 이 코드를 작성해보겠습니다. 객체의 키를 타고 들어가며 `update`라는 동작을 반복해주면 됩니다. 하지만 for문을 가지고 작성하려니 코드가 지저분해질 것만 같고 조금 막막해지는 기분입니다. 136 | 137 | 우리는 앞서 `문(Statement)`을 `표현식(Expression)`의 차이에 대하여 배운 바 있습니다. 이럴 때 자바스크립트에서 일급으로 취급하는 값인 함수를 이용하면 코드를 깔끔하게 작성할 수 있습니다. 즉, 재귀를 사용하는 것이지요. 138 | 139 | ```ts 140 | function update(target: T, path: string[], updater: (value: any) => any): T { 141 | // 재귀 종료 조건 142 | if (path.length === 0) { 143 | return updater(target); 144 | } 145 | // 경로에서 첫번째 키를 추출한 다음, 나머지 경로는 다음 재귀 호출에 전달 146 | const [key, ...rest] = path; 147 | return update(target[key], rest, updater); 148 | } 149 | ``` 150 | 151 | ## 안전한 재귀 사용법 152 | 153 | 이렇게 재귀를 사용하면 코드가 깔끔해지지만, 재귀를 사용할 때는 주의해야 할 점이 있습니다. 재귀를 사용할 때는 반드시 종료 조건을 명시해주어야 합니다. 그렇지 않으면 무한 루프에 빠질 수 있습니다. 154 | 155 | 개인적으로 생각하기에 재귀는 함수형 프로그래밍이 가지는 정말 강력한 도구 중 하나라고 생각합니다. 객체지향에서 각 클래스가 특정 역할을 수행하도록 책임을 나누는 것과 같이, 함수형 프로그래밍에서는 재귀가 특정 동작을 수행하도록 동작을 추상화하는 것이라고 생각합니다. 156 | 157 | ### 1. 종료 조건 158 | 159 | 재귀를 멈추려면 종료 조건이 필요합니다. 위의 코드에서는 `path.length === 0`이 종료 조건입니다. 이 조건이 없다면 무한 루프에 빠질 수 있습니다. 160 | 161 | 배열의 인자가 비었다는 조건이나, 점점 줄어드는 어떠한 값을 종료 조건으로 만들 수 있습니다. 162 | 163 | ### 2. 재귀 호출 164 | 165 | 재귀를 사용할 때는 최소한 하나의 재귀 호출(recursive call)이 있어야 합니다. 재귀 호출이 없다면 재귀는 단순히 반복문과 다를 바가 없습니다. 166 | 167 | 위의 코드에서는 `update(target[key], rest, updater)`가 재귀 호출입니다. 168 | 169 | ### 3. 종료 조건에 다가가기 170 | 171 | 재귀 함수를 만들 때는 최소 하나 이상의 인자가 점점 줄어들어 종료 조건으로 다가가야 합니다. 만약 배열을 기준으로 종료 조건을 잡고 있다면, 각 단계를 거칠 때마다 배열의 항목이 사라져야 합니다. 172 | 173 | 위의 코드에서는 `path`가 `rest`로 점점 줄어들어 종료 조건으로 다가가고 있습니다. 174 | 175 | ## 깊이 중첩된 구조를 설계할 때 생각할 점 176 | 177 | 하지만 여전히 문제가 남아있습니다. 처음보다는 훨씬 사용하기 좋은 인터페이스의 함수가 되었지만, 그 반대 급부로 특정 상황에서 사용하는 경우 사용자가 update 함수에 너무 많은 구체적인 정보를 전달해야 한다는 부작용이 생겼습니다. 178 | 179 | 아래 코드를 살펴보겠습니다. 180 | 181 | ```ts 182 | httpGet('http://my-blog/com/api/category/blog', (blogCategory) => { 183 | renderCategory( 184 | update(blogCategory, ['posts', '12', 'author', 'name'], (name) => 185 | name.toUpperCase() 186 | ) 187 | ); 188 | }); 189 | ``` 190 | 191 | 과연 `["posts", "12", "author", "name"]`이 무엇을 의미하는지 알 수 있을까요? 이 코드는 블로그 카테고리의 12번째 포스트의 작성자의 이름을 대문자로 바꾸는 코드입니다. 하지만 이 코드를 처음 보는 사람은 이 코드가 무엇을 의미하는지 알기 어렵습니다. 192 | 193 | 이럴 때는 적당히 추상화를 해서 사용자가 너무 많은 정보를 전달하지 않도록 해야 합니다. 바로 `추상화 벽`을 이용할 때입니다. 194 | 195 | 특정한 포스트를 id를 기준으로 찾아 수정하는 함수를 만들어보겠습니다. 196 | 197 | ```ts 198 | function updatePostById( 199 | category: Category, 200 | postId: string, 201 | modifyPost: (post: Post) => Post 202 | ): Category { 203 | return update(category, ['posts', postId], modifyPost); 204 | } 205 | ``` 206 | 207 | 사용자 이름을 대문자로 바꾸는 함수를 만들어볼 수도 있습니다. 208 | 209 | ```ts 210 | function capitalizeName(user: User) { 211 | return update(user, ['name'], (name) => name.toUpperCase()); 212 | } 213 | ``` 214 | 215 | 추상화 벽으로 감싼 두 함수를 합치면 다음과 같은 결과물을 얻을 수 있습니다. 216 | 217 | ```ts 218 | httpGet('http://my-blog/com/api/category/blog', (blogCategory) => { 219 | renderCategory( 220 | updatePostById(blogCategory, '12', (post) => capitalizeName(post.author)) 221 | ); 222 | }); 223 | ``` 224 | 225 | ## 앞에서 배운 고차 함수들 226 | 227 | 그동안 배운 고차 함수들을 정리해보겠습니다. 228 | 229 | ### 배열을 반복할 때 for 반복문 대신 사용하기 230 | 231 | - forEach() 232 | - map() 233 | - filter() 234 | - reduce() 235 | 236 | ### 중첩된 데이터를 효율적으로 다루기 237 | 238 | - update() 239 | - nestedUpdate() 240 | 241 | ### 카피-온-라이트 원칙 적용하기 242 | 243 | - withArrayCopy() 244 | - withObjectCopy() 245 | 246 | ### try/catch 로깅 규칙을 코드화 247 | 248 | - wrapLogging() 249 | 250 | ## 요점 정리 251 | 252 | - update()는 일반적인 패턴을 구현한 함수형 도구입니다. 이 함수를 사용하면 중첩된 데이터를 효율적으로 다룰 수 있습니다. 253 | 254 | - nestedUpdate()는 update()를 확장한 함수입니다. 키 경로를 배열로 전달하면 깊이 중첩된 데이터도 쉽게 다룰 수 있습니다. 255 | 256 | - 보통 일반적인 반복문은 재귀보다 명확합니다. 하지만 중첩된 데이터를 다룰 때는 재귀가 더 쉽고 명확합니다. 257 | 258 | - 재귀는 스스로 불렸던 곳이 어디인지 유지하기 위해 스택을 사용합니다. 재귀 함수에서 스택은 줕첩된 데이터 구조를 그대로 반영합니다. (오호;) 259 | 260 | - 깊이 중첩된 데이터는 이해하기 어렵습니다. 깊이 중첩된 데이터를 다룰 때 모든 데이터 구조와 어떤 경로에 어떤 키가 있는지 기억해야 합니다. 261 | 262 | - 많은 키를 가지고 있는 깊이 중첩된 구조에 추상화 벽을 사용하면 알아야 할 것이 줄어듭니다. 추상화 벽으로 깊이 중첩된 데이터 구조를 쉽게 다룰 수 있습니다. 263 | -------------------------------------------------------------------------------- /docs/13-chapter_15.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 15장 타임라인 격리하기 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 코드를 타임라인 다이어그램으로 그리는 방법을 배웁니다. 10 | - 버그를 찾기 위해 타임라인 다이어그램 보는 법을 이해합니다. 11 | - 타임라인끼리 공유하는 자원을 줄여 코드 설계를 개선하는 방법을 알아봅니다. 12 | 13 |

14 | 15 | ## 장바구니 버튼을 빠르게 두 번 클릭했을 때 일어나는 일 16 | 17 | ```ts 18 | const calc_cart_total = () => { 19 | total = 0; 20 | cost_ajax(cart, (cost) => { 21 | total += cost; 22 | shipping_ajax(cart, (shipping) => { 23 | total += shipping; 24 | update_total_dom(total); 25 | }); 26 | }); 27 | }; 28 | 29 | const add_item_to_cart = (name: string, price: number, quantity: number) => { 30 | cart = add_item(cart, name, price, quantity); 31 | calc_cart_total(); 32 | }; 33 | ``` 34 | 35 | 빠르게 클릭했을 때 동일하지 않은 결과가 나왔다. 장바구니 금액이 계속 변화했다.
36 | 이제 여기에서 왜 버그가 나타났는지, 어떻게 해결하는지 타임라인 다이어그램을 통해 알아보자. 37 | 38 |
39 |
40 | 41 | ## 두 가지 타임라인 다이어그램 기본 규칙 42 | 43 | > 1. 두 액션이 순서대로 나타나면 같은 타임라인,
44 | > 2. 두 액션이 동시에 실행되거나 순서를 예상할 수 없다면 분리된 타임라인에 넣는다. 45 | 46 | 이런 규칙을 이용해 타임라인 다이어그램으로 그리면 코드가 시간이 지나며 어떻게 실행되는지 파악할 수 있다. 47 | 48 |
49 |
50 | 51 | ## 액션 순서에 관한 두 가지 사실 52 | 53 | ### 1. ++와 +=는 사실 3단계! 54 | 55 | 400쪽을 보면 사실 `total++`라는 것이 3단계로 이루어진다고 설명한다. 56 | 57 | ```ts 58 | let temp = total; // 읽기 (액션) 59 | temp = temp + 1; // 더하기 (계산) 60 | total = temp; // 쓰기 (액션) 61 | ``` 62 | 63 | 따라서! 다이어그램으로 그리게 되면 두 개의 액션으로 표시해야 한다. (읽기 + 쓰기) 64 | 65 | ### 2. 인자는 함수를 부르기 전에 실행합니다. 66 | 67 | 인자는 함수에 전달되기 전에 실행되므로, 타임라인 다이어그램을 그릴 때에 순서대로 표현해야 한다. 68 | `console.log(total)`이라는 코드는 아래와 같이 2단계로 일어난다. (total 읽기 + console.log()) 69 | 70 | ```ts 71 | const temp = total; 72 | console.log(temp); 73 | ``` 74 | 75 | 이와 같은 2가지 액션 순서에 대한 배경 지식을 가지고 본격적으로 타임라인을 그려보도록 하자.. 76 | 77 | ## add-to-cart 타임라인 그리기 78 | 79 | 타임라인을 그리는 단계는 3단계로 정리할 수 있다. 80 | 81 | > 1. 액션을 확인하고,
82 | > 2. 순서대로 실행되거나 동시에 실행되는 액션을 그린다.
83 | > 3. 마지막으로 플랫폼에 특화된 지식을 활용하여 다이어그램을 단순화한다. 84 | 85 |

86 | 87 | ### 1단계: 액션을 확인한다. 88 | 89 | ![Identify the actions](https://drek4537l1klr.cloudfront.net/normand/Figures/figure_14-3.png) 90 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 91 | 92 | 밑줄 친 부분은 모두 액션. 계산은 다이어그램에 포함하지 않는다. (실행 순서와는 관련이 없으니!)
93 | 총 13개의 액션 중에도 비동기 콜백 두 개가 보인다. (5번, 9번) 94 | 95 | #### 비동기 호출은 새로운 타임라인으로 그리자 96 | 97 | 앞에서 비동기 콜백은 새로운 타임라인으로 그려야 한다는 것을 배웠다.
98 | 여기서 자바스크립트 비동기 콜백이 어떻게 동작하는지 되짚어보자. 99 | 100 | > - 단일 스레드, 비동기: 스레드가 하나이기 때문에 입출력 작업을 하려면 비동기 모델 사용해야 함. 입출력의 결과는 콜백으로 받을 수 있으나 101 | > 언제 끝날지 알 수 없기 때문에 타임라인을 분리해야 함. 102 | 103 | #### 한 단계씩 타임라인 만들기 104 | 105 | 앞에서 이야기한 배경지식을 가지고 아래의 코드를 타임라인으로 만들어보자. 106 | 107 | ```ts 108 | // 1 109 | saveUserAjax(user, () => { 110 | // 2 111 | setUserLoadingDOM(false); 112 | }); 113 | // 3 114 | setUserLoadingDOM(true); 115 | // 4 116 | saveDocumentAjax(document, () => { 117 | // 5 118 | setDocLoadingDOM(false); 119 | }); 120 | // 6 121 | setDocLoadingDOM(true); 122 | ``` 123 | 124 | 결과물은 아래와 같다. 125 | 126 | ![1 step](https://drek4537l1klr.cloudfront.net/normand/Figures/f0405-04.jpg) 127 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 128 | 129 |

130 | 131 | ### 2단계: 순서대로 실행되거나 동시에 실행되는 액션을 그린다. 132 | 133 | 이번에는 앞서 이야기 나눴던 장바구니 코드를 가지고 타임라인을 작성해보자. 134 | 135 | ![2 step](https://drek4537l1klr.cloudfront.net/normand/Figures/figure_14-7.png) 136 | 위와 같이 액션을 확인했다. 다이어그램을 그려보면 아래와 같다. 137 | 138 | ![2 step](https://drek4537l1klr.cloudfront.net/normand/Figures/f0406-01.jpg) 139 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 140 | 141 | #### 타임라인 다이어그램으로 순서대로 실행되는 코드에도 두 가지 종류가 있다는 것을 알 수 있다. 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 155 | 158 | 159 | 160 |
순서가 섞일 수 있는 코드순서가 섞이지 않는 코드
153 | 154 | 156 | 157 |
161 | 162 | 왼쪽은 다른 타임라인에 있는 액션이 끼어들 수 있는 반면 오른쪽에서는 그런 일이 발생하지 않는다. 따라서 타임라인을 짧게, 즉 박스를 더 적게 가져가는 것이 관리하기 쉽다. 163 | 164 | #### 타임라인 다이어그램으로 동시에 실행되는 코드는 순서를 예측할 수 없다는 것을 알 수 있다. 165 | 166 | 타임라인에서 나란히 표현된 액션은 정확한 순서를 예측할 수 없다. 타임라인이 하나라면 실행 가능한 순서는 하나이겠지만 박스가 많아질수록, 타임라인이 늘어날수록 167 | 예상 가능한 실행 순서는 배로 늘어난다는 사실을 알아야 한다. 168 | 169 | ![Timeline diagrams capture the uncertain ordering of parallel code](https://drek4537l1klr.cloudfront.net/normand/Figures/f0408-02.jpg) 170 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 171 | 172 | #### 좋은 타임라인의 원칙 173 | 174 | 1. 타임라인은 적을수록 이해하기 쉽다. 175 | > 하지만 요즘의 시스템은 어쩔 수 없이 여러 타임라인이 존재한다. 176 | 2. 타임라인은 짧을수록 이해하기 쉽다. 177 | > 그래야 실행 가능한 순서의 수도 줄어드니까! 178 | 3. 공유하는 자원이 적을수록 이해하기 쉽다. 179 | > 서로 자원을 공유하지 않는다면 실행 순서를 신경 쓸 필요가 없으니까! 180 | 4. 자원을 공유해야 한다면 서로 조율해야 한다. 181 | > 피할 수 없다면 안전하게라도 공유해라. 즉 올바른 순서대로 자원을 쓰고 돌려줘라. 182 | 5. 시간을 일급으로 다룬다. 183 | > 재사용 가능한 객체를 만들면 타이밍 문제를 쉽게 만들 수 있는데~ 요거는 다음 시간 북리더가 설명해줄 것.. 184 | 185 |

186 | 187 | ### 3단계: 타임라인 단순화하기 188 | 189 | 우리는 자바스크립트가 비동기를 어떻게 처리하는지 알고 있다. 따라서 넘어가겠다. 190 | 191 | > 1. 자바스크립트는 싱글스레드 언어이기 때문에 하나의 타임라인에 있는 모든 액션을 하나로 통합할 수 있었다. 192 | > ![Consolidate all actions on a single timeline](https://drek4537l1klr.cloudfront.net/normand/Figures/f0415-02.jpg) 193 | > 2. 그리고 타임라인이 끝나는 곳에서 새로운 타임라인이 하나 생긴다면 통합할 수 있다. 194 | > 하지만 이 경우 첫 번째 타임라인이 끝나는 곳에 새로운 타임라인이 2개 생기므로 통합하지 않았다. 195 | > 이런 경우 예상 가능한 실행 순서는 2가지다. 196 | 197 | ![Consolidate all actions on a single timeline](https://drek4537l1klr.cloudfront.net/normand/Figures/f0420-03.jpg) 198 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 199 | 200 | 이번에는 `add-to-cart` 타임라인을 똑같은 과정을 거쳐 단순화해보자. 201 | 202 | > 1. 자바스크립트는 싱글스레드 언어이기 때문에 하나의 타임라인에 있는 모든 액션을 하나로 통합할 수 있었다. 203 | > ![Consolidate all actions on a single timeline](https://drek4537l1klr.cloudfront.net/normand/Figures/f0423-01.jpg) 204 | > 2. 그리고 타임라인이 끝나는 곳에서 새로운 타임라인이 하나 생긴다면 통합할 수 있다. 205 | > ![Consolidate all actions on a single timeline](https://drek4537l1klr.cloudfront.net/normand/Figures/f0423-02.jpg) 206 | 207 |
208 |
209 | 210 | ## 타임라인을 나란히 보면 문제가 보인다 211 | 212 | ![Two fast clicks can get the wrong result](https://drek4537l1klr.cloudfront.net/normand/Figures/f0429-01.jpg) 213 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-15/1 214 | 215 | 실행 가능한 순서는 10가지. 어떻게 해결할 수 있을까? 216 | 217 |
218 |
219 | 220 | ## 공유하는 자원을 없애 문제를 해결하자 221 | 222 | 결국 지금 발생하는 문제는 공유하는 자원 때문에 발생한다. 두 타임라인 모두 cart, total이라는 전역변수를 공유하기 때문에 실행 순서가 섞인 상태로 전역변수에 접근하며 버그가 발생하는 것. 223 | 224 | ### 1) 전역변수를 지역변수로 바꾸기 225 | 226 | total을 먼저 지역변수로 바꾸자. 227 | 228 | ```ts 229 | const calc_cart_total = () => { 230 | total = 0; // 전역변수 231 | cost_ajax(cart, (cost) => { 232 | total += cost; 233 | shipping_ajax(cart, (shipping) => { 234 | total += shipping; 235 | update_total_dom(total); 236 | }); 237 | }); 238 | }; 239 | ``` 240 | 241 | ```ts 242 | const calc_cart_total = () => { 243 | let total = 0; // 지역변수 244 | cost_ajax(cart, (cost) => { 245 | total += cost; 246 | shipping_ajax(cart, (shipping) => { 247 | total += shipping; 248 | update_total_dom(total); 249 | }); 250 | }); 251 | }; 252 | ``` 253 | 254 | total을 읽고 쓰는 동작은 더이상 액션이 아니기 때문에 타임라인에서 빠진다. 255 | 256 | ### 2) 전역변수를 인자로 바꾸기 257 | 258 | 암묵적 입력이 적은 액션이 더 좋다는 것을 앞에서 배웠다. 259 | 260 | ```ts 261 | const calc_cart_total = () => { 262 | let total = 0; 263 | cost_ajax(cart, (cost) => { 264 | // 암묵적 인자 265 | total += cost; 266 | shipping_ajax(cart, (shipping) => { 267 | // 암묵적 인자 268 | total += shipping; 269 | update_total_dom(total); 270 | }); 271 | }); 272 | }; 273 | 274 | const add_item_to_cart = (name: string, price: number, quantity: number) => { 275 | cart = add_item(cart, name, price, quantity); 276 | calc_cart_total(); 277 | }; 278 | ``` 279 | 280 | ```ts 281 | const calc_cart_total = (cart: number) => { 282 | let total = 0; 283 | cost_ajax(cart, (cost) => { 284 | // 전역변수를 읽지 않음 285 | total += cost; 286 | shipping_ajax(cart, (shipping) => { 287 | // 전역변수를 읽지 않음 288 | total += shipping; 289 | update_total_dom(total); 290 | }); 291 | }); 292 | }; 293 | 294 | const add_item_to_cart = (name: string, price: number, quantity: number) => { 295 | cart = add_item(cart, name, price, quantity); 296 | calc_cart_total(cart); // 인자로 바꾸기 297 | }; 298 | ``` 299 | 300 | ![There’s still a bug in this code.](https://drek4537l1klr.cloudfront.net/normand/Figures/f0432-03.jpg) 301 | 302 | 하지만 아직 버그가 남아 있는데, DOM 자원을 공유하고 있다. 요것은 다음 북리더가 친절하게 설명해 주실 것이다... 303 | 304 | > 그래서 이제 이 함수는 계산이 아닌가요? 305 | > 306 | > 전역변수를 모두 없애긴 했지만 이 함수는 계산이 아니다. 서버에 두 번 접근하기 때문이다. 또한 DOM을 업데이트하는 부분도 있다. 307 | > 둘 다 액션이기 때문에 아직은 계산이 아니지만.. 계산에 가까워졌다..!!
308 | > 따라서 이 함수는 실행 시점에 덜 의존하게 되었다. 309 | 310 |
311 |
312 | 313 | ## 더 나아가 재사용하기 좋은 코드로 만들어보자 314 | 315 | 4,5장에서 배운 것처럼 DOM을 바꾸는 것은 암묵적 출력이다. 하지만 비동기 콜백이 완료되어야 하기 때문에 리턴값으로는 전달할 수 없다. 316 | 317 | 이럴 때에는 아래와 같이 콜백 함수로 전달하면 된다. 318 | 319 | ```ts 320 | const calc_cart_total = (cart: number, callback: () => void) => { 321 | // 콜백 인자로 바꾸기 322 | let total = 0; 323 | cost_ajax(cart, (cost) => { 324 | total += cost; 325 | shipping_ajax(cart, (shipping) => { 326 | total += shipping; 327 | callback(total); 328 | }); 329 | }); 330 | }; 331 | 332 | const add_item_to_cart = (name: string, price: number, quantity: number) => { 333 | cart = add_item(cart, name, price, quantity); 334 | calc_cart_total(cart, update_total_dom); 335 | }; 336 | ``` 337 | 338 |
339 |
340 | 341 | ## 요점 정리 342 | 343 | - 타임라인 === 동시에 실행될 수 있는 순차적 액션 344 | - 서로 다른 타임라인에 있는 액션은 끼어들 수 있어서 여러 개의 실행 가능한 순서가 됨. 실행 가능한 순서가 많을 때 버그가 없는지 찾기 힘듦. 345 | - 타임라인 다이어그램으로 서로 영향을 주고 받는 부분이 어디인지 알 수 있음. 346 | - 언어에서 지원하는 스레드 모델을 이해해야 함. 분산 시스템에서 어떤 부분이 동시에 실행되고 순서대로 실행되는지 파악해야 함. 347 | - 자원을 공유하는 부분은 버그가 나기 쉬움. 따라서 공유 자원을 최소화할 것. 348 | - 자원을 공유하지 않는 타임라인은 독립적으로 이해하고 실행할 수 있음. 따라서 함께 생각해야 할 내용이 줄어듬. 349 | -------------------------------------------------------------------------------- /docs/14-chapter_16.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 16장 타임라인 사이에 자원 공유하기 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 자원을 공유해서 생기는 버그를 찾는 방법을 배웁니다. 10 | - 안전하게 자원을 공유할 수 있는 자원 공유 기본형을 만드는 방법을 이해합니다. 11 | 12 | ## 좋은 타임라인의 원칙 13 | 14 | 1. 타임라인은 적을수록 이해하기 쉽습니다. 15 | 2. 타임라인은 짧을수록 이해하기 쉽습니다. 16 | 3. 공유하는 자원이 적을수록 이해하기 쉽습니다. 17 | 4. 자원을 공유한다면 서로 조율해야 합니다. 18 | 5. 시간을 일급으로 다룹니다. 19 | 20 | 이번장에는 좋은 타임라인의 원칙 중 네번째 원칙에 집중합니다. 21 | 이 방법을 사용하면 서로 안전하게 자원을 공유할 수 있습니다. 22 | 23 | ## 장바구니에 아직 버그가 있습니다 24 | 25 | 첫 번째 클릭: cart 읽기 -> cart 쓰기 -> DOM 업데이트 26 | 두 번째 클릭: cart 읽기 -> cart 쓰기 -> DOM 업데이트 27 | 28 | 첫번째 클릭과 두번째 클릭이 수행하는 작업중 DOM 을 사용하는 작업은 자원을 공유합니다. 29 | 그러므로 해당 장바구니의 문제를 해결하기 위해서는 실행되는 순서를 보장해야합니다. 30 | 31 | ## DOM이 업데이트되는 순서를 보장해야 합니다 32 | 33 | 큐를 사용합니다. 큐는 FIFO 자료 구조로 실행한 순서를 보장해줍니다. 34 | micro task, macro task, 리액트 렌더링 등에도 큐를 사용합니다. 35 | 36 | ## 자바스크립트에서 큐 만들기 37 | 38 | ### 자바스크립트에는 큐 자료 구조가 없기 때문에 만들어야 합니다. 39 | 40 | 큐는 자료 구조지만 타임라인 조율에 사용한다면 동시성 기본형(concurrency primitive)이라고 부릅니다. 41 | 동시성 기본형은 자원을 안전하게 공유할 수 있는 재사용 가능한 코드를 말합니다. 42 | 43 | 원래 타임라인 44 | 클릭 핸들러: cart 읽기 -> cart 쓰기 -> cost_ajax() -> shipping_ajax() -> DOM 업데이트 45 | 46 | 분리된 타임라인 47 | 클릭 핸들러: cart 읽기 -> cart 쓰기 -> 나머지 작업들은 합쳐서 큐에 추가 48 | 큐 작업: 큐에서 작업 꺼냄 -> cost_ajax() -> shipping_ajax() -> DOM 업데이트 49 | 50 | ### 큐에서 처리할 작업을 큐에 넣기 51 | 52 | ```typescript 53 | const queueItems: Cart[] = []; 54 | 55 | function updateTotalQueue(cart: Cart) { 56 | queueItems.push(cart); 57 | } 58 | 59 | function onClickAddCart() { 60 | cart = addItem(cart, item); 61 | updateTotalQueue(cart); 62 | } 63 | ``` 64 | 65 | ### 큐에 있는 첫 번째 항목을 실행합니다. 66 | 67 | ```typescript 68 | function runNext() { 69 | const cart = queueItems.shift(); 70 | calculateCartTotal(cart, updateTotalDOM); 71 | } 72 | 73 | function updateTotalQueue(cart: Cart) { 74 | queueItems.push(cart); 75 | setTimeout(runNext, 0); 76 | } 77 | ``` 78 | 79 | ### 두 번째 타임라인이 첫 번째 타임라인과 동시에 실행되는 것을 막기 80 | 81 | ```typescript 82 | let working = false; 83 | 84 | function runNext() { 85 | if (working) return; 86 | working = true; 87 | const cart = queueItems.shift(); 88 | calculateCartTotal(cart, updateTotalDOM); 89 | } 90 | ``` 91 | 92 | 참고 키워드: 임계 영역 93 | 94 | ### 다음 작업을 시작할 수 있도록 calculateCartTotal() 콜백 함수를 고쳐봅시다. 95 | 96 | ```typescript 97 | function runNext() { 98 | if (working) return; 99 | working = true; 100 | const cart = queueItems.shift(); 101 | calculateCartTotal(cart, function (total) { 102 | updateTotalDOM(total); 103 | working = false; 104 | runNext(); 105 | }); 106 | } 107 | ``` 108 | 109 | ### 항목이 없을 때 멈추게 하기 110 | 111 | ```typescript 112 | function runNext() { 113 | if (working) return; 114 | if (!queueItems.length) return; 115 | working = true; 116 | const cart = queueItems.shift(); 117 | calculateCartTotal(cart, function (total: number) { 118 | updateTotalDOM(total); 119 | working = false; 120 | runNext(); 121 | }); 122 | } 123 | ``` 124 | 125 | ### 변수와 함수를 함수 범위로 넣기 126 | 127 | ```typescript 128 | function Queue() { 129 | const queueItems: Cart[] = []; 130 | let working = false; 131 | 132 | function runNext() { 133 | if (working) return; 134 | if (!queueItems.length) return; 135 | working = true; 136 | const cart = queueItems.shift(); 137 | calculateCartTotal(cart, function (total: number) { 138 | updateTotalDOM(total); 139 | working = false; 140 | runNext(); 141 | }); 142 | } 143 | 144 | return function (cart: Cart) { 145 | queueItems.push(cart); 146 | setTimeout(runNext, 0); 147 | }; 148 | } 149 | 150 | const updateTotalQueue = Queue(); 151 | ``` 152 | 153 | 참고 키워드: 클로저, 내부 변수 154 | 155 | ## 원칙: 공유하는 방법을 현실에서 착안하기 156 | 157 | 인간은 자원을 공유를 자연스럽게 할 수 있지만 컴퓨터는 세세하게 지정해주어야 합니다. 158 | 159 | - 한 번에 한 명씩 쓸 수 있게 화장실 문을 잠글 수 있습니다. (줄서기, 락, 선점) 160 | - 공공 도서관은 지역사회가 많은 책을 공유할 수 있는 곳입니다. (시간 대여 시스템) 161 | - 칠판을 사용하면 선생님 한 명이 교실 전체에 정보를 공유할 수 있습니다. (publish, subscribe) 162 | 163 | ## 큐를 재사용할 수 있도록 만들기 164 | 165 | ### done 함수 빼내기 166 | 167 | ```typescript 168 | function Queue() { 169 | // ... 170 | function worker(cart, done) { 171 | calculateCartTotal(cart, function (total: number) { 172 | updateTotalDOM(total); 173 | done(total); 174 | }); 175 | } 176 | 177 | function runNext() { 178 | // ... 179 | worker(cart, function () { 180 | working = false; 181 | runNext(); 182 | }); 183 | } 184 | } 185 | ``` 186 | 187 | ### 워커 행동을 바꿀 수 있도록 밖으로 뺍니다. 188 | 189 | ```typescript 190 | function Queue(worker: (cart: Cart, done: VoidFunction) => void) { 191 | // ... 192 | function runNext() { 193 | // ... 194 | worker(cart, function () { 195 | working = false; 196 | runNext(); 197 | }); 198 | } 199 | } 200 | 201 | const updateTotalQueue = Queue(function (cart, done) { 202 | calculateCartTotal(cart, function (total: number) { 203 | updateTotalDOM(total); 204 | done(total); 205 | }); 206 | }); 207 | ``` 208 | 209 | ### 작업이 끝났을 때 실행하는 콜백을 받기 210 | 211 | ```typescript 212 | function Queue(worker: (cart: Cart, done: VoidFunction) => void) { 213 | // ... 214 | function runNext() { 215 | // ... 216 | const item = queueItems.shift(); 217 | worker(item.data, function () { 218 | working = false; 219 | runNext(); 220 | }); 221 | } 222 | 223 | return function (data: Cart, callback: (value: number) => void) { 224 | queueItems.push({ data, callback }); 225 | setTimeout(runNext, 0); 226 | }; 227 | } 228 | ``` 229 | 230 | ### 작업이 완료되었을 때 콜백 부르기 231 | 232 | ```typescript 233 | function Queue(worker: (cart: Cart, done: (value: number) => void) => void) { 234 | // ... 235 | function runNext() { 236 | // ... 237 | const item = queueItems.shift(); 238 | worker(item.data, function (value) { 239 | working = false; 240 | setTimeout(item.callback, 0, value); 241 | runNext(); 242 | }); 243 | } 244 | } 245 | ``` 246 | 247 | ### Queue() 는 액션에 새로운 능력을 줄 수 있는 고차 함수입니다. 248 | 249 | ## 지금까지 만든 타임라인 분석하기 250 | 251 | 장바구니 업데이트 버튼 두번 클릭시 252 | 253 | 1. 동시: 불가능 254 | 2. 왼쪽 먼저: 기대한 순서이므로 정상 작동합니다. 255 | 3. 오른쪽 먼저: 큐에 들어가서 작업되므로 실행 순서를 보장합니다. 256 | 257 | ## 원칙: 문제가 있을 것 같으면 타임라인 다이어그램을 살펴보세요 258 | 259 | 타임라인 다이어그램의 가장 큰 장점은 타이밍 문제를 명확히 보여준다는 것입니다. 260 | 타이밍에 관한 버그는 재현하기 매우 힘들기 때문에 타임라인 다이어그램이 필요합니다. 261 | 타임라인 다이어그램은 서비스에 배포해 보지 않아도 문제를 찾을 수 있습니다. 262 | 263 | ## 큐를 건너뛰도록 만들기 264 | 265 | 드로핑 큐를 이용해서 만들 수 있습니다. 266 | 267 | ## 결론 268 | 269 | 이 장에서 자원 공유 문제에 대해 살펴보았습니다. 270 | 다이어그램을 통해 문제를 찾은 다음 큐로 해결합시다. 271 | 272 | ## 요점 정리 273 | 274 | - 타이밍 문제는 재현하기 어렵고, 테스트로 확인하지 못 합니다. 275 | - 타임라인 다이어그램을 그려 분석하고 타이밍 문제를 확인해 보세요. 276 | - 자원 공유 문제가 있을 때 현실에서 해결 방법을 찾아보세요. 277 | - 재사용 가능한 도구를 만들면 자원 공유에 도움이 됩니다. 278 | - 자원 공유를 위한 도구를 동시성 기본형이라고 부릅니다. 279 | - 동시성 기본형은 액션을 고차 함수로 받습니다. 280 | -------------------------------------------------------------------------------- /docs/15-chapter_17.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 17장 타임라인 조율하기 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 타임라인을 조율하기 위한 동시성 기본형을 만들어 봅니다. 10 | - 시간에 관한 중요한 관점인 순서와 반복을 함수형 개발자들이 어떻게 다루는지 배웁니다. 11 | 12 | ## 좋은 타임라인의 원칙 13 | 14 | 1. 타임라인은 적을수록 이해하기 쉽습니다. 15 | 2. 타임라인은 짧을수록 이해하기 쉽습니다. 16 | 3. 공유하는 자원이 적을수록 이해하기 쉽습니다. 17 | 4. 자원을 공유한다면 서로 조율해야 합니다. 18 | 5. 시간을 일급으로 다룹니다. 19 | 20 | 이번 장에서는 다섯번째 원칙을 보겠습니다. 21 | 이제부터 시간을 다룰 수 있는 대상으로 생각해야 합니다. 22 | 23 | ## 버그가 있습니다! 24 | 25 | 장바구니에 큐를 적용해서 배포한 후 UI 반응 속도를 개선해달라는 요청이 많이 있었습니다. 26 | 그래서 장바구니에 대한 속도 개선과 제품 추가 버튼에 대한 최적화를 했습니다. 27 | 그러자 다음과 같은 버그가 발생하였습니다. 28 | 29 | 장바구니 추가 버튼 클릭 -> 의도한 결과는 상품 금액 + 배송비 금액 -> 하지만 배송비 금액만 추가가 됨 30 | 31 | ## 코드가 어떻게 바뀌었나요 32 | 33 | ```typescript 34 | function calculateCartTotal(cart: Cart, callback: (total: number) => void) { 35 | let total = 0; 36 | costAjax(cart, (cost) => { 37 | total += cost; 38 | }); // <- 2. 여기로 옮겼습니다. 39 | shippingAjax(cart, (shipping) => { 40 | total += shipping; 41 | callback(total); 42 | }); 43 | // }); <- 1. 원래 여기에 있던 괄호를 44 | } 45 | ``` 46 | 47 | 즉, 상품 금액 계산 후 배송비를 계산하도록 한 로직을 상품 금액 계산과 배송비 계산 로직이 동시에 수행됩니다. 48 | 비동기적으로 작동하면 속도는 빨라지지만 타이밍을 맞춰지지 않게 되었습니다. 49 | 50 | ## 액션을 확인하기: 단계 1 51 | 52 | ```typescript 53 | function addItemToCart(item: Item) { 54 | cart-2- = addItem(cart-1-, item); 55 | updateTotalQueue-4-(cart-3-); 56 | } 57 | 58 | function calculateCartTotal(cart: Cart, callback: (total: number) => void) { 59 | let total-5- = 0; 60 | costAjax-6-(cart, (cost) => { 61 | total-7- += cost; 62 | }); 63 | shippingAjax-8-(cart, (shipping) => { 64 | total-9- += shipping; 65 | callback(total-10-); 66 | }); 67 | } 68 | 69 | function calculateCartWorker(cart: Cart, done: (total: number) => void) { 70 | calculateCartTotal(cart, (total) => { 71 | updateTotalDOM-11-(total); 72 | done(total); 73 | }); 74 | } 75 | ``` 76 | 77 | 코드에 있는 액션에 표시를 합니다. (여기서 숫자는 순서가 아닌 일련번호입니다.) 78 | 79 | ## 모든 액션을 그리기: 단계 2 80 | 81 | | 클릭핸들러 | 큐 | costAjax | shippingAjax | 82 | | ------------------------ | -------------------- | ------------- | ----------------------- | 83 | | 1: cart 읽기 | | | | 84 | | 2: cart 쓰기 | | | | 85 | | 3: cart 읽기 | | | | 86 | | 4: updateTotalQueue 호출 | | | | 87 | | | 5: total 초기화 | | | 88 | | | 6: costAjax 호출 | | | 89 | | | 8: shippingAjax 호출 | 7: total 읽기 | | 90 | | | | 7: total 쓰기 | | 91 | | | | | 9: total 읽기 | 92 | | | | | 9: total 쓰기 | 93 | | | | | 10: total 읽기 | 94 | | | | | 11: updateTotalDOM 호출 | 95 | 96 | 코드에서 확인한 액션 모두 다이어그램에 그렸습니다. 97 | 자바스크립트에서 다이어그램을 단순화하기 위한 두단계를 적용해 봅시다. 98 | 99 | ## 다이어그램 단순화하기: 단계 3 100 | 101 | ### 자바스크립트에서 단순화하기 위한 두 단계 102 | 103 | 1. 액션을 통합합니다. 104 | 2. 타임라인을 통합합니다. 105 | 106 | ### 액션 통합 107 | 108 | | 클릭핸들러 | 큐 | costAjax | shippingAjax | 109 | | ---------- | ------- | -------- | ------------ | 110 | | 1, 2, 3, 4 | | | | 111 | | | 5, 6, 8 | | | 112 | | | | 7 | | 113 | | | | | 9, 10, 11 | 114 | 115 | ### 타임라인 통합 116 | 117 | | 클릭핸들러, 큐 | costAjax | shippingAjax | 118 | | -------------- | -------- | ------------ | 119 | | 1, 2, 3, 4 | | | 120 | | 5, 6, 8 | | | 121 | | | 7 | | 122 | | | | 9, 10, 11 | 123 | 124 | ## 실행 가능한 순서 분석하기 125 | 126 | ### costAjax 와 shippingAjax 의 가능한 실행 순서 127 | 128 | 1. 동시: 자바스크립트 스레드 모델로는 불가능합니다. 129 | 2. costAjax 먼저: 버그가 아닌 기대하는 동작입니다. 130 | 3. shippingAjax 먼저: 기대하지 않는 동작이므로 조치가 필요합니다. 131 | 132 | ## 왜 지금 타임라인이 더 빠를까요? 133 | 134 | 병렬로 진행하기 때문입니다. 135 | 136 | ## 모든 병렬 콜백 기다리기 137 | 138 | 병렬을 기다는 선을 컷이라고 부릅니다. 139 | 이 컷이라는 부분을 구현하면 됩니다. 140 | 141 | ## 타임라인을 나누기 위한 동시성 기본형 142 | 143 | 경쟁 조건을 막으면 됩니다. 144 | 경쟁 조건은 어떤 동작이 먼저 끝나는 타임라인에 의존할 때 발생합니다. 145 | 146 | ## 코드에 Cut() 적용하기 147 | 148 | 두가지만 고민하면 됩니다. 149 | 150 | 1. Cut() 을 보관할 범위 151 | 2. Cut() 에 어떤 콜백을 넣을지 152 | 153 | ```typescript 154 | function calculateCartTotal(cart: Cart, callback: (total: number) => void) { 155 | let total = 0; 156 | const done = Cut(2, () => callback(total)); 157 | costAjax(cart, (cost) => { 158 | total += cost; 159 | done(); 160 | }); 161 | shippingAjax(cart, (shipping) => { 162 | total += shipping; 163 | done(); 164 | }); 165 | } 166 | ``` 167 | 168 | Cut 안의 숫자 2는 두번의 호출을 기다리겠다라는 의미입니다. 169 | 170 | ## 불확실한 순서 분석하기 171 | 172 | 1. 동시: 자바스크립트 스레드 모델로는 불가능합니다. 173 | 2. costAjax 먼저: done 두번을 기다리니 기대한 결과가 나타납니다. 174 | 3. shippingAjax 먼저: done 두번을 기다리니 기대한 결과가 나타납니다. 175 | 176 | ## 병렬 실행 분석 177 | 178 | | 클릭핸들러, 큐 | costAjax | shippingAjax | 179 | | -------------- | -------- | ------------ | 180 | | 1, 2, 3, 4 | | | 181 | | 5, 6, 8 | | | 182 | | | 7 | 9 | 183 | | | 10, 11 | | 184 | 185 | ## 여러 번 클릭하는 경우 분석 186 | 187 | 클릭1 다이어그램, 클릭2 다이어그램, 큐 다이어그램만 존재하므로 정상적으로 작동합니다. 188 | 189 | ## 딱 한 번만 호출하는 기본형 190 | 191 | ```typescript 192 | function JustOnce(action: VoidFunction) { 193 | let alreadyCalled = false; 194 | return function (...args: any[]) { 195 | if (alreadyCalled) return; 196 | alreadyCalled = true; 197 | return action(...args); 198 | }; 199 | } 200 | 201 | function sendAddToCartText(phone: string) { 202 | sendTextAjax(phone, 'Thank you'); 203 | } 204 | 205 | const sendAddToCartTextOnce = JustOnce(sendAddToCartText); 206 | 207 | sendAddToCartTextOnce('010-1234-5678'); 208 | sendAddToCartTextOnce('010-1234-5678'); 209 | sendAddToCartTextOnce('010-1234-5678'); 210 | sendAddToCartTextOnce('010-1234-5678'); 211 | ``` 212 | 213 | 몇번을 호출해도 한번만 실행합니다. 214 | 215 | ## 암묵적 시간 모델 vs 명시적 시간 모델 216 | 217 | 자바스크립트 시간 모델은 간단합니다. 218 | 219 | 1. 순차적 구문은 순서대로 실행됩니다. 220 | 2. 두 타임라인에 있는 단계는 왼쪽 먼저 실행되거나, 오른쪽 먼저 실행될 수 있습니다. 221 | 3. 비동기 이벤트는 새로운 타임라인에서 실행됩니다. 222 | 4. 액션은 호출할 때마다 실행됩니다. 223 | 224 | ## 요약: 타임라인 사용하기 225 | 226 | - 타임라인 수를 줄입니다. 227 | - 타임라인 길이를 줄입니다. 228 | - 공유 자원을 없앱니다. 229 | - 동시성 기본형으로 자원을 공유합니다. 230 | - 동시성 기본형으로 조율합니다. 231 | 232 | ## 결론 233 | 234 | 이 장에서 웹 요청의 시간 차이 때문에 발생하는 경쟁 조건에 대해 알아보았습니다. 235 | 요청한 순서대로 응답이 도착하는 것이 보장되지 않으니 236 | Promise 나 Cut 을 사용합시다. 237 | 238 | ## 요점 정리 239 | 240 | - 함수형 개발자는 언어가 제공하는 암묵적 시간 모델 대신 새로운 시간 모델을 만들어 사용합니다. 241 | - 명시적 시간 모델은 종종 일급 값으로 만듭니다. 242 | - 타임라인을 조율하기 위해 동시성 기본형을 만들 수 있습니다. 243 | - 타임라인을 나누는 것도 타임라인을 조율하는 방법 중 하나입니다. 244 | -------------------------------------------------------------------------------- /docs/16-chapter_18.mdx: -------------------------------------------------------------------------------- 1 | import Authors from './Authors.tsx'; 2 | 3 | # 18장 반응형 아키텍처와 어니언 아키텍처 4 | 5 | 6 | 7 | ## 이번 장에서 살펴볼 내용 8 | 9 | - 반응형 아키텍처로 순차적 액션을 파이프라인으로 만드는 방법을 배웁니다. 10 | - 상태 변경을 다루기 위한 기본형을 만듭니다. 11 | - 도메인과 현실 세계의 상호작용을 위해 어니언 아키텍처를 만듭니다. 12 | - 여러 계층에 어니언 아키텍처를 적용하는 방법을 살펴봅니다. 13 | - 전통적인 계층형 아키텍처와 어니언 아키텍처를 비교해 봅니다. 14 | 15 | ## 두 아키텍처 패턴은 독립적입니다. 16 | 17 | ![Two separate architectural patterns](https://drek4537l1klr.cloudfront.net/normand/Figures/f0510-01.jpg) 18 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 19 | 20 | 반응형 아키텍처는 순차적 액션 단계에 사용하고, 어니언 아키텍처는 서비스의 모든 단계에 사용합니다. 21 | 22 | ### 반응형 아키텍처 23 | 24 | - 반응형 아키텍처는 순차적 액션의 순서를 뒤집습니다. 25 | - 효과와 그 효과의 대한 원인을 분리. 26 | 27 | ### 어니언 아키텍처 28 | 29 | - 현실 세계와 상호작용하기 위한 서비스 구조를 만듭니다. 30 | 31 | ## 반응형 아키텍처 32 | 33 | ### 반응형 아키텍처는 무엇일까? 34 | 35 | > 반응형 아키텍처는 애플리케이션을 구조화하는 방법입니다. 36 | 37 | 반응형 아키텍처는 변화에 대한 시스템의 대응성과 확장성을 강조하는데요, 반응형 시스템은 시스템의 상태가 변경되면, 그 상태를 반영하는 효과를 발생시킵니다. 38 | 이 효과는 원인이 되는 상태 변경을 발생시키는 액션을 통해 발생합니다. 반응형 아키텍처는 이러한 효과와 원인을 분리합니다. 39 | 40 | 책에서는 이벤트 핸들러의 예제를 들었는데요, 41 | 반응형 아키텍처를 기반으로 하는 이벤트 핸들러는 이벤트 스트림을 구독하여, 이벤트가 발생할 때마다 반응하는 코드를 실행합니다. 42 | 43 | 이벤트를 처리하는 방식에는 여러 가지가 있지만, 대표적으로 다음과 같은 방식들이 있습니다. 44 | 45 | - 이벤트를 처리하는 함수 (event handler function) : 이벤트가 발생할 때마다 실행되는 함수를 정의하여, 이벤트 처리를 구현합니다. 46 | - 옵저버 패턴 (Observer pattern) : 이벤트 스트림을 구독하는 객체들이 이벤트가 발생할 때마다 실행되는 코드를 구현합니다. 47 | 48 | 따라서 반응형 시스템은 이벤트의 스트림을 구독하며, 이를 통해 시스템은 실시간으로 변화에 대응할 수 있습니다. 49 | 50 | ### 반응형 아키텍처의 절충점 51 | 52 | 반응형 아키텍처는 코드에 나타난 순차적 액션의 순서를 뒤집습니다. 53 | 전통적인 아키텍처와 비교했을 때, 54 | 55 | - 원인과 효과가 결합한 것을 분리합니다. 56 | - 반응형 아키텍처는 코드에 나타난 순차적 액션의 순서를 뒤집습니다. 57 | - 일반적인 아키텍처에서는 원인에 대한 효과가 결합되어 있는 것을 분리하면서 이벤트 스트림을 구독하여 원인과 효과를 분리합니다. 58 | - 여러 단계를 파이프라인으로 처리합니다. 59 | - 여러 단계를 파이프라인으로 처리하면서, 이벤트를 처리하는 과정을 더욱 유연하게 구성할 수 있습니다. 60 | - 타임라인이 유연해집니다. 61 | - 이벤트 스트림을 구독하면서 처리하기 때문에 실시간으로 이벤트를 처리하며, 이벤트가 발생할 때마다 처리할 수 있기 때문입니다. 62 | 63 | 이제 강력한 일급 상태 모델을 만들어 보자. 상태 모델을 만들고 생성한 상태 모델을 통해 위에서 설명한 반응형 아키텍처를 구현해 보겠습니다. 64 | 65 | ## 셀은 일급 상태입니다 66 | 67 | ```ts 68 | function ValueCell(initialValue: number) { 69 | let currentValue = initialValue; // 초기값으로 전달받은 값으로 초기화. 70 | return { 71 | val: () => currentValue, // 현재값 리턴 72 | update: (f) => { 73 | // 교체 패턴 74 | let oldValue = currentValue; 75 | let newValue = f(oldValue); 76 | currentValue = newValue; 77 | }, 78 | }; 79 | } 80 | ``` 81 | 82 | 위의 코드를 기반으로 아래와 같이 장바구니 구현 코드를 리팩터링할 수 있습니다. 83 | 84 | ```ts 85 | const shopping_cart = ValueCell({}); 86 | function add_item_to_cart(name: string, price: number) { 87 | const item = make_cart_item(name, price); 88 | // shopping_cart = add_item(shopping_cart, item); 89 | // 위 코드를 다음과 같이 리팩터링 90 | shopping_cart.update((cart) => add_item(cart, item)); 91 | const total = calc_total(shopping_cart.val()); 92 | set_cart_total_dom(total); 93 | update_shipping_icons(shopping_cart.val()); 94 | update_tax_dom(total); 95 | } 96 | ``` 97 | 98 | ## valueCell을 반응형으로 만들어 보자 99 | 100 | ```ts 101 | // 이전 코드 102 | function ValueCell(initialValue: string) { 103 | var currentValue = initialValue; 104 | return { 105 | val: () => currentValue, 106 | update: function (f) { 107 | const oldValue = currentValue; 108 | const newValue = f(oldValue); 109 | currentValue = newValue; 110 | }, 111 | }; 112 | } 113 | 114 | function ValueCell(initialValue: string) { 115 | let currentValue = initialValue; 116 | var watchers = []; // 감시자 목록 추가 117 | return { 118 | val: () => currentValue, 119 | update: function (f) { 120 | const oldValue = currentValue; 121 | const newValue = f(oldValue); 122 | if (oldValue !== newValue) { 123 | currentValue = newValue; 124 | forEach(watchers, (watcher) => watcher(newValue)); // 감시자 실행 125 | } 126 | }, 127 | // 새로운 감시자 추가 128 | addWatcher: function (f) { 129 | watchers.push(f); 130 | }, 131 | }; 132 | } 133 | ``` 134 | 135 | - 감시자란 currentValue의 값이 변경될 때마다 수행할 작업을 정의할 함수. 136 | - update 함수는 새로운 값이 이전 값과 다른 경우에만 currentValue를 변경하고 감시자를 실행합니다. 137 | - addWatcher 함수: 인자로 전달받은 함수를 watchers 배열에 추가하여 관리. 138 | => ValueCell 객체에 감시자를 추가할 수 있는 기능과, 현재 값이 바뀔때 감시자를 실행시켜 어떤 작업을 수행하도록 하는 기능이 추가. 139 | 140 | ## 셀이 바뀔 때 배송 아이콘을 갱신하자 141 | 142 | ```ts 143 | const shopping_cart = ValueCell({}); 144 | function add_item_to_cart(name: string, price: number) { 145 | const item = make_cart_item(name, price); 146 | shopping_cart.update(function (cart) { 147 | return add_item(cart, item); 148 | }); 149 | const total = calc_total(shopping_cart.val()); 150 | set_cart_total_dom(total); 151 | // 이전 코드 152 | // update_shipping_icons(shopping_cart.val()); 153 | update_tax_dom(total); 154 | } 155 | // 장바구니가 변경될 때 항상 update_shipping_icons가 실행 156 | shopping_cart.addWatcher(update_shipping_icons); 157 | ``` 158 | 159 | 따라서 add_item_to_cart 핸들러에서 DOM 갱신하는 부분을 하나 삭제했습니다. 160 | 이제 남은 DOM 갱신 코드는 2개입니다. 161 | 162 | ## formulaCell은 파생된 값을 계산합니다. 163 | 164 | ValueCell에 감시자 기능을 추가해서 반응형으로 만든 것처럼, FormulaCell로 이미 존재하는 셀에서 파생한 셀을 생성할 수 있습니다. 165 | 다른 셀의 변화가 감지되면 값을 다시 계산합니다. 166 | 167 | ```ts 168 | const FormulaCell = (upstreamCell: UpstreamCell, f: VoidFunction) => { 169 | const myCell = ValueCell(f(upstreamCell.val())); 170 | upstreamCell.addWatcher((newUpstreamValue) => 171 | myCell.update((currentValue) => f(newUpstreamValue)) 172 | ); 173 | return { 174 | val: myCell.val, 175 | addWatcher: myCell.addWatcher, 176 | }; 177 | }; 178 | ``` 179 | 180 | 이 FormulaCell로 코드를 리팩터링 해봅시다. 181 | 182 | ```ts 183 | // ValueCell 객체 생성. 장바구니에 담긴 상품 목록을 저장 184 | const shopping_cart = ValueCell({}); 185 | // formulaCell 생성. 장바구니에 담긴 상품의 총 금액을 계산. shopping_cart가 바뀔 때 cart_total도 변경된다. 186 | const cart_total = FormulaCell(shopping_cart, calc_total); 187 | function add_item_to_cart(name: string, price: number) { 188 | const item = make_cart_item(name, price); 189 | shopping_cart.update(function (cart) { 190 | return add_item(cart, item); 191 | }); 192 | // 이전 코드에서 삭제 193 | // const total = calc_total(shopping_cart.val()); 194 | // set_cart_total_dom(total); 195 | // update_tax_dom(total); 196 | } 197 | shopping_cart.addWatcher(update_shipping_icons); 198 | // 새롭게 추가. cart_total이 바뀌면 DOM이 업데이트. 199 | cart_total.addWatcher(set_cart_total_dom); 200 | cart_total.addWatcher(update_tax_dom); 201 | ``` 202 | 203 | ## 함수형 프로그래밍과 변경 가능한 상태 204 | 205 | 함수형 프로그래밍은 변경 가능한 상태를 피하고, 변경 불가능한 상태를 사용한다고 흔히 이야기 합니다. 206 | 변경 가능한 상태를 남용하지 않고, 변경 가능한 상태를 잘 관리한다면 함수형 프로그래밍의 이점을 누리면서 개발할 수 있습니다. 207 | 208 | 위의 예제에서 ValueCell 객체의 update() 메서드를 사용하면 현재 값을 올바르게 유지할 수 있는데, 209 | 이는 이 메서드가 호출될 때마다 '계산'을 넘기기 때문입니다. 210 | 즉, (계산이 올바르다는 전제 하에) 계산은 현재 값을 받아서 새로운 값을 생성하기 때문에 항상 올바른 값을 유지할 수 있습니다. 211 | 212 | ## 반응형 아키텍처가 시스템을 어떻게 바꿨나요 213 | 214 | ![How reactive architecture reconfigures systems](https://drek4537l1klr.cloudfront.net/normand/Figures/f0519-01.jpg) 215 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 216 | 217 | 1. 원인과 효과가 결합된 것을 분리 218 | 2. 여러 단계를 파이프라인으로 처리 219 | 3. 타임라인이 유연 220 | 221 | ### 1) 원인과 효과가 결합한 것을 분리합니다 222 | 223 | ![How reactive architecture reconfigures systems](https://drek4537l1klr.cloudfront.net/normand/Figures/f0520-01.jpg) 224 | ![How reactive architecture reconfigures systems](https://drek4537l1klr.cloudfront.net/normand/Figures/f0520-02.jpg) 225 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 226 | 227 | 3가지 원인에 의해 배송 아이콘을 갱신하던 로직을 '전역 장바구니 변경'이라는 원인 하나에 기인하도록 수정했습니다. 228 | 229 | > 다만, 코드에 액션을 순서대로 표현하는 것이 더 명확할 수 있습니다. 장바구니처럼 원인과 효과의 중심이 없다면 분리하지 맙시다. 230 | 231 | ### 2) 여러 단계를 파이프라인으로 처리합니다. 232 | 233 | 반응형 아키텍처에서는 각 단계를 독립적으로 처리합니다. 각 단계는 입력을 처리하고 출력을 생성하며, 이러한 출력은 다음 단계에 전달됩니다. 234 | 235 | 즉 간단한 액션과 계산을 조합해서 복잡한 계산을 만들 수 있습니다. 236 | 237 | > 만약 여러 단계가 있지만 데이터를 전달하지 않는다면 이 패턴을 사용하지 마세요. 데이터를 전달하지 않으면 파이프라인이라고 볼 수 없습니다. 238 | 239 | ### 3) 타임라인이 유연해집니다. 240 | 241 | ![Flexibility in your timeline](https://drek4537l1klr.cloudfront.net/normand/Figures/f0523-01.jpg) 242 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 243 | 244 | 반응형 아키텍처는 이벤트 기반의 아키텍처로, 상태 변화를 이벤트로 감지하고 감지한 이벤트에 대해 응답하는 구조를 가지고 있습니다. 245 | 그래서 상태 변화가 발생할 때마다 바로바로 응답할 수 있다는 의미에서 타임라인이 유연해집니다. 246 | 247 | 15장에서 짧은 타임라인이 좋은 것이라고 했지만 많은 것도 좋지 않습니다. 공유하는 자원이 많지 않다면 타임라인이 많아져도 상관 없습니다. 248 | 따라서 위의 타임라인은 서로 다른 자원을 사용하기 때문에 안전합니다. 249 | 250 | ## 어니언 아키텍처 251 | 252 | 어니언 아키텍처는 반응형 아키텍처보다 더 넓은 범위에 사용합니다. 253 | 어니언 아키텍처는 서비스 전체를 구성하는 데 사용하는데, 반응형 아키텍처와 함께 사용한다면 반응형이 어니언 아키텍처 안에 들어가는 걸 볼 수 있습니다. 254 | 255 | ### 어니언 아키텍처란? 256 | 257 | ![What is the onion architecture?](https://drek4537l1klr.cloudfront.net/normand/Figures/f0527-01.jpg) 258 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 259 | 260 | 어니언 아키텍처는 현실 세계와 상호 작용하기 위한 서비스 구조를 만드는 방법인데요, 특정 계층이 꼭 필요한 것은 아니지만 대부분의 경우 위와 같은 구조를 가지고 있습니다. 261 | 262 | > - 인터랙션: 어니언 아키텍처는 인터랙션 스트림을 기반으로 하여 사용자 인터랙션을 처리합니다. 이벤트를 스트림으로 관리하고 이벤트를 기반으로 데이터를 처리합니다. 263 | > - 도메인: 어니언 아키텍처는 도메인 기반의 애플리케이션을 구성합니다. 즉, 도메인 로직과 애플리케이션 로직을 분리하여 각각을 함수로 구성하여 애플리케이션을 쉽게 테스트 할 수 있도록 합니다. 264 | > - 언어: 어니언 아키텍처는 함수형 프로그래밍 언어를 사용하여 애플리케이션을 구성합니다. 이러한 함수형 언어는 순수 함수를 사용하여 부작용을 최소화하고, 값을 변경하지 않습니다. 265 | 266 | 1. 현실 세계와 상호작용은 인터랙션 계층에서 합니다. 267 | 2. 계층에서 호출하는 방향은 중심 방향입니다. 268 | 3. 계층은 외부에 어떤 계층이 있는지 모릅니다. 269 | 270 | ### 전통적인 계층형 아키텍처 vs 함수형 아키텍처 271 | 272 | - 전통적인 계층형 아키텍처는 애플리케이션을 계층적으로 구성하는 아키텍처 스타일입니다. 273 | 각 계층은 특정 기능을 수행하며, 이러한 계층들은 서로 의존적입니다. 각 계층은 하위 계층으로부터 서비스를 제공받습니다. 274 | - 함수형 아키텍처는 일반적으로 순수함수, 계산로직을 기반으로 하고, 부작용을 최소화하는 방법으로 애플리케이션을 구성합니다. 275 | 이러한 함수들은 상태를 공유하지 않고 입력 값으로만 작동하며, 애플리케이션을 재사용하고 테스트하기 쉽도록 합니다. 276 | 277 | ![A functional architecture](https://drek4537l1klr.cloudfront.net/normand/Figures/f0531-01.jpg) 278 | 출처: https://livebook.manning.com/book/grokking-simplicity/chapter-18/9 279 | 280 | - 데이터베이스는 변경 가능하고 접근하는 모든 것을 액션으로 만든다는 것이 핵심 281 | - 함수형 개발자는 액션에서 계산을 빼내어야 하는데, 액션과 계산을 명확하게 구분하려고 하고 도메인 로직은 모두 계산으로 만들어야 한다. 282 | (for 가독성, 유지보수성, 재사용성, 테스트...) 283 | 284 | ### 변경과 재사용이 쉬워야 한다 285 | 286 | - 어니언 아키텍처는 인터랙션 계층을 바꾸기 쉽습니다. 그래서 도메인 계층을 재사용하기 좋습니다. 287 | 288 | > 어니언 아키텍처는 도메인 로직과 인터랙션 계층을 분리하면서, 그 사이에서 인터페이스를 정의하여 의존성을 최소화 하는데 초점을 두고 있습니다. 289 | > 290 | > 인터랙션 계층을 바꾸기 쉽다는 것은 어플리케이션이 서로 다른 환경에서 작동할 수 있다는 뜻입니다. 291 | > 즉, 인터랙션 계층만 바꾸면 어플리케이션을 웹, 모바일, 서버less 등 다양한 환경에서 실행 가능하게 만들 수 있습니다. 292 | > 다양한 인터렉션 계층에서 분리된 도메인 계층을 재사용할 수 있게 되는 것입니다. 293 | 294 | - 전형적인 아키텍처에서 도메인 규칙은 데이터베이스를 부릅니다. 하지만 어니언 아키텍처에서는 그렇게 하면 안됩니다. 295 | > 어니언 아키텍처는 도메인 로직과 인터랙션 계층, 그리고 언어 계층을 분리하는데 초점을 두고 있습니다. 296 | > 전통적인 아키텍처는 데이터를 저장하고, 저장된 데이터를 가져와서 계산을 수행하는 구조로 데이터베이스에서 장바구니 합계를 가져와 도메인에서 처리하지만, 297 | > 298 | > 어니언 아키텍처에서는 인터랙션 계층에서 값을 가져오고 도메인 계층에서 합산을 합니다. 299 | 300 | ### 도메인 규칙은 도메인 용어를 사용합니다. 301 | 302 | 프로그램의 핵심 로직을 '도메인 규칙' 혹은 '비즈니스 규칙'이라고 합니다. 303 | 304 | > 도메인 규칙은 도메인 용어를 사용하는데, 도메인 규칙에 속하는지 인터랙션 계층에 속하는지 판단하려면 코드에서 사용하는 용어를 보면 됩니다. 305 | > 306 | > "제품, 이미지, 가격, 할인" 307 | 308 | ### 가독성을 따져 봐야 합니다. 309 | 310 | 특정 패러다임이 항상 좋은 것은 아닙니다. 도메인을 계산으로 만드는 것도 마찬가지인데 다음의 사항을 고려해야 합니다. 311 | 312 | 1. 코드의 가독성: 도메인 계층을 계산으로 만들고 인터랙션 계층과 분리하면서 읽기 좋은 코드를 만들어야 합니다. 313 | 2. 개발 속도: 비즈니스를 고려하는 것 또한 중요합니다. 314 | 3. 시스템 성능: 불변 데이터 구조보다 변경 가능한 데이터 구조가 성능이 좋습니다. 성능 개선과 도메인을 계산으로 만드는 건 분리해서 생각합시다. 315 | 316 | ## 결론 317 | 318 | 반응형 아키텍처와 어니언 아키텍처에 대해 알아보았습니다. 319 | 320 | - 반응형 아키텍처는 순차적 액션의 순서를 뒤집고, 액션과 계산을 조합해 파이프라인으로 만듭니다. 321 | - 어니언 아키텍처는 소프트웨어를 인터랙션, 도메인, 언어 계층 3가지로 나눕니다. 322 | - 가장 바깥인 인터랙션 계층은 액션으로 되어 있는데, 도메인 계층과 액션을 사용을 조율합니다. 323 | - 도메인 계층은 도메인 로직 같은 소프트웨어 동작으로 되어 있는데, 도메인 계층은 대부분 계산으로 구성됩니다. 324 | - 언어 계층은 소프트웨어를 만들 수 있는 언어 기능과 라이브러리로 되어 있습니다. 325 | - 어니언 아키텍처는 프랙털입니다. 액션의 모든 추상화 수준에서 찾을 수 있습니다. -------------------------------------------------------------------------------- /docs/Authors.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-size: 16px; 3 | font-weight: 600; 4 | } 5 | 6 | .authors { 7 | width: 100%; 8 | display: flex; 9 | flex-direction: row; 10 | flex-wrap: wrap; 11 | margin-top: 5px; 12 | } 13 | 14 | .author { 15 | display: flex; 16 | align-items: center; 17 | width: 50%; 18 | margin: 0 0 0.5rem 0; 19 | } 20 | 21 | @media (max-width: 996px) { 22 | .author { 23 | display: flex; 24 | align-items: center; 25 | width: 100%; 26 | margin: 0 0 0.5rem 0.5rem; 27 | } 28 | } 29 | 30 | .avatar__link { 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | .avatar__photo { 36 | width: var(--ifm-avatar-photo-size-md); 37 | height: var(--ifm-avatar-photo-size-md); 38 | border-radius: 50%; 39 | overflow: hidden; 40 | } 41 | 42 | .avatar__info { 43 | display: flex; 44 | flex-direction: column; 45 | justify-content: center; 46 | margin-left: 10px; 47 | line-height: 140%; 48 | text-align: var(--ifm-avatar-intro-alignment); 49 | font-size: 12px; 50 | } 51 | 52 | .bold { 53 | font-size: 14px; 54 | font-weight: 600; 55 | } 56 | -------------------------------------------------------------------------------- /docs/Authors.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import styles from './Authors.module.css'; 3 | 4 | const AUTHORS = { 5 | positiveko: { 6 | name: 'positiveko', 7 | title: 'Front End Engineer', 8 | url: 'https://github.com/positiveko', 9 | image_url: 'https://github.com/positiveko.png', 10 | }, 11 | saengmotmi: { 12 | name: 'saengmotmi', 13 | title: 'Front End Engineer', 14 | url: 'https://github.com/saengmotmi', 15 | image_url: 'https://github.com/saengmotmi.png', 16 | }, 17 | Jtree03: { 18 | name: 'Jtree03', 19 | title: 'Software Engineer', 20 | url: 'https://github.com/jtree03', 21 | image_url: 'https://github.com/jtree03.png', 22 | }, 23 | yongsk0066: { 24 | name: 'yongsk0066', 25 | title: 'Front End Engineer', 26 | url: 'https://github.com/yongsk0066', 27 | image_url: 'https://github.com/yongsk0066.png', 28 | }, 29 | } as const; 30 | 31 | type TAuthor = keyof typeof AUTHORS; 32 | 33 | interface IAuthors { 34 | bookLeader: TAuthor; 35 | authors?: TAuthor[]; 36 | } 37 | 38 | export default function Authors({ 39 | bookLeader, 40 | authors, 41 | }: IAuthors): JSX.Element { 42 | const Author = ({ author }: { author: TAuthor }): JSX.Element => { 43 | const authorInfo = AUTHORS[author]; 44 | 45 | return ( 46 |
47 | 53 | {author} 58 | 59 |
60 | 61 | {authorInfo.name} {author === bookLeader && ` 🏆`} 62 | 63 | {authorInfo.title} 64 |
65 |
66 | ); 67 | }; 68 | 69 | const renderAuthors = useCallback((): JSX.Element => { 70 | return ( 71 |
72 | {AUTHORS[bookLeader] && } 73 | {authors && 74 | authors.map((author) => { 75 | if (AUTHORS[author]) { 76 | return ; 77 | } 78 | })} 79 |
80 | ); 81 | }, [bookLeader, authors]); 82 | 83 | return ( 84 |
85 | written by 86 | {renderAuthors()} 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /docs/chap6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/chap6.png -------------------------------------------------------------------------------- /docs/images/7-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/7-1.jpeg -------------------------------------------------------------------------------- /docs/images/8-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/8-1.jpeg -------------------------------------------------------------------------------- /docs/images/8-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/8-2.jpeg -------------------------------------------------------------------------------- /docs/images/8-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/8-3.jpeg -------------------------------------------------------------------------------- /docs/images/8-4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/8-4.jpeg -------------------------------------------------------------------------------- /docs/images/8-5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/docs/images/8-5.jpeg -------------------------------------------------------------------------------- /docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // Note: type annotations allow type checking and IDEs autocompletion 2 | 3 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 4 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 5 | 6 | /** @type {import('@docusaurus/types').Config} */ 7 | 8 | const repoName = 'imfp'; 9 | 10 | const config = { 11 | title: '쏙쏙 들어오는 함수형 코딩', 12 | tagline: '심플한 코드로 복잡한 소프트웨어 길들이기', 13 | url: 'https://dev-in-book.github.io', 14 | baseUrl: `/${repoName}/`, 15 | onBrokenLinks: 'throw', 16 | onBrokenMarkdownLinks: 'warn', 17 | favicon: 'img/favicon.ico', 18 | organizationName: 'dev-in-book', 19 | projectName: 'imfp', 20 | presets: [ 21 | [ 22 | '@docusaurus/preset-classic', 23 | /** @type {import('@docusaurus/preset-classic').Options} */ 24 | ({ 25 | docs: { 26 | sidebarPath: require.resolve('./sidebars.js'), 27 | // Please change this to your repo. 28 | editUrl: `https://github.dev/dev-in-book/${repoName}/blob/main`, 29 | }, 30 | blog: { 31 | showReadingTime: true, 32 | // Please change this to your repo. 33 | editUrl: `https://github.dev/dev-in-book/${repoName}`, 34 | }, 35 | theme: { 36 | customCss: require.resolve('./src/css/custom.css'), 37 | }, 38 | }), 39 | ], 40 | ], 41 | 42 | themeConfig: 43 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 44 | ({ 45 | navbar: { 46 | title: '함수형 코딩', 47 | logo: { 48 | alt: 'dev-in-book', 49 | src: '/img/imfp.jpeg', 50 | }, 51 | items: [ 52 | { 53 | type: 'doc', 54 | docId: 'chapter_3', 55 | position: 'left', 56 | label: '📕 시작하기', 57 | }, 58 | { to: '/blog', label: '👨‍👩‍👧‍👦 참여자', position: 'left' }, 59 | { 60 | href: `https://github.dev/dev-in-book/${repoName}`, 61 | label: '⛳️ GitHub', 62 | position: 'right', 63 | }, 64 | ], 65 | }, 66 | footer: { 67 | style: 'dark', 68 | links: [ 69 | { 70 | title: 'Docs', 71 | items: [ 72 | { 73 | label: '함수형 코딩', 74 | to: '/docs/chapter_3', 75 | }, 76 | { 77 | label: 'Member', 78 | href: '/blog/member', 79 | }, 80 | { 81 | label: 'GitHub', 82 | href: `https://github.dev/dev-in-book/${repoName}`, 83 | }, 84 | ], 85 | }, 86 | ], 87 | copyright: `${new Date().getFullYear()} Dev in Book, Built with Docusaurus.`, 88 | }, 89 | prism: { 90 | theme: lightCodeTheme, 91 | darkTheme: darkCodeTheme, 92 | }, 93 | }), 94 | }; 95 | 96 | module.exports = config; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devinbook-imfp", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.0.0-beta.9", 19 | "@docusaurus/preset-classic": "2.0.0-beta.9", 20 | "@mdx-js/react": "^1.6.21", 21 | "@svgr/webpack": "^5.5.0", 22 | "clsx": "^1.1.1", 23 | "file-loader": "^6.2.0", 24 | "prism-react-renderer": "^1.2.1", 25 | "react": "^17.0.1", 26 | "react-dom": "^17.0.1", 27 | "url-loader": "^4.1.1" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.5%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@docusaurus/module-type-aliases": "^2.0.0-beta.9", 43 | "@tsconfig/docusaurus": "^1.0.4", 44 | "@types/react": "^17.0.37", 45 | "@types/react-helmet": "^6.1.4", 46 | "@types/react-router-dom": "^5.3.2", 47 | "typescript": "^4.5.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: 'Easy to Use', 8 | Svg: require('../../static/img/undraw_docusaurus_mountain.svg').default, 9 | description: ( 10 | <> 11 | Docusaurus was designed from the ground up to be easily installed and 12 | used to get your website up and running quickly. 13 | 14 | ), 15 | }, 16 | { 17 | title: 'Focus on What Matters', 18 | Svg: require('../../static/img/undraw_docusaurus_tree.svg').default, 19 | description: ( 20 | <> 21 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 22 | ahead and move your docs into the docs directory. 23 | 24 | ), 25 | }, 26 | { 27 | title: 'Powered by React', 28 | Svg: require('../../static/img/undraw_docusaurus_react.svg').default, 29 | description: ( 30 | <> 31 | Extend or customize your website layout by reusing React. Docusaurus can 32 | be extended while reusing the same header and footer. 33 | 34 | ), 35 | }, 36 | ]; 37 | 38 | function Feature({Svg, title, description}) { 39 | return ( 40 |
41 |
42 | 43 |
44 |
45 |

{title}

46 |

{description}

47 |
48 |
49 | ); 50 | } 51 | 52 | export default function HomepageFeatures() { 53 | return ( 54 |
55 |
56 |
57 | {FeatureList.map((props, idx) => ( 58 | 59 | ))} 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #25c2a0; 10 | --ifm-color-primary-dark: rgb(33, 175, 144); 11 | --ifm-color-primary-darker: rgb(31, 165, 136); 12 | --ifm-color-primary-darkest: rgb(26, 136, 112); 13 | --ifm-color-primary-light: rgb(70, 203, 174); 14 | --ifm-color-primary-lighter: rgb(102, 212, 189); 15 | --ifm-color-primary-lightest: rgb(146, 224, 208); 16 | --ifm-code-font-size: 95%; 17 | } 18 | 19 | .docusaurus-highlight-code-line { 20 | background-color: rgba(0, 0, 0, 0.1); 21 | display: block; 22 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 23 | padding: 0 var(--ifm-pre-padding); 24 | } 25 | 26 | html[data-theme='dark'] .docusaurus-highlight-code-line { 27 | background-color: rgba(0, 0, 0, 0.3); 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import styles from './index.module.css'; 7 | 8 | function HomepageHeader() { 9 | const { siteConfig } = useDocusaurusContext(); 10 | return ( 11 |
12 |
13 |

{siteConfig.title}

14 |
15 | 19 | START ⛳️ 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default function Home() { 28 | const { siteConfig } = useDocusaurusContext(); 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/.nojekyll -------------------------------------------------------------------------------- /static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/img/docusaurus.png -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/imfp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/img/imfp.jpeg -------------------------------------------------------------------------------- /static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-in-book/imfp/a5aef00cf85ed0e538c8ba58aff06ac9849bbcf5/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /static/img/undraw_docusaurus_mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /static/img/undraw_docusaurus_react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | docu_tree -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | --------------------------------------------------------------------------------