[
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: chapter9 build\nrun-name: ${{ github. actor }} has been added new commit.\n\non:\n  push:\n    branches-ignore:\n      - 'main'\n    paths:\n      - ./chapter9/zero-to-next\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 16\n      - name: 'install dependencies'\n        working-directory: ./chapter9/zero-to-next\n        run: npm ci\n      - name: 'build'\n        working-directory: ./chapter9/zero-to-next\n        run: npm run build"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\nbuild/\n\n.vscode\n.DS_Store\n.idea"
  },
  {
    "path": "README.md",
    "content": "# react-deep-dive-example\n\n《모던 리액트 Deep Dive》 예제 코드입니다.\n\n## Table of Contents\n\n### 2장 리액트 핵심 요소 깊게 살펴보기 [📁](./chapter2)\n\n#### react [📁](./chapter2/react)\n\n2장에서 다룬 리액트와 관련된 리액트 예제를 모아두었습니다.\n\n### chapter4 서버 사이드 렌더링 [📁](./chapter4)\n\n#### ssr-example [📁](chapter4/ssr-example)\n\n4-2 장에서 다룬 리액트 서버사이드 렌더링 api를 바탕으로 실제 리액트 api 를 기반으로 바닐라 서버사이드 렌더링 애플리케이션을 만든 예제 애플리케이션입니다.\n\n#### next-example [📁](chapter4/next-example)\n\n4-3 장에서 다룬 nextjs 에 대한 예제 애플리케이션입니다.\n\n### chapter8 좋은 리액트 코드 작성을 위한 환경 구축하기 [📁](./chapter8)\n\n#### eslint-plugin-yceffort [📁](chapter8/eslint-plugin-yceffort)\n\n8-1 장에서 다룬 사용자 정의 `eslint-plugin`을 만들어본 예제 입니다. `eslint-plugin-yceffort`라는 이름으로 만들어졌습니다.\n\n#### react-test [📁](chapter8/react-test)\n\n8-2 장에서 다룬 리액트 테스트 코드관련 예제 코드를 모아두었습니다.\n\n### chapter9 모던 리액트 개발 도구로 개발 및 배포 환경 구축하기 [📁](./chapter9)\n\n#### zero-to-next [📁](chapter9/zero-to-next)\n\n9-1장 에서 다룬 빈 폴더에서 부터 nextjs 애플리케이션을 만들어본 예제입니다. \n\n#### danger-react-app [📁](chapter9/danger-react-app)\n\n9-2장 에서 다룬 보안 취약점이 있는 리액트 애플리케이션을 수정한 예제 입니다. 주요 보안 이슈를 수정했지만 여전히 잠재적인 보안 취약점이 있을 수 있으므로 사용하는 것을 권장하지 않습니다.\n\n#### deploy [📁](chapter9/deploy)\n\n9-3장에서 다뤘던 여러 SaaS 서비스에 배포 해본 예제입니다.\n\n### chapter10 리액트 17과 18의 변경사항 살펴보기 [📁](./chapter10)\n\n#### react-gradual-demo [📁](chapter10/react-gradual-demo)\n\n10-1장 리액트 17의 변경에 대해서 다룬 내용 중 하나인 리액트의 점진적인 업데이트를 구현해본 예제 애플리케이션입니다.\n\n### chapter11 Next.js13과 리액트 18 [📁](./chapter11)\n\n#### server-components-demo [📁](chapter11/server-components-demo)\n\n11-2 장에서 소개한 서버 컴포넌트에 대한 예제 애플리케이션입니다. nextjs와 같은 프레임워크를 사용하지 않은 순수 리액트 서버 컴포넌트 예제 입니다.\n\n#### next13 [📁](chapter11/next13)\n\n11-3 장에서 다룬 next@13 이상 버전에서 제공 되고 있는 app directory 를 활용한 예제 애플리케이션입니다.\n\n\n"
  },
  {
    "path": "chapter10/react-gradual-demo/.eslintignore",
    "content": "node_modules\nbuild\nsrc/*/shared\n"
  },
  {
    "path": "chapter10/react-gradual-demo/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/.gitignore",
    "content": "node_modules\nbuild\n.DS_Store\nsrc/*/shared\n"
  },
  {
    "path": "chapter10/react-gradual-demo/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter10/react-gradual-demo/LICENSE",
    "content": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "chapter10/react-gradual-demo/README.md",
    "content": "# Demo of Gradual React Upgrades\n\nhttps://github.com/reactjs/react-gradual-upgrade-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 프로젝트입니다.\n\n## 프로젝트 구조\n\n### modern\n\nreact@17.x 로 구성된 프로젝트이며, gradual demo를 실행하는 기본 리액트 프로젝트 입니다.\n\n### legacy\n\nreact@16.x로 구성된 프로젝트이며, modern에서 import 하여 사용되는 프로젝트 입니다.\n\n### shared\n\n`modern`, `legacy` 두 개의 프로젝트에서 모두 사용되는 파일로 구성되어 있으며, 훅과 Context를 제공합니다. 훅과 context 가 `modern`과 `legacy` 모두에서 사용될 수 있음을 보여주기 위해 만들어진 예제 파일입니다.\n\n## 동작 방식\n\n1. 프로젝트를 빌드 합니다.\n2. 빌드와 동시에 `shared`에 있는 내용을 각각 `modern`과 `legacy`로 복사합니다. 이는 마치 `npm`에서 업로드된 라이브러리를 사용하는 것과 비슷한 구조로 동작합니다.\n3. 프로젝트를 시작합니다."
  },
  {
    "path": "chapter10/react-gradual-demo/package.json",
    "content": "{\n  \"name\": \"react-gradual-demo\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"react-scripts\": \"^5.0.1\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"cpx\": \"1.5.0\",\n    \"eslint\": \"^8.38.0\",\n    \"npm-run-all\": \"4.1.5\",\n    \"prettier\": \"^2.8.7\"\n  },\n  \"scripts\": {\n    \"//\": \"watch:* 명령어와 함께 react-script로 앱 시작\",\n    \"postinstall\": \"run-s install:*\",\n    \"install:legacy\": \"cd src/legacy && npm install\",\n    \"install:modern\": \"cd src/modern && npm install\",\n    \"copy:legacy\": \"cpx 'src/shared/**' 'src/legacy/shared/'\",\n    \"copy:modern\": \"cpx 'src/shared/**' 'src/modern/shared/'\",\n    \"watch:legacy\": \"cpx 'src/shared/**' 'src/legacy/shared/' --watch --no-initial\",\n    \"watch:modern\": \"cpx 'src/shared/**' 'src/modern/shared/' --watch --no-initial\",\n    \"prebuild\": \"run-p copy:*\",\n    \"prestart\": \"run-p copy:*\",\n    \"start\": \"run-p start-app watch:*\",\n    \"start-app\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"eject\": \"react-scripts eject\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/index.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport './modern/index';\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/legacy/Greeting.js",
    "content": "import React, {Component} from 'react';\nimport {findDOMNode} from 'react-dom';\n\nimport ThemeContext from './shared/ThemeContext';\nimport Clock from './shared/Clock';\n\nexport default class AboutSection extends Component {\n\n  componentDidMount() {\n    // eslint-disable-next-line react/no-find-dom-node\n    const legacyDomNode = findDOMNode(this);\n    // eslint-disable-next-line no-console\n    console.log(legacyDomNode)\n  }\n\n  handleClick = () => {\n    // eslint-disable-next-line no-console\n    console.log('hello')\n  }\n\n  render() {\n    return (\n      <ThemeContext.Consumer>\n        {theme => (\n          // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions\n          <div style={{border: '1px dashed black', padding: 20}} onClick={this.handleClick}>\n            <h3>src/legacy/Greeting.js</h3>\n            <h4 style={{color: theme}}>\n              This component is rendered by the nested React ({React.version}).\n            </h4>\n            <Clock />\n          </div>\n        )}\n      </ThemeContext.Consumer>\n    );\n  }\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/legacy/README.md",
    "content": "# legacy\n\n`react@16`을 기반으로 작성된 컴포넌트\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/legacy/createLegacyRoot.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport ThemeContext from './shared/ThemeContext';\n\nexport default function createLegacyRoot(container) {\n  return {\n    // 렌더링\n    render(Component, props, context) {\n      ReactDOM.render(\n        <ThemeContext.Provider value={context.theme}>\n          <Component {...props} />\n        </ThemeContext.Provider>,\n        container\n      );\n    },\n    // 이 컴포넌트의 부모 컴포넌트가 제거될 때 호출될 unmount\n    unmount() {\n      ReactDOM.unmountComponentAtNode(container);\n    },\n  };\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/legacy/package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"react@16 application\",\n  \"dependencies\": {\n    \"react\": \"16.8\",\n    \"react-dom\": \"16.8\"\n  }\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/AboutPage.js",
    "content": "import React, {useContext} from 'react';\n\nimport Clock from './shared/Clock';\nimport ThemeContext from './shared/ThemeContext';\nimport lazyLegacyRoot from './lazyLegacyRoot';\n\nconst Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));\n\nexport default function AboutPage() {\n  const theme = useContext(ThemeContext);\n  return (\n    <>\n      <h2>src/modern/AboutPage.js</h2>\n      <h3 style={{color: theme}}>\n        This component is rendered by the outer React ({React.version}).\n      </h3>\n        <Clock />\n        <Greeting />\n      <br />\n    </>\n  );\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/App.js",
    "content": "import React, {useState, Suspense} from 'react';\n\nimport AboutPage from './AboutPage';\nimport ThemeContext from './shared/ThemeContext';\n\nexport default function App() {\n  const [theme, setTheme] = useState('slategrey');\n\n  function handleToggleClick() {\n    if (theme === 'slategrey') {\n      setTheme('hotpink');\n    } else {\n      setTheme('slategrey');\n    }\n  }\n\n  return (\n    <ThemeContext.Provider value={theme}>\n      <div style={{fontFamily: 'sans-serif'}}>\n        <div\n          style={{\n            margin: 20,\n            padding: 20,\n            border: '1px solid black',\n            minHeight: 300,\n          }}>\n          <button onClick={handleToggleClick}>Toggle Theme Context</button>\n          <br />\n          <Suspense fallback={<Spinner />}>\n            <AboutPage />\n          </Suspense>\n        </div>\n      </div>\n    </ThemeContext.Provider>\n  );\n}\n\nfunction Spinner() {\n  return null;\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/HomePage.js",
    "content": "import React, {useContext} from 'react';\n\nimport ThemeContext from './shared/ThemeContext';\nimport Clock from './shared/Clock';\n\nexport default function HomePage() {\n  const theme = useContext(ThemeContext);\n  return (\n    <>\n      <h2>src/modern/HomePage.js</h2>\n      <h3 style={{color: theme}}>\n        This component is rendered by the outer React ({React.version}).\n      </h3>\n      <Clock />\n    </>\n  );\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/README.md",
    "content": "# modern\n\n`react@17`을 기반으로 작성된 컴포넌트가 모여있으며, 애플리케이션의 시작점이다. `react@17` 을 루트에 선언해 두면, 자식 컴포넌트의 리액트 버전은 17 외에도 가능하다.\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/index.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport React, {StrictMode} from 'react';\nimport ReactDOM from 'react-dom';\n\nimport App from './App';\n\nReactDOM.render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n  document.getElementById('root')\n);\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/lazyLegacyRoot.js",
    "content": "import React, {useContext, useMemo, useRef, useLayoutEffect} from 'react';\n\nimport ThemeContext from './shared/ThemeContext';\n\nconst rendererModule = {\n  status: 'pending',\n  promise: null,\n  result: null,\n};\n\nexport default function lazyLegacyRoot(getLegacyComponent) {\n  const componentModule = {\n    status: 'pending',\n    promise: null,\n    result: null,\n  };\n\n  return function Wrapper(props) {\n    // legacy/createLegacyRoot 를 promise 로 layzy 하게 불러온다.\n    const createLegacyRoot = readModule(rendererModule, () =>\n      import('../legacy/createLegacyRoot')\n    ).default;\n\n    const Component = readModule(componentModule, getLegacyComponent).default;\n    // 구 리액트를 렌더링할 위치\n    const containerRef = useRef(null);\n    // 구 리액트의 루트 컴포넌트\n    const rootRef = useRef(null);\n\n    const theme = useContext(ThemeContext);\n    const context = useMemo(\n      () => ({\n        theme,\n      }),\n      [theme]\n    );\n    useLayoutEffect(() => {\n      // 루트 컴포넌트가 없다면\n      if (!rootRef.current) {\n        // 루트 컴포넌트를 만든다.\n        rootRef.current = createLegacyRoot(containerRef.current);\n      }\n      const root = rootRef.current;\n\n      // cleanUp 시에 unmount\n      return () => {\n        root.unmount();\n      };\n    }, [createLegacyRoot]);\n\n    useLayoutEffect(() => {\n      if (rootRef.current) {\n        // 루트 컴포넌트가 존재하면 적절한 props와 context로 렌더링한다.\n        rootRef.current.render(Component, props, context);\n      }\n    }, [Component, props, context]);\n\n    return <div style={{display: 'contents'}} ref={containerRef} />;\n  };\n}\n\nfunction readModule(record, importStatement) {\n  // promise가 없으면 아직 import 하지 못한 것이므로 import 를 실행한다.\n  if (!record.promise) {\n    /* eslint-disable */\n    record.promise = importStatement().then(\n        value => {\n          if (record.status === 'pending') {\n            record.status = 'fulfilled';\n            record.promise = null;\n            // 성공시 import 반환 값\n            record.result = value;\n            return value\n          }\n        },\n        error => {\n          if (record.status === 'pending') {\n            record.status = 'rejected';\n            record.promise = null;\n            // 실패시 에러\n            record.result = error;\n          }\n        }\n    );\n  }\n\n  // 성공 또는 실패시에 결과를 반환한다.\n  if (record.status === 'fulfilled' || record.status === 'rejected') {\n    return record.result;\n  }\n\n  throw record.promise;\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/modern/package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"react@17 application\",\n  \"dependencies\": {\n    \"react\": \"17.0.0\",\n    \"react-dom\": \"17.0.0\"\n  }\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/shared/Clock.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\n\nimport useTime from './useTime';\n\nexport default function Clock() {\n  const time = useTime();\n  return <p>Time: {time}</p>;\n}\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/shared/README.md",
    "content": "# @shared\n\n`react@16`과 `react@17`에서 공통으로 사용하는 패키지. npm 에서 제공하는 리액트 라이브러리와 비슷하게 보면 된다.\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/shared/ThemeContext.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {createContext} from 'react';\n\nconst ThemeContext = createContext(null);\n\nexport default ThemeContext;\n"
  },
  {
    "path": "chapter10/react-gradual-demo/src/shared/useTime.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport {useState, useEffect} from 'react';\n\nexport default function useTimer() {\n  const [value, setValue] = useState(() => new Date());\n  useEffect(() => {\n    const id = setInterval(() => {\n      setValue(new Date());\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n  return value.toLocaleTimeString();\n}\n"
  },
  {
    "path": "chapter11/next13/.eslintignore",
    "content": ".next\nnode_modules"
  },
  {
    "path": "chapter11/next13/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    'next/core-web-vitals',\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n  rules: {\n    '@typescript-eslint/naming-convention': ['off'],\n  },\n}\n"
  },
  {
    "path": "chapter11/next13/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter11/next13/.npmrc",
    "content": "legacy-peer-deps=true\n"
  },
  {
    "path": "chapter11/next13/.prettierignore",
    "content": ".next\nnode_modules"
  },
  {
    "path": "chapter11/next13/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter11/next13/README.md",
    "content": "# Chapter8 react@18 nextjs@13 예제\n\nhttps://react-deep-dive-example-six.vercel.app/\n\nhttps://github.com/vercel/app-playground 저장소에서 제공하는 예제를 조금 더 간결하고 이해하기 쉽도록 재구성한 저장소입니다. 스타일과 예제 내역을 참고했으며, 구체적인 예제는 코드 설명을 위해 조금씩 수정을 가미했습니다. 원본 예제를 알고 싶다면 vercel의 원래 저장소를 참고해주세요.\n\n## warning\n\n- 스타일이 대부분 [tailwindcss](https://tailwindcss.com/)를 기반으로 작성되어 있기 때문에 `className`이 조금 지저분 할 수 있습니다. `className`은 대부분 스타일을 위해 사용되고 있으니 굳이 `className`을 이해하실 필요는 없습니다.\n- 2023년 5월 기준 `typescript@5.1.0-beta` 와 `styled-components@6.0.0-rc.1`가 일부 리액트 서버 컴포넌트 관련한 코드를 지원하기 시작하여 release candidate 버전임에도 설치했습니다.\n- `typescript@5.1.0-beta` 설치로 인해 아래와 같이 `peerDependencies`의 버전을 제대로 추론하지 못하는 문제를 해결하기 위해 `.npmrc`에 `legacy-peer-deps=true` 옵션을 추가했습니다. 이 문제는 향후에 `typescript@5.1.0`이 정식으로 출시되면 해결 될 것입니다.\n\n  ```text\n  npm ERR! Could not resolve dependency:\n  npm ERR! peerOptional typescript@\">=3.3.1\" from eslint-config-next@13.4.1\n  npm ERR! node_modules/eslint-config-next\n  npm ERR!   dev eslint-config-next@\"^13.4.0\" from the root project\n  npm ERR!\n  npm ERR! Conflicting peer dependency: typescript@5.0.4\n  npm ERR! node_modules/typescript\n  npm ERR!   peerOptional typescript@\">=3.3.1\" from eslint-config-next@13.4.1\n  npm ERR!   node_modules/eslint-config-next\n  npm ERR!     dev eslint-config-next@\"^13.4.0\" from the root project\n  ```\n"
  },
  {
    "path": "chapter11/next13/app/api/hello/route.ts",
    "content": "export async function GET() {\n  return new Response(JSON.stringify({ name: 'John Doe' }), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/api/posts/[id]/route.ts",
    "content": "import type { NextRequest } from 'next/server'\n\nexport async function GET(\n  req: NextRequest,\n  context: { params: { id: string } },\n) {\n  const response = await fetch(\n    `https://jsonplaceholder.typicode.com/posts/${context.params.id}`,\n  )\n  console.log(context)\n  const result = await response.json()\n\n  const now = new Date()\n  const nowStr = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`\n\n  // eslint-disable-next-line no-console\n  console.log('request has been resolved', nowStr)\n  return new Response(JSON.stringify(result), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/api/posts/route.ts",
    "content": "import type { NextRequest } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n  const response = await fetch('https://jsonplaceholder.typicode.com/posts')\n  const result = await response.json()\n  return new Response(JSON.stringify(result), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/api/users/[id]/route.ts",
    "content": "import type { NextRequest } from 'next/server'\n\nexport async function GET(\n  request: NextRequest,\n  context: { params: { id: string } },\n) {\n  const response = await fetch(\n    `https://jsonplaceholder.typicode.com/users/${context.params.id}`,\n  )\n  const result = await response.json()\n\n  const now = new Date()\n  const nowStr = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`\n\n  // eslint-disable-next-line no-console\n  console.log('request has been resolved', nowStr)\n  return new Response(JSON.stringify(result), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/api/users/[id].ts",
    "content": ""
  },
  {
    "path": "chapter11/next13/app/api/users/route.ts",
    "content": "export async function GET() {\n  const response = await fetch('https://jsonplaceholder.typicode.com/users')\n  const result = await response.json()\n\n  return new Response(JSON.stringify(result), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/context/[id]/page.tsx",
    "content": "import Counter from '#components/Counter'\nimport { fetchUserById } from '#services/server'\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const user = await fetchUserById(params.id)\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        이름: {user.name}\n      </h1>\n\n      <Counter />\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/context/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { CounterProvider } from '#context/counter'\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <CounterProvider>\n      <div className=\"space-y-9\">\n        <div className=\"flex justify-between\">\n          <TabGroup path=\"/context\" items={items} />\n        </div>\n\n        <div>{children}</div>\n      </div>\n    </CounterProvider>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/context/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Client Context</h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            Context는 클라이언트 컴포넌트로, 기존에 리액트에서 사용하던 Context\n            문법을 그대로 사용하면 서버와 클라이언트 컴포넌트 모두에서 사용할 수\n            있다.\n          </li>\n          <li>\n            Context는 상태를 가지고 있어야 하므로 클라이언트 컴포넌트가 될 수\n            밖에 없으며, 반드시 파일 상단에 \"use client\"를 선언해주어야 한다.\n          </li>\n          <li>\n            Context.Provider로 하위 라우팅을 감싸주면, 라우팅 내부에서 이동이\n            발생하더라도 Context 내부의 값잉 유지되는 것을 볼 수 있다.\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/error/[id]/page.tsx",
    "content": "import { notFound } from 'next/navigation'\nimport { ReactNode } from 'react'\n\nexport default function Page(): ReactNode | Promise<ReactNode> {\n  if (true.toString() === 'true') {\n    notFound()\n  }\n\n  return <>렌더링 되지 않습니다.</>\n}\n"
  },
  {
    "path": "chapter11/next13/app/error/error.tsx",
    "content": "'use client'\n\nimport { useEffect } from 'react'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport default function Error({ error, reset }: any) {\n  useEffect(() => {\n    // eslint-disable-next-line no-console\n    console.log('logging error:', error)\n  }, [error])\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"text-sm text-vercel-pink\">\n        <strong className=\"font-bold\">Error:</strong> {error?.message}\n      </div>\n      <div>\n        <button\n          className=\"rounded-lg px-3 py-1 text-sm font-medium bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white\"\n          onClick={() => reset()}\n        >\n          에러 리셋\n        </button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/error/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/error\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/error/not-found.tsx",
    "content": "export default function NotFound() {\n  return '404 입니다😭'\n}\n"
  },
  {
    "path": "chapter11/next13/app/error/page.tsx",
    "content": "import ErrorButton from '#components/ErrorButton'\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"flex justify-between gap-x-3\">\n        <h1 className=\"text-xl font-medium text-gray-400/80\">Error Handling</h1>\n\n        <ErrorButton />\n      </div>\n\n      <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n        <li>\n          `error`는 nextjs의 또 다른 예약어로, 해당 라우트 내부에서 사용가능한\n          에러 바운더리를 정의할 수 있는 파일이다.\n        </li>\n        <li>\n          에러 버튼을 눌러보자. 해당 라우트 내부의 레이아웃과 페이지에만 영향을\n          미치며, 여전히 다른 페이지는 상호작용이 가능하다.\n        </li>\n        <li>\n          `not-found`파일을 활용하면 해당 라우트 내부의 404 페이지를 정의할 수\n          있다.\n        </li>\n        <li>\n          유저 버튼을 누르면 /error/{'{id}'}로 이동하는데 이 페이지는\n          `notFound()`를 실행하여 404페이지로 보낸다.\n        </li>\n        <li>주의: `error`는 반드시 클라이언트 컴포넌트여야 한다.</li>\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(main)/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup\n          path=\"/grouped-layouts\"\n          items={[\n            {\n              text: 'Home',\n            },\n            { text: 'users', slug: 'users' },\n            { text: 'todos', slug: 'todos' },\n            { text: 'hello', slug: 'hello' },\n          ]}\n        />\n      </div>\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(main)/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Route Groups</h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            라우팅 그룹은 URL 구조에 영향을 주지 않으면서, 주소에 따라 서로다른\n            레이아웃을 적용할 수 있는 방법이다.\n          </li>\n          <li>\n            만약 대표 루트 URL 페이지가 선언되어 있지 않다면 `(main)`을 해당\n            페이지의 루트로 간주하여 라우팅한다.\n          </li>\n          <li>\n            서로 다른 탭을 네비게이션 해보면, 주소에는 영향이 없지만 주소에\n            따라서 서로 다른 레이아웃을 적용할 수 있다는 것을 알 수 있다.\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(todos)/hello/page.tsx",
    "content": "export default async function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        url: /groped-layouts/hello\n      </h1>\n\n      <div className=\"space-y-4\">안녕하세요.</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(todos)/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <h1>여기는 /groped-layout/(todos) 입니다.</h1>\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(todos)/todos/page.tsx",
    "content": "import { fetchTodos } from '#services/server'\n\nexport default async function Page() {\n  const todos = await fetchTodos()\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        url: /groped-layouts/todos\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          {todos.map((todo) => (\n            <li key={todo.id}>{todo.title}</li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(users)/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <h1>여기는 /groped-layout/(user) 입니다.</h1>\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/grouped-layouts/(users)/users/page.tsx",
    "content": "import { fetchUsers } from '#services/server'\n\nexport default async function Page() {\n  const users = await fetchUsers()\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        url: /groped-layouts/users\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          {users.map((user) => (\n            <li key={user.id}>{user.name}</li>\n          ))}\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/[userId]/head.tsx",
    "content": "import DefaultHeader from '#components/DefaultHeader'\nimport { API_URL_BASE } from '#services/constant'\n\nexport default async function Head({ params }: { params: { userId: string } }) {\n  const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)\n  const user = await response.json()\n\n  return (\n    <>\n      <DefaultHeader />\n      <title>{user.name}</title>\n      <meta name=\"description\" content=\"head를 재정의해보았습니다.\" />\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/[userId]/page.tsx",
    "content": "import SubPage from './sub'\nimport { API_URL_BASE } from '#services/constant'\n\nexport default async function Page({ params }: { params: { userId: string } }) {\n  const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)\n  const user = await response.json()\n\n  return (\n    <>\n      <div className=\"space-y-4\">\n        <h1 className=\"text-xl font-medium text-gray-400/80\">\n          이름: {user.name}\n        </h1>\n      </div>\n      {/* @ts-expect-error Async Server Component */}\n      <SubPage params={{ userId: params.userId }} />\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/[userId]/sub.tsx",
    "content": "import { API_URL_BASE } from '#services/constant'\n\nexport default async function SubPage({\n  params,\n}: {\n  params: { userId: string }\n}) {\n  const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)\n  const user = await response.json()\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        서브페이지 이름: {user.name}\n      </h1>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/head.tsx",
    "content": "import DefaultHeader from '#components/DefaultHeader'\n\nexport default function Head() {\n  return (\n    <>\n      <DefaultHeader />\n      <title>라우트 내부에서 head를 재정의할 수 있습니다.</title>\n      <meta name=\"description\" content=\"head를 재정의해보았습니다.\" />\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/head\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-6\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        head 태그 설정하기\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            `head`를 활용하면 라우트 내부의 {'<head>'}를 원하는 대로 커스텀할 수\n            있다.\n          </li>\n          <li>\n            또한 `head`내부에서 데이터를 불러와서 동적으로 결정하는 것 또한\n            가능하다.\n          </li>\n          <li>\n            Next will dedupe requests for the same data across `layout.js`,\n            `page.js` and `head.js` when rendering a route.\n          </li>\n          <li>\n            nextjs는 `head` 내부에 데이터 요청이 있다면 이 요청이 완료되고{' '}\n            {'<head>'}가 렌더링이 완료될 때 까지 기다린다. 이는 첫번째 스트리밍\n            응답에 무조건 {'<head>'}가 포함될 수 있도록 보장해준다.\n          </li>\n          <li>\n            추가로 같은 라우트 내부에서 발생하는 같은 중복 api 요청에 대한\n            처리도 잘되어 있는 것을 확인할 수 있다. 이는 프로덕션 모드에서만\n            확인가능하며, {'/head/{id}'}페이지에서 같은 api를 3차례 부르지만\n            한번만 요청이 가는 것을 확인할 수 있다.\n          </li>\n          <li>\n            클라이언트에서의 중복처리도 원래 가능한 것으로 알려져 있지만, 현재\n            클라이언트 요청에 대한 중복처리는 아직 개발중인 것으로 보인다.\n            (2023년 1월 기준)\n            <a\n              className=\"inline-flex gap-x-2 rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium text-gray-100 hover:bg-gray-500 hover:text-white\"\n              href=\"https://github.com/vercel/next.js/discussions/41745#discussioncomment-3986980\"\n            >\n              관련 링크 보기\n            </a>\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/head.tsx",
    "content": "import DefaultHeader from '../src/components/DefaultHeader'\n\nexport default function Head() {\n  return (\n    <>\n      <DefaultHeader />\n      <title>Next@13 예제</title>\n      <meta\n        name=\"description\"\n        content=\"Nextjs@13을 기반으로 한 리액트 deep dive 예제입니다.\"\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/internal-api/hello/route.ts",
    "content": "import { NextRequest } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n  return new Response(JSON.stringify({ name: 'hello' }), {\n    status: 200,\n    headers: {\n      'content-type': 'application/json',\n    },\n  })\n}\n"
  },
  {
    "path": "chapter11/next13/app/isr/[id]/page.tsx",
    "content": "import { fetchPostById } from '#services/server'\n\nexport const dynamicParams = true\n\nexport const revalidate = 15 // revalidate this page every 60 seconds\n\nexport async function generateStaticParams() {\n  return [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]\n}\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const data = await fetchPostById(params.id)\n\n  console.log(`generate page ${params.id}`)\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"self-start whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100\">\n        마지막 렌더링 시간 (프로덕션 모드만 확인 가능): UTC{' '}\n        {new Date().toLocaleTimeString()}\n      </div>\n      <h1 className=\"text-2xl font-medium text-gray-100\">{data.title}</h1>\n      <p className=\"font-medium text-gray-400\">{data.body}</p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/isr/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\n\nconst ids = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup\n          path=\"/isr\"\n          items={[\n            {\n              text: 'Home',\n            },\n            ...ids.map((x) => ({\n              text: `Post ${x.id}`,\n              slug: x.id,\n            })),\n          ]}\n        />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/isr/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Incremental Static Regeneration\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            이 예제에서는 과거 `getStaticProps`와 `revalidate`의 조합으로\n            제공하는 `incremental static regeneration`을 구현한 예제다.\n          </li>\n          <li>\n            이 하위 페이지들은 15초 간격으로 페이지를 재생성하며, 15초 이내에\n            방문한 사용자에 대해서는 기존에 캐싱된 페이지를 보여준다.\n          </li>\n          <li>\n            먼저 최초에 페이지를 방문하면 캐싱된 페이지를 보여준다. 그리고 만약\n            그 방문 시점이 revalidate시간, 즉 생성후 15초를 지났다면 새로\n            페이지를 다시 만들고, 그 이후에 방문한 사용자에게는 재생성한\n            페이지를 보여준다.\n          </li>\n          <li>우측 상단의 마지막으로 렌더링된 시간을 주목해서 살펴보자.</li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/layout.tsx",
    "content": "import './globals.css'\nimport { ReactNode } from 'react'\nimport SideBar from '#components/Sidebar'\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body className=\"overflow-y-scroll\">\n        <SideBar />\n\n        <div className=\"lg:pl-72\">\n          <div className=\"mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:py-8 lg:px-8\">\n            <div className=\"rounded-lg p-px shadow-lg\">\n              <div className=\"rounded-lg p-3.5 lg:p-6\">{children}</div>\n            </div>\n          </div>\n        </div>\n      </body>\n    </html>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/layouts/[userId]/page.tsx",
    "content": "import { fetchUserById } from '#services/server'\n\nexport default async function Page({ params }: { params: { userId: string } }) {\n  const user = await fetchUserById(params.userId)\n  if (!user) {\n    return null\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        이름: {user.name}\n      </h1>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/layouts/layout.tsx",
    "content": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services/server'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/layouts\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/layouts/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Layouts</h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>레이아웃은 특정 주소 내부에 공유할 수 있는 UI 를 말한다.</li>\n          <li>\n            네비게이션이 발생하더라도 레이아웃은 그 상태를 유지하고, 다시\n            렌더링하지 않는다.\n          </li>\n          <li>레이아웃은 여러 페이지에 걸쳐 중첩하는 것 또한 가능하다.</li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/loading/[userId]/page.tsx",
    "content": "import { sleep } from '#lib/utils'\nimport { fetchUserById } from '#services/server'\n\nexport default async function Page({ params }: { params: { userId: string } }) {\n  await sleep(5 * 1000)\n  const user = await fetchUserById(params.userId)\n\n  if (!user) {\n    return null\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        이름: {user.name}\n      </h1>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/loading/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services/server'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/loading\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/loading/loading.tsx",
    "content": "export default function Loading() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Loading...</h1>\n\n      <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">로딩 중...</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/loading/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Loading</h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            파일명 loading은 nextjs에서 사용하는 예약어로, 페이지가 아직 렌더링\n            준비가 되지 않았을 때 노출되는 컴포넌트다.\n          </li>\n          <li>\n            Streaming 예제의 Suspense와 다르게, 별도로 Suspense로 감싸지 않아도\n            하위 라우팅 내부에서 공통으로 사용할 수 있다는 장점이 있다.\n          </li>\n          <li>\n            유저를 클릭하면, 유저에 해당하는 컴포넌트가 렌더링 되기 전까지\n            loading.tsx가 잠깐 노출되는 것을 확인할 수 있다.\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/page.tsx",
    "content": "import Link from 'next/link'\nimport { demos } from '../src/constant/menu'\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-8\">\n      <h1 className=\"text-xl font-medium text-gray-300\">Examples</h1>\n\n      <div className=\"space-y-10 text-white\">\n        {demos.map((section) => {\n          return (\n            <div key={section.name} className=\"space-y-5\">\n              <div className=\"text-xs font-semibold uppercase tracking-wider text-gray-400\">\n                {section.name}\n              </div>\n\n              <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-2\">\n                {section.items.map((item) => {\n                  return (\n                    <Link\n                      href={`/${item.slug}`}\n                      key={item.name}\n                      className=\"group block space-y-1.5 rounded-lg bg-gray-900 px-5 py-3 hover:bg-gray-800\"\n                    >\n                      <div className=\"font-medium text-gray-200 group-hover:text-gray-50\">\n                        {item.name}\n                      </div>\n\n                      {item.description ? (\n                        <div className=\"line-clamp-3 text-sm text-gray-400 group-hover:text-gray-300\">\n                          {item.description}\n                        </div>\n                      ) : null}\n                    </Link>\n                  )\n                })}\n              </div>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/form/[id]/loading.tsx",
    "content": "export default function Loading() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Loading...</h1>\n      <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">로딩 중...</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/form/[id]/page.tsx",
    "content": "import kv from '@vercel/kv'\nimport { revalidatePath } from 'next/cache'\n\ninterface Data {\n  name: string\n  age: number\n}\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const key = `test:${params.id}`\n  const data = await kv.get<Data>(key)\n\n  async function handleSubmit(formData: FormData) {\n    'use server'\n\n    const name = formData.get('name')\n    const age = formData.get('age')\n\n    await kv.set(key, {\n      name,\n      age,\n    })\n\n    revalidatePath(`/server-action/form/${params.id}`)\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">form with data</h1>\n      <h2 className=\"text-l font-medium text-gray-400/80\">\n        서버에 저장된 정보: {data?.name} {data?.age}\n      </h2>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>\n          <form action={handleSubmit}>\n            <li>\n              <label htmlFor=\"name\">이름: </label>\n              <input\n                type=\"text\"\n                id=\"name\"\n                name=\"name\"\n                defaultValue={data?.name}\n                placeholder=\"이름을 입력해주세요.\"\n              />\n            </li>\n\n            <li>\n              <label htmlFor=\"age\">나이: </label>\n              <input\n                type=\"number\"\n                id=\"age\"\n                name=\"age\"\n                defaultValue={data?.age}\n                placeholder=\"나이를 입력해주세요.\"\n              />\n            </li>\n\n            <li>\n              <button type=\"submit\">submit</button>\n            </li>\n          </form>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/form/page.tsx",
    "content": "export default function Page() {\n  async function handleSubmit() {\n    'use server'\n\n    console.log('해당 작업은 서버에서 수행합니다. 따라서 CORS 이슈가 없습니다.')\n\n    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {\n      method: 'post',\n      body: JSON.stringify({\n        title: 'foo',\n        body: 'bar',\n        userId: 1,\n      }),\n      headers: {\n        'Content-type': 'application/json; charset=UTF-8',\n      },\n    })\n\n    const result = await response.json()\n    console.log(result)\n  }\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">form</h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>\n          <li>\n            <form action={handleSubmit}>\n              <button type=\"submit\">form 요청 보내보기</button>\n            </form>\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const items = [\n    {\n      text: 'Home',\n    },\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/server-action\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Server action (alpha)\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            서버 액션은 컴포넌트에서 직접 서버사이드 데이터 조작을 할 수 있게\n            해주는 nextjs의 새로운 기능이다.\n          </li>\n          <li>\n            13.4.0 기준으로 실험 기능이므로, `next.config.js`에서\n            `experimental.serverActions = true`로 설정해두어야 한다.\n          </li>\n          <li>서버 액션에서 할 수 있는 것들을 서브 메뉴로 확인해보자.</li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/server-action/start-transition/[id]/page.tsx",
    "content": "import kv from '@vercel/kv'\nimport { ClientButtonComponent } from '#components/server-action/client-component'\n\ninterface Data {\n  name: string\n  age: number\n}\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const key = `test:${params.id}`\n  const data = await kv.get<Data>(key)\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">form with data</h1>\n      <h2 className=\"text-l font-medium text-gray-400/80\">\n        서버에 저장된 정보: {data?.name} {data?.age}\n      </h2>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>\n          <li>이 작업은 useTransition을 기반으로 실행됩니다.</li>\n          <li>\n            <ClientButtonComponent id={params.id} />\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssg/[id]/page.tsx",
    "content": "import { fetchPostById } from '#services/server'\n\nexport async function generateStaticParams() {\n  return [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]\n}\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const data = await fetchPostById(params.id)\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-2xl font-medium text-gray-100\">{data.title}</h1>\n      <p className=\"font-medium text-gray-400\">{data.body}</p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssg/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nconst ids = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup\n          path=\"/ssg\"\n          items={[\n            {\n              text: 'Home',\n            },\n            ...ids.map((x) => ({\n              text: `Post ${x.id}`,\n              slug: x.id,\n            })),\n          ]}\n        />\n        <div className=\"self-start whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100\">\n          마지막 렌더링 시간 (프로덕션 모드만 확인 가능)\n          {new Date().toLocaleTimeString()}\n        </div>\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssg/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Static-Site Generation\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            이 예제는 과거 `getStaticProps`와 `getStaticPaths`를 구현한 예제다.\n            `getStaticPaths`는 `generateStaticParams`으로 대체되었으며, 데이터를\n            불러오는 것은 `fetch`를 사용하는 것으로 동일하다. 최초 빌드시에 미리\n            데이터를 불러오고, 이후 재요청이 있으면 계속 해당 데이터를 사용한다.\n          </li>\n          <li>\n            미리 빌드된 페이지를 확인하고 싶다면, `./next/server/app/ssg`로\n            이동해서 확인해보면 된다. 미리 빌드된 html 파일이 준비되어 있을\n            것이다.\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssr/[id]/page.tsx",
    "content": "import { fetchPostById } from '#services/server'\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  const data = await fetchPostById(params.id, { cache: 'no-cache' })\n\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-2xl font-medium text-gray-100\">{data.title}</h1>\n      <p className=\"font-medium text-gray-400\">{data.body}</p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssr/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services/server'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/ssr\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/ssr/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Static-Site Generation\n      </h1>\n\n      <div className=\"space-y-4\">\n        <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n          <li>\n            서버사이드 렌더링을 수행하면, HTML 페이지를 매 요청이 있을 때 마다\n            새로 만들게 된다. 서버에서는 HTML과 요청의 결과에 따른 JSON 데이터와\n            함께 클라이언트에 필요한 자바스크립트 파일이 전송된다.\n          </li>\n          <li>\n            `./next/server/app/ssr`를 확인해보면, `/ssg` 페이지와는 다르게 미리\n            빌드된 결과물 없이 항상 데이터를 `fetch`할 준비만 되어 있는 것을 볼\n            수 있다.\n          </li>\n          <li>\n            클라이언트에서는 이벤트 핸들러 등이 추가되지 않은 정적인 HTML을\n            받아서 페이지를 미리 보여주고, 리액트는 이 정적인 페이지에 JSON\n            데이터와 자바스크립트를 받아 컴포넌트를 상호작용 가능한 페이지로\n            만들어 준다. 이러한 일련의 과정을 hydration 이라고 한다.\n          </li>\n        </ul>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/streaming/[id]/components.tsx",
    "content": "import { sleep } from '#lib/utils'\nimport { fetchPosts, fetchUsers } from '#services/server'\n\nexport async function Users() {\n  // Suspense를 보기 위해 강제로 지연시킵니다.\n  await sleep(3 * 1000)\n  const users = await fetchUsers()\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  )\n}\n\nexport async function PostByUserId({ userId }: { userId: string }) {\n  await sleep(5 * 1000)\n  const allPosts = await fetchPosts()\n  const posts = allPosts.filter((post) => post.userId === parseInt(userId, 10))\n\n  return (\n    <ul>\n      {posts.map((post) => (\n        <li key={post.id}>{post.title}</li>\n      ))}\n    </ul>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/streaming/[id]/page.tsx",
    "content": "import { Suspense } from 'react'\n\nimport { PostByUserId, Users } from './components'\n\nexport default async function Page({ params }: { params: { id: string } }) {\n  return (\n    <div className=\"space-y-8 lg:space-y-14\">\n      <Suspense fallback={<div>유저 목록을 로딩중입니다.</div>}>\n        {/* 타입스크립트에서 Promise 컴포넌트에 대해 에러를 내기 때문에 임시 처리 */}\n        {/* @ts-expect-error Async Server Component */}\n        <Users />\n      </Suspense>\n\n      <Suspense\n        fallback={<div>유저 {params.id}의 작성 글을 로딩중입니다.</div>}\n      >\n        {/* @ts-expect-error Async Server Component */}\n        <PostByUserId userId={params.id} />\n      </Suspense>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/streaming/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services/server'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n  const users = await fetchUsers()\n\n  const items = [\n    {\n      text: 'Home',\n    },\n    ...users.map((user) => ({\n      text: user.name,\n      slug: user.id.toString(),\n    })),\n  ]\n\n  return (\n    <div className=\"space-y-9\">\n      <div className=\"flex justify-between\">\n        <TabGroup path=\"/streaming\" items={items} />\n      </div>\n\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/streaming/page.tsx",
    "content": "export default async function Page() {\n  return (\n    <div className=\"space-y-8\">\n      <div className=\"space-y-4\">\n        <h1 className=\"text-xl font-medium text-gray-400/80\">\n          Streaming with Suspense\n        </h1>\n        <div className=\"space-y-4\">\n          <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n            <li>\n              스트리밍을 활용하면 서버에서 클라이언트로 UI 컴포넌트를 점진적으로\n              조금씩 보내는 것(스트리밍)이 가능해진다.\n            </li>\n            <li>\n              스트리밍을 활용하면 서버사이드렌더링과 다르게, 전체 페이지를 모두\n              보여줄 때 까지 기다리게 하는 것이 아니라 필요한 부분 부터 먼저\n              렌더링을 마치고 인터랙션할 수 있는 상태로 제공하는 것이\n              가능해진다.\n            </li>\n            <li>\n              위 유저 목록 중 하나를 누르면 유저 컴포넌트로 가는데, 이\n              컴포넌트는 각각 유저목록과 유저의 작성 글을 서로다른 Suspense\n              내부에서 불러온다. 이를 활용하면 `loading` 컴포넌트를 사용했을 때\n              보다 더 세밀하게 로딩을 보여줄 수 있다.\n            </li>\n          </ul>\n        </div>\n      </div>\n\n      <div className=\"grid grid-cols-4 gap-6\">\n        {[1, 2, 3, 4, 5].map((id) => (\n          <div key={id} className=\"col-span-4 lg:col-span-1\" />\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/css-modules/page.tsx",
    "content": "import styles from './styles.module.css'\n\nconst SkeletonCard = () => (\n  <div className={styles.skeleton}>\n    <div className={styles['skeleton-img']} />\n    <div className={styles['skeleton-btn']} />\n    <div className={styles['skeleton-line-one']} />\n    <div className={styles['skeleton-line-two']} />\n  </div>\n)\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Styled with CSS Modules\n      </h1>\n      <div className={styles.container}>\n        <SkeletonCard />\n        <SkeletonCard />\n        <SkeletonCard />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/css-modules/styles.module.css",
    "content": ".container {\n  display: grid;\n  grid-template-columns: repeat(1, minmax(0, 1fr));\n  gap: 1.5rem /* 24px */;\n}\n\n@media (min-width: 1024px) {\n  .container {\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n  }\n}\n\n.skeleton {\n  padding: 1rem /* 16px */;\n  border-radius: 1rem /* 16px */;\n  background-color: rgb(24 24 27 / 0.8);\n}\n\n.skeleton-img,\n.skeleton-btn,\n.skeleton-line-one,\n.skeleton-line-two {\n  border-radius: 0.5rem /* 8px */;\n}\n\n.skeleton-img {\n  height: 3.5rem /* 56px */;\n  background-color: rgb(63 63 70 / 1);\n}\n\n.skeleton-btn,\n.skeleton-line-one,\n.skeleton-line-two {\n  margin-top: 0.75rem /* 12px */;\n  height: 0.75rem /* 12px */;\n}\n\n.skeleton-btn {\n  background-color: rgb(121 40 202 / 1);\n  width: 25%;\n}\n\n.skeleton-line-one,\n.skeleton-line-two {\n  background-color: rgb(63 63 70 / 1);\n}\n\n.skeleton-line-one {\n  width: 91.666667%;\n}\n\n.skeleton-line-two {\n  width: 66.666667%;\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/global-css/page.tsx",
    "content": "import './style.css'\n\nconst SkeletonCard = () => (\n  <div className=\"skeleton\">\n    <div className=\"skeleton-img\" />\n    <div className=\"skeleton-btn\" />\n    <div className=\"skeleton-line-one\" />\n    <div className=\"skeleton-line-two\" />\n  </div>\n)\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Styled with a Global CSS Stylesheet\n      </h1>\n      <div className=\"container\">\n        <SkeletonCard />\n        <SkeletonCard />\n        <SkeletonCard />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/global-css/style.css",
    "content": ".container {\n  display: grid;\n  grid-template-columns: repeat(1, minmax(0, 1fr));\n  gap: 1.5rem /* 24px */;\n}\n\n@media (min-width: 1024px) {\n  .container {\n    grid-template-columns: repeat(3, minmax(0, 1fr));\n  }\n}\n\n.skeleton {\n  padding: 1rem /* 16px */;\n  border-radius: 1rem /* 16px */;\n  background-color: rgb(24 24 27 / 0.8);\n}\n\n.skeleton-img,\n.skeleton-btn,\n.skeleton-line-one,\n.skeleton-line-two {\n  border-radius: 0.5rem /* 8px */;\n}\n\n.skeleton-img {\n  height: 3.5rem /* 56px */;\n  background-color: rgb(63 63 70 / 1);\n}\n\n.skeleton-btn,\n.skeleton-line-one,\n.skeleton-line-two {\n  margin-top: 0.75rem /* 12px */;\n  height: 0.75rem /* 12px */;\n}\n\n.skeleton-btn {\n  background-color: rgb(245 166 35 / 1);\n  width: 25%;\n}\n\n.skeleton-line-one,\n.skeleton-line-two {\n  background-color: rgb(63 63 70 / 1);\n}\n\n.skeleton-line-one {\n  width: 91.666667%;\n}\n\n.skeleton-line-two {\n  width: 66.666667%;\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nconst items = [\n  {\n    text: 'Global CSS',\n    slug: 'global-css',\n  },\n  {\n    text: 'CSS Modules',\n    slug: 'css-modules',\n  },\n  {\n    text: 'Styled Components',\n    slug: 'styled-components',\n  },\n  {\n    text: 'Styled JSX',\n    slug: 'styled-jsx',\n  },\n]\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"space-y-9\">\n      <TabGroup\n        path=\"/styles\"\n        items={[\n          {\n            text: 'Home',\n          },\n          ...items,\n        ]}\n      />\n      <div>{children}</div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/page.tsx",
    "content": "export default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">Styling</h1>\n      <ul className=\"list-disc space-y-2 pl-4 text-sm text-gray-300\">\n        <li>스타일을 적용하는 다양한 방법</li>\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-components/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport StyledComponentsRegistry from '#components/StyledComponentsRegistry'\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return <StyledComponentsRegistry>{children}</StyledComponentsRegistry>\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-components/page.tsx",
    "content": "import {\n  SkeletonInner,\n  SkeletonImg,\n  SkeletonBtn,\n  SkeletonLineOne,\n  SkeletonLineTwo,\n  Container,\n} from '#components/components'\n\nconst Skeleton = () => (\n  <SkeletonInner>\n    <SkeletonImg />\n    <SkeletonBtn />\n    <SkeletonLineOne />\n    <SkeletonLineTwo />\n  </SkeletonInner>\n)\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Styled Components (styled로 만들어진 컴포넌트는 반드시 클라이언트\n        컴포넌트 여야 합니다.)\n      </h1>\n      <Container>\n        <Skeleton />\n        <Skeleton />\n        <Skeleton />\n      </Container>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-jsx/StyledRegistry.tsx",
    "content": "'use client'\n\nimport { ReactNode, useState } from 'react'\nimport { useServerInsertedHTML } from 'next/navigation'\nimport { StyleRegistry, createStyleRegistry } from 'styled-jsx'\n\nexport default function StyledJsxRegistry({\n  children,\n}: {\n  children: ReactNode\n}) {\n  // Only create stylesheet once with lazy initial state\n  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state\n  const [jsxStyleRegistry] = useState(() => createStyleRegistry())\n\n  useServerInsertedHTML(() => {\n    const styles = jsxStyleRegistry.styles()\n    jsxStyleRegistry.flush()\n    return <>{styles}</>\n  })\n\n  return <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-jsx/components.tsx",
    "content": "'use client'\n\nexport const SkeletonCard = () => (\n  <>\n    <div className=\"skeleton\">\n      <div className=\"skeleton-img\" />\n      <div className=\"skeleton-btn\" />\n      <div className=\"skeleton-line-one\" />\n      <div className=\"skeleton-line-two\" />\n    </div>\n    {/* eslint-disable-next-line react/no-unknown-property */}\n    <style jsx>\n      {`\n        .skeleton {\n          padding: 1rem /* 16px */;\n          border-radius: 1rem /* 16px */;\n          background-color: rgb(24 24 27 / 0.8);\n        }\n        .skeleton-img,\n        .skeleton-btn,\n        .skeleton-line-one,\n        .skeleton-line-two {\n          border-radius: 0.5rem /* 8px */;\n        }\n        .skeleton-img {\n          height: 3.5rem /* 56px */;\n          background-color: rgb(63 63 70 / 1);\n        }\n        .skeleton-btn,\n        .skeleton-line-one,\n        .skeleton-line-two {\n          margin-top: 0.75rem /* 12px */;\n          height: 0.75rem /* 12px */;\n        }\n        .skeleton-btn {\n          background-color: rgb(0 112 243 / 1);\n          width: 25%;\n        }\n        .skeleton-line-one,\n        .skeleton-line-two {\n          background-color: rgb(63 63 70 / 1);\n        }\n        .skeleton-line-one {\n          width: 91.666667%;\n        }\n        .skeleton-line-two {\n          width: 66.666667%;\n        }\n      `}\n    </style>\n  </>\n)\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-jsx/layout.tsx",
    "content": "import { ReactNode } from 'react'\n\nimport StyledJsxRegistry from './StyledRegistry'\n\nexport default function Layout({ children }: { children: ReactNode }) {\n  return <StyledJsxRegistry>{children}</StyledJsxRegistry>\n}\n"
  },
  {
    "path": "chapter11/next13/app/styles/styled-jsx/page.tsx",
    "content": "'use client'\n\nimport { SkeletonCard } from './components'\n\nexport default function Page() {\n  return (\n    <div className=\"space-y-4\">\n      <h1 className=\"text-xl font-medium text-gray-400/80\">\n        Styled JSX ({'<style jsx>'}문법을 사용하기 위해서는 반드시 클라이언트\n        컴포넌트여야 합니다.)\n      </h1>\n      <div className=\"container\">\n        <SkeletonCard />\n        <SkeletonCard />\n        <SkeletonCard />\n      </div>\n      {/* eslint-disable-next-line react/no-unknown-property */}\n      <style jsx>\n        {`\n          .container {\n            display: grid;\n            grid-template-columns: repeat(1, minmax(0, 1fr));\n            gap: 1.5rem /* 24px */;\n          }\n\n          @media (min-width: 1024px) {\n            .container {\n              grid-template-columns: repeat(3, minmax(0, 1fr));\n            }\n          }\n        `}\n      </style>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/middleware.ts",
    "content": "import { NextRequest, NextResponse } from 'next/server'\n\nexport function middleware(request: NextRequest) {\n  const requestHeaders = new Headers(request.headers)\n  requestHeaders.set('x-middleware-request', 'request')\n\n  const response = NextResponse.next({\n    request: {\n      headers: requestHeaders,\n    },\n  })\n\n  response.headers.set('x-middleware-response', 'response')\n  return response\n}\n"
  },
  {
    "path": "chapter11/next13/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  experimental: {\n    serverActions: true,\n  },\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter11/next13/package.json",
    "content": "{\n  \"name\": \"next13\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"dev:turbo\": \"next dev --turbo\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint .\",\n    \"lint:fix\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --check\",\n    \"prettier:fix\": \"prettier . --write\"\n  },\n  \"dependencies\": {\n    \"@types/node\": \"18.11.18\",\n    \"@vercel/kv\": \"^0.1.2\",\n    \"clsx\": \"^1.2.1\",\n    \"next\": \"^13.4.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"styled-components\": \"6.0.0-rc.1\",\n    \"typescript\": \"5.1.0-beta\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"@types/react\": \"^18.2.6\",\n    \"@types/react-dom\": \"^18.2.4\",\n    \"autoprefixer\": \"^10.4.13\",\n    \"eslint\": \"^8.38.0\",\n    \"eslint-config-next\": \"^13.4.0\",\n    \"postcss\": \"^8.4.21\",\n    \"prettier\": \"^2.8.7\",\n    \"tailwindcss\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "chapter11/next13/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/Counter.tsx",
    "content": "'use client'\n\nimport { useCallback } from 'react'\n\nimport { useCounter } from '../context/counter'\n\nconst Counter = () => {\n  const [count, setCount] = useCounter()\n\n  const handleClick = useCallback(\n    () => setCount((prev) => prev + 1),\n    [setCount],\n  )\n\n  return (\n    <button\n      onClick={handleClick}\n      className=\"rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100 hover:bg-gray-500 hover:text-white\"\n    >\n      {count} Clicks\n    </button>\n  )\n}\n\nexport default Counter\n"
  },
  {
    "path": "chapter11/next13/src/components/DefaultHeader.tsx",
    "content": "import { memo } from 'react'\n\nfunction DefaultHeader() {\n  return (\n    <>\n      <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n      <link\n        href=\"/favicon/apple-touch-icon.png\"\n        rel=\"apple-touch-icon\"\n        sizes=\"180x180\"\n      />\n      <link\n        href=\"/favicon/favicon-32x32.png\"\n        rel=\"icon\"\n        sizes=\"32x32\"\n        type=\"image/png\"\n      />\n      <link\n        href=\"/favicon/favicon-16x16.png\"\n        rel=\"icon\"\n        sizes=\"16x16\"\n        type=\"image/png\"\n      />\n      <link href=\"/favicon/favicon.ico\" rel=\"shortcut icon\" />\n    </>\n  )\n}\n\nexport default memo(DefaultHeader)\n"
  },
  {
    "path": "chapter11/next13/src/components/ErrorButton.tsx",
    "content": "'use client'\n\nimport { useCallback, useState } from 'react'\n\nexport default function ErrorButton() {\n  const [clicked, setClicked] = useState(false)\n\n  const handleButtonClick = useCallback(() => {\n    setClicked(true)\n  }, [])\n\n  if (clicked) {\n    // 임의로 발생시킨 에러\n    throw new Error('clicked 로 인해 발생한 에러')\n  }\n\n  return (\n    <button\n      className=\"rounded-lg px-3 py-1 text-sm font-medium bg-red-600 text-red-50 hover:bg-red-500 hover:text-white\"\n      onClick={handleButtonClick}\n    >\n      에러 던지기!\n    </button>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/Sidebar.tsx",
    "content": "'use client'\n\nimport Link from 'next/link'\nimport { useSelectedLayoutSegment } from 'next/navigation'\nimport { clsx } from 'clsx'\nimport { useCallback, useState } from 'react'\n\nimport { demos, type Item } from '#constant/menu'\n\nexport default function SideBar() {\n  const [open, setOpen] = useState(false)\n  const handleClose = useCallback(() => setOpen(false), [])\n  const handleButtonClick = useCallback(() => setOpen((prev) => !prev), [])\n\n  return (\n    <div className=\"fixed top-0 z-10 flex w-full flex-col border-b border-gray-800 bg-black lg:bottom-0 lg:z-auto lg:w-72 lg:border-r lg:border-gray-800\">\n      <div className=\"flex h-14 items-center py-4 px-4 lg:h-auto\">\n        <Link\n          href=\"/\"\n          className=\"group flex w-full items-center gap-x-2.5\"\n          onClick={handleClose}\n        >\n          <h3 className=\"font-semibold tracking-wide text-gray-400 group-hover:text-gray-50\">\n            Next@13 App Directory 예제\n          </h3>\n        </Link>\n      </div>\n      <button\n        type=\"button\"\n        className=\"group absolute right-0 top-0 flex h-14 items-center gap-x-2 px-4 lg:hidden\"\n        onClick={handleButtonClick}\n      >\n        <div className=\"font-medium text-gray-100 group-hover:text-gray-400\">\n          Menu\n        </div>\n      </button>\n\n      <div\n        className={clsx(\n          'overflow-y-auto lg:static lg:block',\n          open ? 'fixed inset-x-0 bottom-0 top-14 mt-px bg-black' : 'hidden',\n        )}\n      >\n        <nav className=\"space-y-6 px-2 py-5\">\n          {demos.map((section) => {\n            return (\n              <div key={section.name}>\n                <div className=\"mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400/80\">\n                  <div>{section.name}</div>\n                </div>\n\n                <div className=\"space-y-1\">\n                  {section.items.map((item) => (\n                    <GlobalNavItem\n                      key={item.slug}\n                      item={item}\n                      close={handleClose}\n                    />\n                  ))}\n                </div>\n              </div>\n            )\n          })}\n        </nav>\n      </div>\n    </div>\n  )\n}\n\nfunction GlobalNavItem({\n  item,\n  close,\n}: {\n  item: Item\n  close: () => false | void\n}) {\n  const segment = useSelectedLayoutSegment()\n  const isActive = item.slug === segment\n\n  return (\n    <Link\n      onClick={close}\n      href={`/${item.slug}`}\n      className={clsx(\n        'block rounded-md px-3 py-2 text-sm font-medium hover:text-gray-300',\n        isActive ? 'text-white' : 'text-gray-400 hover:bg-gray-800',\n      )}\n    >\n      {item.name}\n    </Link>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/StyledComponentsRegistry.tsx",
    "content": "'use client'\n\nimport { ReactNode, useState } from 'react'\nimport { useServerInsertedHTML } from 'next/navigation'\nimport { ServerStyleSheet, StyleSheetManager } from 'styled-components'\n\nexport default function StyledComponentsRegistry({\n  children,\n}: {\n  children: ReactNode\n}) {\n  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())\n\n  useServerInsertedHTML(() => {\n    const styles = styledComponentsStyleSheet.getStyleElement()\n    styledComponentsStyleSheet.instance.clearTag()\n    return <>{styles}</>\n  })\n\n  if (typeof window !== 'undefined') {\n    return <>{children}</>\n  }\n\n  return (\n    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>\n      <>{children}</>\n    </StyleSheetManager>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/Tab.tsx",
    "content": "'use client'\n\nimport { clsx } from 'clsx'\nimport Link from 'next/link'\nimport { useSelectedLayoutSegment } from 'next/navigation'\n\nimport { type Item } from './TabGroup'\n\nexport const Tab = ({\n  path,\n  item: { slug, text },\n}: {\n  path: string\n  item: Item\n}) => {\n  const segment = useSelectedLayoutSegment()\n  const href = slug ? path + '/' + slug : path\n  const isActive =\n    // Example home pages e.g. `/layouts`\n    (!slug && segment === null) ||\n    // Nested pages e.g. `/layouts/electronics`\n    segment === slug\n\n  return (\n    <Link\n      href={href}\n      prefetch={false}\n      className={clsx(\n        'rounded-lg px-3 py-1 text-sm font-medium',\n        isActive\n          ? 'bg-vercel-blue text-white'\n          : 'bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white',\n      )}\n    >\n      {text}\n    </Link>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/TabGroup.tsx",
    "content": "import { Tab } from './Tab'\n\nexport interface Item {\n  text: string\n  slug?: string\n}\n\nexport const TabGroup = ({ path, items }: { path: string; items: Item[] }) => {\n  return (\n    <div className=\"flex flex-wrap gap-2 items-center\">\n      {items.map((item) => (\n        <Tab key={path + item.slug} item={item} path={path} />\n      ))}\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/components/components.ts",
    "content": "'use client'\n\nimport styled from 'styled-components'\n\nexport const Container = styled.div`\n  display: grid;\n  grid-template-columns: repeat(3, minmax(0, 1fr));\n  gap: 1.5rem /* 24px */;\n`\n\nexport const SkeletonInner = styled.div`\n  padding: 1rem /* 16px */;\n  background-color: rgb(24 24 27 / 0.8);\n  border-radius: 1rem /* 16px */;\n`\n\nexport const SkeletonImg = styled.div`\n  height: 3.5rem /* 56px */;\n  border-radius: 0.5rem /* 8px */;\n  background-color: rgb(63 63 70 / 1);\n`\n\nexport const SkeletonBtn = styled.div`\n  margin-top: 0.75rem /* 12px */;\n  width: 25%;\n  height: 0.75rem /* 12px */;\n  border-radius: 0.5rem /* 8px */;\n  background-color: rgb(255 0 128 / 1);\n`\n\nexport const SkeletonLineOne = styled.div`\n  margin-top: 0.75rem /* 12px */;\n  height: 0.75rem /* 12px */;\n  width: 91.666667%;\n  border-radius: 0.5rem /* 8px */;\n  background-color: rgb(63 63 70 / 1);\n`\n\nexport const SkeletonLineTwo = styled.div`\n  margin-top: 0.75rem /* 12px */;\n  height: 0.75rem /* 12px */;\n  width: 66.666667%;\n  border-radius: 0.5rem /* 8px */;\n  background-color: rgb(63 63 70 / 1);\n`\n"
  },
  {
    "path": "chapter11/next13/src/components/server-action/client-component.tsx",
    "content": "'use client'\nimport { useCallback, useTransition } from 'react'\nimport { updateData } from '#server-action'\nimport { SkeletonBtn } from '#components/components'\n\nexport function ClientButtonComponent({ id }: { id: string }) {\n  const [isPending, startTransition] = useTransition()\n\n  const handleClick = useCallback(() => {\n    startTransition(() => updateData(id, { name: '기본값', age: 0 }))\n  }, [])\n\n  return isPending ? (\n    <SkeletonBtn />\n  ) : (\n    <button onClick={handleClick}>기본값으로 돌리기</button>\n  )\n}\n"
  },
  {
    "path": "chapter11/next13/src/constant/menu.ts",
    "content": "export interface Item {\n  name: string\n  slug: string\n  description: string\n}\n\nexport const demos: Array<{ name: string; items: Item[] }> = [\n  {\n    name: 'Layouts',\n    items: [\n      {\n        name: 'Nested Layouts',\n        slug: 'layouts',\n        description: '중첩 레이아웃 - 주소에 따라 적용할 수 있는 레이아웃',\n      },\n      {\n        name: 'Grouped Layouts',\n        slug: 'grouped-layouts',\n        description:\n          '그룹 레이아웃 - 주소에 영향을 미치지 않고 특정 주소에 따라 그룹화',\n      },\n    ],\n  },\n  {\n    name: 'File Conventions',\n    items: [\n      {\n        name: 'loading.js',\n        slug: 'loading',\n        description:\n          '데이터를 불러오거나 렌더링하는 동안 표시할 수 있는 로딩 컴포넌트',\n      },\n      {\n        name: 'error.js',\n        slug: 'error',\n        description: '에러 발생시 렌더링할 수 있는 에러 컴포넌트',\n      },\n      {\n        name: 'head.js',\n        slug: 'head',\n        description: 'URL에 따라 보여줄 수 있는 head',\n      },\n    ],\n  },\n  {\n    name: 'Data Fetching',\n    items: [\n      {\n        name: 'Static-Site Generation',\n        slug: 'ssg',\n        description: '기존 getStaticProps을 nextjs@13에서 구현하는 방법',\n      },\n      {\n        name: 'Server-Side Rendering',\n        slug: 'ssr',\n        description: '기존 `getServerSideProps`를 nextjs@13에서 구현하는 방법',\n      },\n      {\n        name: 'Incremental Static Regeneration',\n        slug: 'isr',\n        description: '기존 `getStaticProps`와 revalidate 옵션을 구현하는 방법',\n      },\n      {\n        name: 'Streaming with Suspense',\n        slug: 'streaming',\n        description: 'React Suspense를 활용한 서버 스트리밍 데이터 불러오기',\n      },\n    ],\n  },\n  {\n    name: 'Components',\n    items: [\n      {\n        name: 'Client context',\n        slug: 'context',\n        description:\n          '`Context`는 상태를 가지고 있으므로 반드시 클라이언트 컴포넌트여야 한다.',\n      },\n    ],\n  },\n  {\n    name: 'Styles',\n    items: [\n      {\n        name: 'CSS and CSS-in-JS',\n        slug: 'styles',\n        description: '스타일을 적용하는 다양한 방법',\n      },\n    ],\n  },\n  {\n    name: 'Server Action',\n    items: [\n      {\n        name: 'form action',\n        slug: 'server-action/form',\n        description: '서버액션을 form과 함께 사용해보기',\n      },\n      {\n        name: 'form action with data',\n        slug: 'server-action/form/1',\n        description: '서버액션을 form과 데이터를 기반으로 사용해보기',\n      },\n      {\n        name: 'form action with useTransition',\n        slug: 'server-action/start-transition/1',\n        description: '서버액션을 useTransition과 함께 사용해보기',\n      },\n    ],\n  },\n]\n"
  },
  {
    "path": "chapter11/next13/src/context/counter.tsx",
    "content": "'use client'\n\nimport {\n  createContext,\n  Dispatch,\n  SetStateAction,\n  useState,\n  useContext,\n  ReactNode,\n} from 'react'\n\nconst CounterContext = createContext<\n  [number, Dispatch<SetStateAction<number>>] | undefined\n>(undefined)\n\nexport function CounterProvider({ children }: { children: ReactNode }) {\n  const [count, setCount] = useState(0)\n\n  return (\n    <CounterContext.Provider value={[count, setCount]}>\n      {children}\n    </CounterContext.Provider>\n  )\n}\n\nexport function useCounter() {\n  const context = useContext(CounterContext)\n\n  if (context === undefined) {\n    throw new Error('useCounter must be used within a CounterProvider')\n  }\n  return context\n}\n"
  },
  {
    "path": "chapter11/next13/src/lib/utils.ts",
    "content": "export async function sleep(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"
  },
  {
    "path": "chapter11/next13/src/server-action/index.ts",
    "content": "'use server'\n\nimport kv from '@vercel/kv'\nimport { revalidatePath } from 'next/cache'\nimport { cookies } from 'next/headers'\n\nexport async function updateData(\n  id: string,\n  data: { name: string; age: number },\n) {\n  const key = `test:${id}`\n\n  await kv.set(key, {\n    name: data.name,\n    age: data.age,\n  })\n\n  revalidatePath(`/server-action/form/${id}`)\n}\n"
  },
  {
    "path": "chapter11/next13/src/services/constant.ts",
    "content": "export const API_URL_BASE = process.env.VERCEL_URL\n  ? 'https://' + process.env.VERCEL_URL\n  : 'http://localhost:3000'\n"
  },
  {
    "path": "chapter11/next13/src/services/server.ts",
    "content": "interface User {\n  id: number\n  name: string\n  email: string\n  address: {\n    street: string\n    suite: string\n    city: string\n    zipcode: string\n    geo: {\n      lat: string\n      lng: string\n    }\n  }\n  phone: string\n  website: string\n  company: {\n    name: string\n    catchPhrase: string\n    bs: string\n  }\n}\n\nexport async function fetchUsers(): Promise<Array<User>> {\n  const response = await fetch('https://jsonplaceholder.typicode.com/users')\n  const result: Array<User> = await response.json()\n\n  return result\n}\n\nexport async function fetchUserById(id: string | number): Promise<User> {\n  const response = await fetch(\n    `https://jsonplaceholder.typicode.com/users/${id}`,\n  )\n  const result: User = await response.json()\n\n  return result\n}\n\ninterface Todo {\n  userId: number\n  id: number\n  title: string\n  completed: boolean\n}\n\nexport async function fetchTodos(): Promise<Array<Todo>> {\n  const response = await fetch('https://jsonplaceholder.typicode.com/todos')\n  const result: Array<Todo> = await response.json()\n\n  return result\n}\n\ninterface Post {\n  userId: number\n  id: number\n  title: string\n  body: string\n}\n\nexport async function fetchPosts(): Promise<Array<Post>> {\n  const response = await fetch('https://jsonplaceholder.typicode.com/posts')\n  const result: Array<Post> = await response.json()\n\n  return result\n}\n\nexport async function fetchPostById(\n  id: number | string,\n  options?: RequestInit,\n): Promise<Post> {\n  const response = await fetch(\n    `https://jsonplaceholder.typicode.com/posts/${id}`,\n    options,\n  )\n  const result: Post = await response.json()\n\n  return result\n}\n"
  },
  {
    "path": "chapter11/next13/tailwind.config.js",
    "content": "const colors = require('tailwindcss/colors')\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    './app/**/*.{js,ts,jsx,tsx}',\n    './src/components/**/*.{js,ts,jsx,tsx}',\n  ],\n  future: {\n    hoverOnlyWhenSupported: true,\n  },\n  theme: {\n    extend: {\n      colors: {\n        gray: colors.zinc,\n        'gray-1000': 'rgb(17,17,19)',\n        'gray-1100': 'rgb(10,10,11)',\n        vercel: {\n          pink: '#FF0080',\n          blue: '#0070F3',\n          cyan: '#50E3C2',\n          orange: '#F5A623',\n          violet: '#7928CA',\n        },\n      },\n      backgroundImage: ({ theme }) => ({\n        'vc-border-gradient': `radial-gradient(at left top, ${theme(\n          'colors.gray.500',\n        )}, 50px, ${theme('colors.gray.800')} 50%)`,\n      }),\n      keyframes: ({ theme }) => ({\n        rerender: {\n          '0%': {\n            'border-color': theme('colors.vercel.pink'),\n          },\n          '40%': {\n            'border-color': theme('colors.vercel.pink'),\n          },\n        },\n        highlight: {\n          '0%': {\n            background: theme('colors.vercel.pink'),\n            color: theme('colors.white'),\n          },\n          '40%': {\n            background: theme('colors.vercel.pink'),\n            color: theme('colors.white'),\n          },\n        },\n        shimmer: {\n          '100%': {\n            transform: 'translateX(100%)',\n          },\n        },\n        translateXReset: {\n          '100%': {\n            transform: 'translateX(0)',\n          },\n        },\n        fadeToTransparent: {\n          '0%': {\n            opacity: 1,\n          },\n          '40%': {\n            opacity: 1,\n          },\n          '100%': {\n            opacity: 0,\n          },\n        },\n      }),\n    },\n  },\n}\n"
  },
  {
    "path": "chapter11/next13/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"#app/*\": [\"app/*\"],\n      \"#components/*\": [\"src/components/*\"],\n      \"#constant/*\": [\"src/constant/*\"],\n      \"#context/*\": [\"src/context/*\"],\n      \"#lib/*\": [\"src/lib/*\"],\n      \"#server-action/*\": [\"src/server-action/*\"],\n      \"#server-action\": [\"src/server-action/index.ts\"],\n      \"#services/*\": [\"src/services/*\"]\n    },\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# notes\nnotes/*.md\n\n# misc\n.DS_Store\n.eslintcache\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# vscode\n.vscode\n"
  },
  {
    "path": "chapter11/server-components-demo/.nvmrc",
    "content": "lts/hydrogen"
  },
  {
    "path": "chapter11/server-components-demo/.prettierignore",
    "content": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.eslintcache\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n*.html\n*.json\n*.md\n"
  },
  {
    "path": "chapter11/server-components-demo/.prettierrc.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use strict';\n\nmodule.exports = {\n  arrowParens: 'always',\n  bracketSpacing: false,\n  singleQuote: true,\n  jsxBracketSameLine: true,\n  trailingComma: 'es5',\n  printWidth: 80,\n};\n"
  },
  {
    "path": "chapter11/server-components-demo/CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all project spaces, and it also applies when\nan individual is representing the project or its community in public spaces.\nExamples of representing a project or community include using an official\nproject e-mail address, posting via an official social media account, or acting\nas an appointed representative at an online or offline event. Representation of\na project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at <opensource-conduct@fb.com>. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "chapter11/server-components-demo/Dockerfile",
    "content": "FROM node:lts-hydrogen\n\nWORKDIR /opt/notes-app\n\nCOPY package.json package-lock.json ./\n\nRUN npm install --legacy-peer-deps\n\nCOPY . .\n\nENTRYPOINT [ \"npm\", \"run\" ]\nCMD [ \"start\" ]\n"
  },
  {
    "path": "chapter11/server-components-demo/LICENSE",
    "content": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "chapter11/server-components-demo/README.md",
    "content": "# Demo of sever components\n\nhttps://github.com/reactjs/server-components-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 프로젝트입니다.\n\n## 프로젝트 구조\n\nTBD\n\n## 실행하기\n\n### 빠르게 실행하기\n\n- 데이터 생성하기\n  - `docker-compose up -d` 로 detach 모드로 실행\n  - `docker-compose exec notes-app npm run seed`로 데이터 생성\n- 애플리케이션 실행하기\n  - `docker-compose up`\n"
  },
  {
    "path": "chapter11/server-components-demo/credentials.js",
    "content": "module.exports = {\n  host: process.env.DB_HOST || 'localhost',\n  database: 'notesapi',\n  user: 'notesadmin',\n  password: 'password',\n  port: '5432',\n};\n"
  },
  {
    "path": "chapter11/server-components-demo/docker-compose.yml",
    "content": "version: \"3.8\"\nservices:\n  postgres:\n    image: postgres:13\n    environment:\n      POSTGRES_USER: notesadmin\n      POSTGRES_PASSWORD: password\n      POSTGRES_DB: notesapi\n    ports:\n      - '5432:5432'\n    volumes:\n      - ./scripts/init_db.sh:/docker-entrypoint-initdb.d/init_db.sh\n      - db:/var/lib/postgresql/data\n\n  notes-app:\n    build:\n      context: .\n    depends_on:\n      - postgres\n    ports:\n      - '4000:4000'\n    environment:\n      DB_HOST: postgres\n      PORT: 4000\n    volumes:\n      - ./notes:/opt/notes-app/notes\n      - ./public:/opt/notes-app/public\n      - ./scripts:/opt/notes-app/scripts\n      - ./server:/opt/notes-app/server\n      - ./src:/opt/notes-app/src\n      - ./credentials.js:/opt/notes-app/credentials.js\n\nvolumes:\n  db:\n"
  },
  {
    "path": "chapter11/server-components-demo/notes/.gitkeep",
    "content": ""
  },
  {
    "path": "chapter11/server-components-demo/package.json",
    "content": "{\n  \"name\": \"react-notes\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"engines\": {\n    \"node\": \">=14.9.0\"\n  },\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@babel/core\": \"7.21.3\",\n    \"@babel/plugin-transform-modules-commonjs\": \"^7.21.2\",\n    \"@babel/preset-react\": \"^7.18.6\",\n    \"@babel/register\": \"^7.21.0\",\n    \"acorn-jsx\": \"^5.3.2\",\n    \"acorn-loose\": \"^8.3.0\",\n    \"babel-loader\": \"8.3.0\",\n    \"compression\": \"^1.7.4\",\n    \"concurrently\": \"^7.6.0\",\n    \"date-fns\": \"^2.29.3\",\n    \"excerpts\": \"^0.0.3\",\n    \"express\": \"^4.18.2\",\n    \"html-webpack-plugin\": \"5.5.0\",\n    \"marked\": \"^4.2.12\",\n    \"nodemon\": \"^2.0.21\",\n    \"pg\": \"^8.10.0\",\n    \"react\": \"18.3.0-next-1308e49a6-20230330\",\n    \"react-dom\": \"18.3.0-next-1308e49a6-20230330\",\n    \"react-error-boundary\": \"^4.0.9\",\n    \"react-server-dom-webpack\": \"18.3.0-next-1308e49a6-20230330\",\n    \"resolve\": \"1.22.1\",\n    \"rimraf\": \"^4.4.0\",\n    \"sanitize-html\": \"^2.10.0\",\n    \"server-only\": \"^0.0.1\",\n    \"webpack\": \"5.76.2\"\n  },\n  \"devDependencies\": {\n    \"cross-env\": \"^7.0.3\",\n    \"prettier\": \"1.19.1\"\n  },\n  \"scripts\": {\n    \"start\": \"concurrently \\\"npm run server:dev\\\" \\\"npm run bundler:dev\\\"\",\n    \"start:prod\": \"concurrently \\\"npm run server:prod\\\" \\\"npm run bundler:prod\\\"\",\n    \"server:dev\": \"cross-env NODE_ENV=development nodemon -- --conditions=react-server server\",\n    \"server:prod\": \"cross-env NODE_ENV=production nodemon -- --conditions=react-server server\",\n    \"bundler:dev\": \"cross-env NODE_ENV=development nodemon -- scripts/build.js\",\n    \"bundler:prod\": \"cross-env NODE_ENV=production nodemon -- scripts/build.js\",\n    \"prettier\": \"prettier --write **/*.js\",\n    \"seed\": \"node ./scripts/seed.js\"\n  },\n  \"babel\": {\n    \"presets\": [\n      [\n        \"@babel/preset-react\",\n        {\n          \"runtime\": \"automatic\"\n        }\n      ]\n    ]\n  },\n  \"nodemonConfig\": {\n    \"ignore\": [\n      \"build/*\"\n    ]\n  },\n  \"overrides\": {\n    \"react\": \"18.3.0-next-1308e49a6-20230330\",\n    \"react-dom\": \"18.3.0-next-1308e49a6-20230330\"\n  }\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"description\" content=\"React with Server Components demo\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"stylesheet\" href=\"style.css\" />\n    <title>React Notes</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script>\n      // In development, we restart the server on every edit.\n      // For the purposes of this demo, retry fetch automatically.\n      let nativeFetch = window.fetch;\n      window.fetch = async function fetchWithRetry(...args) {\n        for (let i = 0; i < 4; i++) {\n          try {\n            return await nativeFetch(...args);\n          } catch (e) {\n            if (args[1] && args[1].method !== 'GET') {\n              // Don't retry mutations to avoid confusion\n              throw e;\n            }\n            await new Promise(resolve => setTimeout(resolve, 500));\n          }\n        }\n        return nativeFetch(...args);\n      }\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter11/server-components-demo/public/style.css",
    "content": "/* -------------------------------- CSSRESET --------------------------------*/\n/* CSS Reset adapted from https://dev.to/hankchizljaw/a-modern-css-reset-6p3 */\n/* Box sizing rules */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n/* Remove default padding */\nul[class],\nol[class] {\n  padding: 0;\n}\n\n/* Remove default margin */\nbody,\nh1,\nh2,\nh3,\nh4,\np,\nul[class],\nol[class],\nli,\nfigure,\nfigcaption,\nblockquote,\ndl,\ndd {\n  margin: 0;\n}\n\n/* Set core body defaults */\nbody {\n  min-height: 100vh;\n  scroll-behavior: smooth;\n  text-rendering: optimizeSpeed;\n  line-height: 1.5;\n}\n\n/* Remove list styles on ul, ol elements with a class attribute */\nul[class],\nol[class] {\n  list-style: none;\n}\n\n/* A elements that don't have a class get default styles */\na:not([class]) {\n  text-decoration-skip-ink: auto;\n}\n\n/* Make images easier to work with */\nimg {\n  max-width: 100%;\n  display: block;\n}\n\n/* Natural flow and rhythm in articles by default */\narticle > * + * {\n  margin-block-start: 1em;\n}\n\n/* Inherit fonts for inputs and buttons */\ninput,\nbutton,\ntextarea,\nselect {\n  font: inherit;\n}\n\n/* Remove all animations and transitions for people that prefer not to see them */\n@media (prefers-reduced-motion: reduce) {\n  * {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}\n/* -------------------------------- /CSSRESET --------------------------------*/\n\n:root {\n  /* Colors */\n  --main-border-color: #ddd;\n  --primary-border: #037dba;\n  --gray-20: #404346;\n  --gray-60: #8a8d91;\n  --gray-70: #bcc0c4;\n  --gray-80: #c9ccd1;\n  --gray-90: #e4e6eb;\n  --gray-95: #f0f2f5;\n  --gray-100: #f5f7fa;\n  --primary-blue: #037dba;\n  --secondary-blue: #0396df;\n  --tertiary-blue: #c6efff;\n  --flash-blue: #4cf7ff;\n  --outline-blue: rgba(4, 164, 244, 0.6);\n  --navy-blue: #035e8c;\n  --red-25: #bd0d2a;\n  --secondary-text: #65676b;\n  --white: #fff;\n  --yellow: #fffae1;\n\n  --outline-box-shadow: 0 0 0 2px var(--outline-blue);\n  --outline-box-shadow-contrast: 0 0 0 2px var(--navy-blue);\n\n  /* Fonts */\n  --sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto,\n    Ubuntu, Helvetica, sans-serif;\n  --monospace: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,\n    monospace;\n}\n\nhtml {\n  font-size: 100%;\n}\n\nbody {\n  font-family: var(--sans-serif);\n  background: var(--gray-100);\n  font-weight: 400;\n  line-height: 1.75;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5 {\n  margin: 0;\n  font-weight: 700;\n  line-height: 1.3;\n}\n\nh1 {\n  font-size: 3.052rem;\n}\nh2 {\n  font-size: 2.441rem;\n}\nh3 {\n  font-size: 1.953rem;\n}\nh4 {\n  font-size: 1.563rem;\n}\nh5 {\n  font-size: 1.25rem;\n}\nsmall,\n.text_small {\n  font-size: 0.8rem;\n}\npre,\ncode {\n  font-family: var(--monospace);\n  border-radius: 6px;\n}\npre {\n  background: var(--gray-95);\n  padding: 12px;\n  line-height: 1.5;\n}\ncode {\n  background: var(--yellow);\n  padding: 0 3px;\n  font-size: 0.94rem;\n  word-break: break-word;\n}\npre code {\n  background: none;\n}\na {\n  color: var(--primary-blue);\n}\n\n.text-with-markdown h1,\n.text-with-markdown h2,\n.text-with-markdown h3,\n.text-with-markdown h4,\n.text-with-markdown h5 {\n  margin-block: 2rem 0.7rem;\n  margin-inline: 0;\n}\n\n.text-with-markdown blockquote {\n  font-style: italic;\n  color: var(--gray-20);\n  border-left: 3px solid var(--gray-80);\n  padding-left: 10px;\n}\n\nhr {\n  border: 0;\n  height: 0;\n  border-top: 1px solid rgba(0, 0, 0, 0.1);\n  border-bottom: 1px solid rgba(255, 255, 255, 0.3);\n}\n\n/* ---------------------------------------------------------------------------*/\n.main {\n  display: flex;\n  height: 100vh;\n  width: 100%;\n  overflow: hidden;\n}\n\n.col {\n  height: 100%;\n}\n.col:last-child {\n  flex-grow: 1;\n}\n\n.logo {\n  height: 20px;\n  width: 22px;\n  margin-inline-end: 10px;\n}\n\n.edit-button {\n  border-radius: 100px;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  padding: 6px 20px 8px;\n  cursor: pointer;\n  font-weight: 700;\n  outline-style: none;\n}\n.edit-button--solid {\n  background: var(--primary-blue);\n  color: var(--white);\n  border: none;\n  margin-inline-start: 6px;\n  transition: all 0.2s ease-in-out;\n}\n.edit-button--solid:hover {\n  background: var(--secondary-blue);\n}\n.edit-button--solid:focus {\n  box-shadow: var(--outline-box-shadow-contrast);\n}\n.edit-button--outline {\n  background: var(--white);\n  color: var(--primary-blue);\n  border: 1px solid var(--primary-blue);\n  margin-inline-start: 12px;\n  transition: all 0.1s ease-in-out;\n}\n.edit-button--outline:disabled {\n  opacity: 0.5;\n}\n.edit-button--outline:hover:not([disabled]) {\n  background: var(--primary-blue);\n  color: var(--white);\n}\n.edit-button--outline:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n\nul.notes-list {\n  padding: 16px 0;\n}\n.notes-list > li {\n  padding: 0 16px;\n}\n.notes-empty {\n  padding: 16px;\n}\n\n.sidebar {\n  background: var(--white);\n  box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.1), 0px 2px 2px rgba(0, 0, 0, 0.1);\n  overflow-y: scroll;\n  z-index: 1000;\n  flex-shrink: 0;\n  max-width: 350px;\n  min-width: 250px;\n  width: 30%;\n}\n.sidebar-header {\n  letter-spacing: 0.15em;\n  text-transform: uppercase;\n  padding: 36px 16px 16px;\n  display: flex;\n  align-items: center;\n}\n.sidebar-menu {\n  padding: 0 16px 16px;\n  display: flex;\n  justify-content: space-between;\n}\n.sidebar-menu > .search {\n  position: relative;\n  flex-grow: 1;\n}\n.sidebar-note-list-item {\n  position: relative;\n  margin-bottom: 12px;\n  padding: 16px;\n  width: 100%;\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  flex-wrap: wrap;\n  max-height: 100px;\n  transition: max-height 250ms ease-out;\n  transform: scale(1);\n}\n.sidebar-note-list-item.note-expanded {\n  max-height: 300px;\n  transition: max-height 0.5s ease;\n}\n.sidebar-note-list-item.flash {\n  animation-name: flash;\n  animation-duration: 0.6s;\n}\n\n.sidebar-note-open {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  width: 100%;\n  z-index: 0;\n  border: none;\n  border-radius: 6px;\n  text-align: start;\n  background: var(--gray-95);\n  cursor: pointer;\n  outline-style: none;\n  color: transparent;\n  font-size: 0px;\n}\n.sidebar-note-open:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n.sidebar-note-open:hover {\n  background: var(--gray-90);\n}\n.sidebar-note-header {\n  z-index: 1;\n  max-width: 85%;\n  pointer-events: none;\n}\n.sidebar-note-header > strong {\n  display: block;\n  font-size: 1.25rem;\n  line-height: 1.2;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.sidebar-note-toggle-expand {\n  z-index: 2;\n  border-radius: 50%;\n  height: 24px;\n  border: 1px solid var(--gray-60);\n  cursor: pointer;\n  flex-shrink: 0;\n  visibility: hidden;\n  opacity: 0;\n  cursor: default;\n  transition: visibility 0s linear 20ms, opacity 300ms;\n  outline-style: none;\n}\n.sidebar-note-toggle-expand:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n.sidebar-note-open:hover + .sidebar-note-toggle-expand,\n.sidebar-note-open:focus + .sidebar-note-toggle-expand,\n.sidebar-note-toggle-expand:hover,\n.sidebar-note-toggle-expand:focus {\n  visibility: visible;\n  opacity: 1;\n  transition: visibility 0s linear 0s, opacity 300ms;\n}\n.sidebar-note-toggle-expand img {\n  width: 10px;\n  height: 10px;\n}\n\n.sidebar-note-excerpt {\n  pointer-events: none;\n  z-index: 2;\n  flex: 1 1 250px;\n  color: var(--secondary-text);\n  position: relative;\n  animation: slideIn 100ms;\n}\n\n.search input {\n  padding: 0 16px;\n  border-radius: 100px;\n  border: 1px solid var(--gray-90);\n  width: 100%;\n  height: 100%;\n  outline-style: none;\n}\n.search input:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n.search .spinner {\n  position: absolute;\n  right: 10px;\n  top: 10px;\n}\n\n.note-viewer {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.note {\n  background: var(--white);\n  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1), 0px 0px 1px rgba(0, 0, 0, 0.1);\n  border-radius: 8px;\n  height: 95%;\n  width: 95%;\n  min-width: 400px;\n  padding: 8%;\n  overflow-y: auto;\n}\n.note--empty-state {\n  margin-inline: 20px 20px;\n}\n.note-text--empty-state {\n  font-size: 1.5rem;\n}\n.note-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  flex-wrap: wrap-reverse;\n  margin-inline-start: -12px;\n}\n.note-menu {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  flex-grow: 1;\n}\n.note-title {\n  line-height: 1.3;\n  flex-grow: 1;\n  overflow-wrap: break-word;\n  margin-inline-start: 12px;\n}\n.note-updated-at {\n  color: var(--secondary-text);\n  white-space: nowrap;\n  margin-inline-start: 12px;\n}\n.note-preview {\n  margin-block-start: 50px;\n}\n\n.note-editor {\n  background: var(--white);\n  display: flex;\n  height: 100%;\n  width: 100%;\n  padding: 58px;\n  overflow-y: auto;\n}\n.note-editor .label {\n  margin-bottom: 20px;\n}\n.note-editor-form {\n  display: flex;\n  flex-direction: column;\n  width: 400px;\n  flex-shrink: 0;\n  position: sticky;\n  top: 0;\n}\n.note-editor-form input,\n.note-editor-form textarea {\n  background: none;\n  border: 1px solid var(--gray-70);\n  border-radius: 2px;\n  font-family: var(--monospace);\n  font-size: 0.8rem;\n  padding: 12px;\n  outline-style: none;\n}\n.note-editor-form input:focus,\n.note-editor-form textarea:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n.note-editor-form input {\n  height: 44px;\n  margin-bottom: 16px;\n}\n.note-editor-form textarea {\n  height: 100%;\n  max-width: 400px;\n}\n.note-editor-menu {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  margin-bottom: 12px;\n}\n.note-editor-preview {\n  margin-inline-start: 40px;\n  width: 100%;\n}\n.note-editor-done,\n.note-editor-delete {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  border-radius: 100px;\n  letter-spacing: 0.12em;\n  text-transform: uppercase;\n  padding: 6px 20px 8px;\n  cursor: pointer;\n  font-weight: 700;\n  margin-inline-start: 12px;\n  outline-style: none;\n  transition: all 0.2s ease-in-out;\n}\n.note-editor-done:disabled,\n.note-editor-delete:disabled {\n  opacity: 0.5;\n}\n.note-editor-done {\n  border: none;\n  background: var(--primary-blue);\n  color: var(--white);\n}\n.note-editor-done:focus {\n  box-shadow: var(--outline-box-shadow-contrast);\n}\n.note-editor-done:hover:not([disabled]) {\n  background: var(--secondary-blue);\n}\n.note-editor-delete {\n  border: 1px solid var(--red-25);\n  background: var(--white);\n  color: var(--red-25);\n}\n.note-editor-delete:focus {\n  box-shadow: var(--outline-box-shadow);\n}\n.note-editor-delete:hover:not([disabled]) {\n  background: var(--red-25);\n  color: var(--white);\n}\n/* Hack to color our svg */\n.note-editor-delete:hover:not([disabled]) img {\n  filter: grayscale(1) invert(1) brightness(2);\n}\n.note-editor-done > img {\n  width: 14px;\n}\n.note-editor-delete > img {\n  width: 10px;\n}\n.note-editor-done > img,\n.note-editor-delete > img {\n  margin-inline-end: 12px;\n}\n.note-editor-done[disabled],\n.note-editor-delete[disabled] {\n  opacity: 0.5;\n}\n\n.label {\n  display: inline-block;\n  border-radius: 100px;\n  letter-spacing: 0.05em;\n  text-transform: uppercase;\n  font-weight: 700;\n  padding: 4px 14px;\n}\n.label--preview {\n  background: rgba(38, 183, 255, 0.15);\n  color: var(--primary-blue);\n}\n\n.text-with-markdown p {\n  margin-bottom: 16px;\n}\n.text-with-markdown img {\n  width: 100%;\n}\n\n/* https://codepen.io/mandelid/pen/vwKoe */\n.spinner {\n  display: inline-block;\n  transition: opacity linear 0.1s 0.2s;\n  width: 20px;\n  height: 20px;\n  border: 3px solid rgba(80, 80, 80, 0.5);\n  border-radius: 50%;\n  border-top-color: #fff;\n  animation: spin 1s ease-in-out infinite;\n  opacity: 0;\n}\n.spinner--active {\n  opacity: 1;\n}\n\n.skeleton::after {\n  content: 'Loading...';\n}\n.skeleton {\n  height: 100%;\n  background-color: #eee;\n  background-image: linear-gradient(90deg, #eee, #f5f5f5, #eee);\n  background-size: 200px 100%;\n  background-repeat: no-repeat;\n  border-radius: 4px;\n  display: block;\n  line-height: 1;\n  width: 100%;\n  animation: shimmer 1.2s ease-in-out infinite;\n  color: transparent;\n}\n.skeleton:first-of-type {\n  margin: 0;\n}\n.skeleton--button {\n  border-radius: 100px;\n  padding: 6px 20px 8px;\n  width: auto;\n}\n.v-stack + .v-stack {\n  margin-block-start: 0.8em;\n}\n\n.offscreen {\n  border: 0;\n  clip: rect(0, 0, 0, 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  width: 1px;\n  position: absolute;\n}\n\n/* ---------------------------------------------------------------------------*/\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -200px 0;\n  }\n  100% {\n    background-position: calc(200px + 100%) 0;\n  }\n}\n\n@keyframes slideIn {\n  0% {\n    top: -10px;\n    opacity: 0;\n  }\n  100% {\n    top: 0;\n    opacity: 1;\n  }\n}\n\n@keyframes flash {\n  0% {\n    transform: scale(1);\n    opacity: 1;\n  }\n  50% {\n    transform: scale(1.05);\n    opacity: 0.9;\n  }\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/scripts/build.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use strict';\n\nconst path = require('path');\nconst rimraf = require('rimraf');\nconst webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');\n\nconst isProduction = process.env.NODE_ENV === 'production';\nrimraf.sync(path.resolve(__dirname, '../build'));\nwebpack(\n  {\n    mode: isProduction ? 'production' : 'development',\n    devtool: isProduction ? 'source-map' : 'cheap-module-source-map',\n    entry: [path.resolve(__dirname, '../src/framework/bootstrap.js')],\n    output: {\n      path: path.resolve(__dirname, '../build'),\n      filename: 'main.js',\n    },\n    module: {\n      rules: [\n        {\n          test: /\\.js$/,\n          use: 'babel-loader',\n          exclude: /node_modules/,\n        },\n      ],\n    },\n    plugins: [\n      new HtmlWebpackPlugin({\n        inject: true,\n        template: path.resolve(__dirname, '../public/index.html'),\n      }),\n      new ReactServerWebpackPlugin({isServer: false}),\n    ],\n  },\n  (err, stats) => {\n    if (err) {\n      console.error(err.stack || err);\n      if (err.details) {\n        console.error(err.details);\n      }\n      process.exit(1);\n      return;\n    }\n    const info = stats.toJson();\n    if (stats.hasErrors()) {\n      console.log('Finished running webpack with errors.');\n      info.errors.forEach((e) => console.error(e));\n      process.exit(1);\n    } else {\n      console.log('Finished running webpack.');\n    }\n  }\n);\n"
  },
  {
    "path": "chapter11/server-components-demo/scripts/init_db.sh",
    "content": "#!/bin/bash\nset -e\n\npsql -v ON_ERROR_STOP=1 --username \"$POSTGRES_USER\" --dbname \"$POSTGRES_DB\" <<-EOSQL\n  DROP TABLE IF EXISTS notes;\n  CREATE TABLE notes (\n    id SERIAL PRIMARY KEY,\n    created_at TIMESTAMP NOT NULL,\n    updated_at TIMESTAMP NOT NULL,\n    title TEXT,\n    body TEXT\n  );\nEOSQL\n"
  },
  {
    "path": "chapter11/server-components-demo/scripts/seed.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst {Pool} = require('pg');\nconst {readdir, unlink, writeFile} = require('fs/promises');\nconst startOfYear = require('date-fns/startOfYear');\nconst credentials = require('../credentials');\n\nconst NOTES_PATH = './notes';\nconst pool = new Pool(credentials);\n\nconst now = new Date();\nconst startOfThisYear = startOfYear(now);\n// Thanks, https://stackoverflow.com/a/9035732\nfunction randomDateBetween(start, end) {\n  return new Date(\n    start.getTime() + Math.random() * (end.getTime() - start.getTime())\n  );\n}\n\nconst dropTableStatement = 'DROP TABLE IF EXISTS notes;';\nconst createTableStatement = `CREATE TABLE notes (\n  id SERIAL PRIMARY KEY,\n  created_at TIMESTAMP NOT NULL,\n  updated_at TIMESTAMP NOT NULL,\n  title TEXT,\n  body TEXT\n);`;\nconst insertNoteStatement = `INSERT INTO notes(title, body, created_at, updated_at)\n  VALUES ($1, $2, $3, $3)\n  RETURNING *`;\nconst seedData = [\n  [\n    'Meeting Notes',\n    'This is an example note. It contains **Markdown**!',\n    randomDateBetween(startOfThisYear, now),\n  ],\n  [\n    'Make a thing',\n    `It's very easy to make some words **bold** and other words *italic* with\nMarkdown. You can even [link to React's website!](https://www.reactjs.org).`,\n    randomDateBetween(startOfThisYear, now),\n  ],\n  [\n    'A note with a very long title because sometimes you need more words',\n    `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing)\nnotes in this app! These note live on the server in the \\`notes\\` folder.\n\n![This app is powered by React](https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/React_Native_Logo.png/800px-React_Native_Logo.png)`,\n    randomDateBetween(startOfThisYear, now),\n  ],\n  ['I wrote this note today', 'It was an excellent note.', now],\n];\n\nasync function seed() {\n  await pool.query(dropTableStatement);\n  await pool.query(createTableStatement);\n  const res = await Promise.all(\n    seedData.map((row) => pool.query(insertNoteStatement, row))\n  );\n\n  const oldNotes = await readdir(path.resolve(NOTES_PATH));\n  await Promise.all(\n    oldNotes\n      .filter((filename) => filename.endsWith('.md'))\n      .map((filename) => unlink(path.resolve(NOTES_PATH, filename)))\n  );\n\n  await Promise.all(\n    res.map(({rows}) => {\n      const id = rows[0].id;\n      const content = rows[0].body;\n      const data = new Uint8Array(Buffer.from(content));\n      return writeFile(path.resolve(NOTES_PATH, `${id}.md`), data, (err) => {\n        if (err) {\n          throw err;\n        }\n      });\n    })\n  );\n}\n\nseed();\n"
  },
  {
    "path": "chapter11/server-components-demo/server/api.server.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use strict';\n\nconst register = require('react-server-dom-webpack/node-register');\nregister();\nconst babelRegister = require('@babel/register');\n\nbabelRegister({\n  ignore: [/[\\\\\\/](build|server|node_modules)[\\\\\\/]/],\n  presets: [['@babel/preset-react', {runtime: 'automatic'}]],\n  plugins: ['@babel/transform-modules-commonjs'],\n});\n\nconst express = require('express');\nconst compress = require('compression');\nconst {readFileSync} = require('fs');\nconst {unlink, writeFile} = require('fs').promises;\nconst {renderToPipeableStream} = require('react-server-dom-webpack/server');\nconst path = require('path');\nconst {Pool} = require('pg');\nconst React = require('react');\nconst ReactApp = require('../src/App').default;\n\n// Don't keep credentials in the source tree in a real app!\nconst pool = new Pool(require('../credentials'));\n\nconst PORT = process.env.PORT || 4000;\nconst app = express();\n\napp.use(compress());\napp.use(express.json());\n\napp\n  .listen(PORT, () => {\n    console.log(`React Notes listening at ${PORT}...`);\n  })\n  .on('error', function(error) {\n    if (error.syscall !== 'listen') {\n      throw error;\n    }\n    const isPipe = (portOrPipe) => Number.isNaN(portOrPipe);\n    const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;\n    switch (error.code) {\n      case 'EACCES':\n        console.error(bind + ' requires elevated privileges');\n        process.exit(1);\n        break;\n      case 'EADDRINUSE':\n        console.error(bind + ' is already in use');\n        process.exit(1);\n        break;\n      default:\n        throw error;\n    }\n  });\n\nfunction handleErrors(fn) {\n  return async function(req, res, next) {\n    try {\n      return await fn(req, res);\n    } catch (x) {\n      next(x);\n    }\n  };\n}\n\napp.get(\n  '/',\n  handleErrors(async function(_req, res) {\n    await waitForWebpack();\n    const html = readFileSync(\n      path.resolve(__dirname, '../build/index.html'),\n      'utf8'\n    );\n    // Note: this is sending an empty HTML shell, like a client-side-only app.\n    // However, the intended solution (which isn't built out yet) is to read\n    // from the Server endpoint and turn its response into an HTML stream.\n    res.send(html);\n  })\n);\n\nasync function renderReactTree(res, props) {\n  await waitForWebpack();\n  const manifest = readFileSync(\n    path.resolve(__dirname, '../build/react-client-manifest.json'),\n    'utf8'\n  );\n  const moduleMap = JSON.parse(manifest);\n  const {pipe} = renderToPipeableStream(\n    React.createElement(ReactApp, props),\n    moduleMap\n  );\n  pipe(res);\n}\n\nfunction sendResponse(req, res, redirectToId) {\n  const location = JSON.parse(req.query.location);\n  if (redirectToId) {\n    location.selectedId = redirectToId;\n  }\n  res.set('X-Location', JSON.stringify(location));\n  renderReactTree(res, {\n    selectedId: location.selectedId,\n    isEditing: location.isEditing,\n    searchText: location.searchText,\n  });\n}\n\napp.get('/react', function(req, res) {\n  sendResponse(req, res, null);\n});\n\nconst NOTES_PATH = path.resolve(__dirname, '../notes');\n\napp.post(\n  '/notes',\n  handleErrors(async function(req, res) {\n    const now = new Date();\n    const result = await pool.query(\n      'insert into notes (title, body, created_at, updated_at) values ($1, $2, $3, $3) returning id',\n      [req.body.title, req.body.body, now]\n    );\n    const insertedId = result.rows[0].id;\n    await writeFile(\n      path.resolve(NOTES_PATH, `${insertedId}.md`),\n      req.body.body,\n      'utf8'\n    );\n    sendResponse(req, res, insertedId);\n  })\n);\n\napp.put(\n  '/notes/:id',\n  handleErrors(async function(req, res) {\n    const now = new Date();\n    const updatedId = Number(req.params.id);\n    await pool.query(\n      'update notes set title = $1, body = $2, updated_at = $3 where id = $4',\n      [req.body.title, req.body.body, now, updatedId]\n    );\n    await writeFile(\n      path.resolve(NOTES_PATH, `${updatedId}.md`),\n      req.body.body,\n      'utf8'\n    );\n    sendResponse(req, res, null);\n  })\n);\n\napp.delete(\n  '/notes/:id',\n  handleErrors(async function(req, res) {\n    await pool.query('delete from notes where id = $1', [req.params.id]);\n    await unlink(path.resolve(NOTES_PATH, `${req.params.id}.md`));\n    sendResponse(req, res, null);\n  })\n);\n\napp.get(\n  '/notes',\n  handleErrors(async function(_req, res) {\n    const {rows} = await pool.query('select * from notes order by id desc');\n    res.json(rows);\n  })\n);\n\napp.get(\n  '/notes/:id',\n  handleErrors(async function(req, res) {\n    const {rows} = await pool.query('select * from notes where id = $1', [\n      req.params.id,\n    ]);\n    res.json(rows[0]);\n  })\n);\n\napp.get('/sleep/:ms', function(req, res) {\n  setTimeout(() => {\n    res.json({ok: true});\n  }, req.params.ms);\n});\n\napp.use(express.static('build'));\napp.use(express.static('public'));\n\nasync function waitForWebpack() {\n  while (true) {\n    try {\n      readFileSync(path.resolve(__dirname, '../build/index.html'));\n      return;\n    } catch (err) {\n      console.log(\n        'Could not find webpack build output. Will retry in a second...'\n      );\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n    }\n  }\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/server/package.json",
    "content": "{\n  \"type\": \"commonjs\",\n  \"main\": \"./api.server.js\"\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/App.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {Suspense} from 'react';\n\nimport Note from './Note';\nimport NoteList from './NoteList';\nimport EditButton from './EditButton';\nimport SearchField from './SearchField';\nimport NoteSkeleton from './NoteSkeleton';\nimport NoteListSkeleton from './NoteListSkeleton';\n\nexport default function App({selectedId, isEditing, searchText}) {\n  return (\n    <div className=\"main\">\n      <section className=\"col sidebar\">\n        <section className=\"sidebar-header\">\n          <img\n            className=\"logo\"\n            src=\"logo.svg\"\n            width=\"22px\"\n            height=\"20px\"\n            alt=\"\"\n            role=\"presentation\"\n          />\n          <strong>React Notes</strong>\n        </section>\n        <section className=\"sidebar-menu\" role=\"menubar\">\n          <SearchField />\n          <EditButton noteId={null}>New</EditButton>\n        </section>\n        <nav>\n          <Suspense fallback={<NoteListSkeleton />}>\n            <NoteList searchText={searchText} />\n          </Suspense>\n        </nav>\n      </section>\n      <section key={selectedId} className=\"col note-viewer\">\n        <Suspense fallback={<NoteSkeleton isEditing={isEditing} />}>\n          <Note selectedId={selectedId} isEditing={isEditing} />\n        </Suspense>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/EditButton.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use client';\n\nimport {useTransition} from 'react';\nimport {useRouter} from './framework/router';\n\nexport default function EditButton({noteId, children}) {\n  const [isPending, startTransition] = useTransition();\n  const {navigate} = useRouter();\n  const isDraft = noteId == null;\n  return (\n    <button\n      className={[\n        'edit-button',\n        isDraft ? 'edit-button--solid' : 'edit-button--outline',\n      ].join(' ')}\n      disabled={isPending}\n      onClick={() => {\n        startTransition(() => {\n          navigate({\n            selectedId: noteId,\n            isEditing: true,\n          });\n        });\n      }}\n      role=\"menuitem\">\n      {children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/Note.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {format} from 'date-fns';\n\n// Uncomment if you want to read from a file instead.\n// import {readFile} from 'fs/promises';\n// import {resolve} from 'path';\n\nimport NotePreview from './NotePreview';\nimport EditButton from './EditButton';\nimport NoteEditor from './NoteEditor';\n\nexport default async function Note({selectedId, isEditing}) {\n  if (selectedId === null) {\n    if (isEditing) {\n      return (\n        <NoteEditor noteId={null} initialTitle=\"Untitled\" initialBody=\"\" />\n      );\n    } else {\n      return (\n        <div className=\"note--empty-state\">\n          <span className=\"note-text--empty-state\">\n            Click a note on the left to view something! 🥺\n          </span>\n        </div>\n      );\n    }\n  }\n\n  const noteResponse = await fetch(`http://localhost:4000/notes/${selectedId}`);\n  const note = await noteResponse.json();\n\n  let {id, title, body, updated_at} = note;\n  const updatedAt = new Date(updated_at);\n\n  // We could also read from a file instead.\n  // body = await readFile(resolve(`./notes/${note.id}.md`), 'utf8');\n\n  // Now let's see how the Suspense boundary above lets us not block on this.\n  // await fetch('http://localhost:4000/sleep/3000');\n\n  if (isEditing) {\n    return <NoteEditor noteId={id} initialTitle={title} initialBody={body} />;\n  } else {\n    return (\n      <div className=\"note\">\n        <div className=\"note-header\">\n          <h1 className=\"note-title\">{title}</h1>\n          <div className=\"note-menu\" role=\"menubar\">\n            <small className=\"note-updated-at\" role=\"status\">\n              Last updated on {format(updatedAt, \"d MMM yyyy 'at' h:mm bb\")}\n            </small>\n            <EditButton noteId={id}>Edit</EditButton>\n          </div>\n        </div>\n        <NotePreview body={body} />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/NoteEditor.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use client';\n\nimport {useState, useTransition} from 'react';\nimport {useRouter, useMutation} from './framework/router';\n\nimport NotePreview from './NotePreview';\n\nexport default function NoteEditor({noteId, initialTitle, initialBody}) {\n  const [title, setTitle] = useState(initialTitle);\n  const [body, setBody] = useState(initialBody);\n  const {location} = useRouter();\n  const [isNavigating, startNavigating] = useTransition();\n  const [isSaving, saveNote] = useMutation({\n    endpoint: noteId !== null ? `/notes/${noteId}` : `/notes`,\n    method: noteId !== null ? 'PUT' : 'POST',\n  });\n  const [isDeleting, deleteNote] = useMutation({\n    endpoint: `/notes/${noteId}`,\n    method: 'DELETE',\n  });\n\n  async function handleSave() {\n    const payload = {title, body};\n    const requestedLocation = {\n      selectedId: noteId,\n      isEditing: false,\n      searchText: location.searchText,\n    };\n    await saveNote(payload, requestedLocation);\n  }\n\n  async function handleDelete() {\n    const payload = {};\n    const requestedLocation = {\n      selectedId: null,\n      isEditing: false,\n      searchText: location.searchText,\n    };\n    await deleteNote(payload, requestedLocation);\n  }\n\n  const isDraft = noteId === null;\n  return (\n    <div className=\"note-editor\">\n      <form\n        className=\"note-editor-form\"\n        autoComplete=\"off\"\n        onSubmit={(e) => e.preventDefault()}>\n        <label className=\"offscreen\" htmlFor=\"note-title-input\">\n          Enter a title for your note\n        </label>\n        <input\n          id=\"note-title-input\"\n          type=\"text\"\n          value={title}\n          onChange={(e) => {\n            setTitle(e.target.value);\n          }}\n        />\n        <label className=\"offscreen\" htmlFor=\"note-body-input\">\n          Enter the body for your note\n        </label>\n        <textarea\n          id=\"note-body-input\"\n          value={body}\n          onChange={(e) => {\n            setBody(e.target.value);\n          }}\n        />\n      </form>\n      <div className=\"note-editor-preview\">\n        <div className=\"note-editor-menu\" role=\"menubar\">\n          <button\n            className=\"note-editor-done\"\n            disabled={isSaving || isNavigating}\n            onClick={() => handleSave()}\n            role=\"menuitem\">\n            <img\n              src=\"checkmark.svg\"\n              width=\"14px\"\n              height=\"10px\"\n              alt=\"\"\n              role=\"presentation\"\n            />\n            Done\n          </button>\n          {!isDraft && (\n            <button\n              className=\"note-editor-delete\"\n              disabled={isDeleting || isNavigating}\n              onClick={() => handleDelete()}\n              role=\"menuitem\">\n              <img\n                src=\"cross.svg\"\n                width=\"10px\"\n                height=\"10px\"\n                alt=\"\"\n                role=\"presentation\"\n              />\n              Delete\n            </button>\n          )}\n        </div>\n        <div className=\"label label--preview\" role=\"status\">\n          Preview\n        </div>\n        <h1 className=\"note-title\">{title}</h1>\n        <NotePreview title={title} body={body} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/NoteList.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {db} from './db';\nimport SidebarNote from './SidebarNote';\n\nexport default async function NoteList({searchText}) {\n  // const notes = await (await fetch('http://localhost:4000/notes')).json();\n\n  // WARNING: This is for demo purposes only.\n  // We don't encourage this in real apps. There are far safer ways to access\n  // data in a real application!\n  const notes = (await db.query(\n    `select * from notes where title ilike $1 order by id desc`,\n    ['%' + searchText + '%']\n  )).rows;\n\n  // Now let's see how the Suspense boundary above lets us not block on this.\n  // await fetch('http://localhost:4000/sleep/3000');\n\n  return notes.length > 0 ? (\n    <ul className=\"notes-list\">\n      {notes.map((note) => (\n        <li key={note.id}>\n          <SidebarNote note={note} />\n        </li>\n      ))}\n    </ul>\n  ) : (\n    <div className=\"notes-empty\">\n      {searchText\n        ? `Couldn't find any notes titled \"${searchText}\".`\n        : 'No notes created yet!'}{' '}\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/NoteListSkeleton.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function NoteListSkeleton() {\n  return (\n    <div>\n      <ul className=\"notes-list skeleton-container\">\n        <li className=\"v-stack\">\n          <div\n            className=\"sidebar-note-list-item skeleton\"\n            style={{height: '5em'}}\n          />\n        </li>\n        <li className=\"v-stack\">\n          <div\n            className=\"sidebar-note-list-item skeleton\"\n            style={{height: '5em'}}\n          />\n        </li>\n        <li className=\"v-stack\">\n          <div\n            className=\"sidebar-note-list-item skeleton\"\n            style={{height: '5em'}}\n          />\n        </li>\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/NotePreview.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport TextWithMarkdown from './TextWithMarkdown';\n\nexport default function NotePreview({body}) {\n  return (\n    <div className=\"note-preview\">\n      <TextWithMarkdown text={body} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/NoteSkeleton.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function NoteSkeleton({isEditing}) {\n  return isEditing ? <NoteEditorSkeleton /> : <NotePreviewSkeleton />;\n}\n\nfunction NoteEditorSkeleton() {\n  return (\n    <div\n      className=\"note-editor skeleton-container\"\n      role=\"progressbar\"\n      aria-busy=\"true\">\n      <div className=\"note-editor-form\">\n        <div className=\"skeleton v-stack\" style={{height: '3rem'}} />\n        <div className=\"skeleton v-stack\" style={{height: '100%'}} />\n      </div>\n      <div className=\"note-editor-preview\">\n        <div className=\"note-editor-menu\">\n          <div\n            className=\"skeleton skeleton--button\"\n            style={{width: '8em', height: '2.5em'}}\n          />\n          <div\n            className=\"skeleton skeleton--button\"\n            style={{width: '8em', height: '2.5em', marginInline: '12px 0'}}\n          />\n        </div>\n        <div\n          className=\"note-title skeleton\"\n          style={{height: '3rem', width: '65%', marginInline: '12px 1em'}}\n        />\n        <div className=\"note-preview\">\n          <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n          <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n          <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n          <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n          <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction NotePreviewSkeleton() {\n  return (\n    <div\n      className=\"note skeleton-container\"\n      role=\"progressbar\"\n      aria-busy=\"true\">\n      <div className=\"note-header\">\n        <div\n          className=\"note-title skeleton\"\n          style={{height: '3rem', width: '65%', marginInline: '12px 1em'}}\n        />\n        <div\n          className=\"skeleton skeleton--button\"\n          style={{width: '8em', height: '2.5em'}}\n        />\n      </div>\n      <div className=\"note-preview\">\n        <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n        <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n        <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n        <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n        <div className=\"skeleton v-stack\" style={{height: '1.5em'}} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/SearchField.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use client';\n\nimport {useState, useTransition} from 'react';\nimport {useRouter} from './framework/router';\n\nimport Spinner from './Spinner';\n\nexport default function SearchField() {\n  const [text, setText] = useState('');\n  const [isSearching, startSearching] = useTransition();\n  const {navigate} = useRouter();\n  return (\n    <form className=\"search\" role=\"search\" onSubmit={(e) => e.preventDefault()}>\n      <label className=\"offscreen\" htmlFor=\"sidebar-search-input\">\n        Search for a note by title\n      </label>\n      <input\n        id=\"sidebar-search-input\"\n        placeholder=\"Search\"\n        value={text}\n        onChange={(e) => {\n          const newText = e.target.value;\n          setText(newText);\n          startSearching(() => {\n            navigate({\n              searchText: newText,\n            });\n          });\n        }}\n      />\n      <Spinner active={isSearching} />\n    </form>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/SidebarNote.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {format, isToday} from 'date-fns';\nimport excerpts from 'excerpts';\nimport {marked} from 'marked';\n\nimport SidebarNoteContent from './SidebarNoteContent';\n\nexport default function SidebarNote({note}) {\n  const updatedAt = new Date(note.updated_at);\n  const lastUpdatedAt = isToday(updatedAt)\n    ? format(updatedAt, 'h:mm bb')\n    : format(updatedAt, 'M/d/yy');\n  const summary = excerpts(marked(note.body), {words: 20});\n  return (\n    <SidebarNoteContent\n      id={note.id}\n      title={note.title}\n      expandedChildren={\n        <p className=\"sidebar-note-excerpt\">{summary || <i>(No content)</i>}</p>\n      }>\n      <header className=\"sidebar-note-header\">\n        <strong>{note.title}</strong>\n        <small>{lastUpdatedAt}</small>\n      </header>\n    </SidebarNoteContent>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/SidebarNoteContent.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use client';\n\nimport {useState, useRef, useEffect, useTransition} from 'react';\nimport {useRouter} from './framework/router';\n\nexport default function SidebarNoteContent({id, title, children, expandedChildren}) {\n  const {location, navigate} = useRouter();\n  const [isPending, startTransition] = useTransition();\n  const [isExpanded, setIsExpanded] = useState(false);\n  const isActive = id === location.selectedId;\n\n  // Animate after title is edited.\n  const itemRef = useRef(null);\n  const prevTitleRef = useRef(title);\n  useEffect(() => {\n    if (title !== prevTitleRef.current) {\n      prevTitleRef.current = title;\n      itemRef.current.classList.add('flash');\n    }\n  }, [title]);\n\n  return (\n    <div\n      ref={itemRef}\n      onAnimationEnd={() => {\n        itemRef.current.classList.remove('flash');\n      }}\n      className={[\n        'sidebar-note-list-item',\n        isExpanded ? 'note-expanded' : '',\n      ].join(' ')}>\n      {children}\n      <button\n        className=\"sidebar-note-open\"\n        style={{\n          backgroundColor: isPending\n            ? 'var(--gray-80)'\n            : isActive\n            ? 'var(--tertiary-blue)'\n            : '',\n          border: isActive\n            ? '1px solid var(--primary-border)'\n            : '1px solid transparent',\n        }}\n        onClick={() => {\n          startTransition(() => {\n            navigate({\n              selectedId: id,\n              isEditing: false,\n            });\n          });\n        }}>\n        Open note for preview\n      </button>\n      <button\n        className=\"sidebar-note-toggle-expand\"\n        onClick={(e) => {\n          e.stopPropagation();\n          setIsExpanded(!isExpanded);\n        }}>\n        {isExpanded ? (\n          <img\n            src=\"chevron-down.svg\"\n            width=\"10px\"\n            height=\"10px\"\n            alt=\"Collapse\"\n          />\n        ) : (\n          <img src=\"chevron-up.svg\" width=\"10px\" height=\"10px\" alt=\"Expand\" />\n        )}\n      </button>\n      {isExpanded && expandedChildren}\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/Spinner.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nexport default function Spinner({active = true}) {\n  return (\n    <div\n      className={['spinner', active && 'spinner--active'].join(' ')}\n      role=\"progressbar\"\n      aria-busy={active ? 'true' : 'false'}\n    />\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/TextWithMarkdown.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\nimport {marked} from 'marked';\nimport sanitizeHtml from 'sanitize-html';\n\nconst allowedTags = sanitizeHtml.defaults.allowedTags.concat([\n  'img',\n  'h1',\n  'h2',\n  'h3',\n]);\nconst allowedAttributes = Object.assign(\n  {},\n  sanitizeHtml.defaults.allowedAttributes,\n  {\n    img: ['alt', 'src'],\n  }\n);\n\nexport default function TextWithMarkdown({text}) {\n  return (\n    <div\n      className=\"text-with-markdown\"\n      dangerouslySetInnerHTML={{\n        __html: sanitizeHtml(marked(text), {\n          allowedTags,\n          allowedAttributes,\n        }),\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/db.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n// Error early if this is accidentally imported on the client.\nimport 'server-only';\n\nimport {Pool} from 'pg';\nimport credentials from '../credentials';\n\n// Don't keep credentials in the source tree in a real app!\nexport const db = new Pool(credentials);\n"
  },
  {
    "path": "chapter11/server-components-demo/src/framework/bootstrap.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n// ---------------------------------------------------------\n// Note: this code would usually be provided by a framework.\n// ---------------------------------------------------------\n\nimport {createRoot} from 'react-dom/client';\nimport {ErrorBoundary} from 'react-error-boundary';\nimport {Router} from './router'\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(<Root />);\n\nfunction Root() {\n  return (\n    <ErrorBoundary FallbackComponent={Error}>\n      <Router />\n    </ErrorBoundary>\n  );\n}\n\nfunction Error({error}) {\n  return (\n    <div>\n      <h1>Application Error</h1>\n      <pre style={{whiteSpace: 'pre-wrap'}}>{error.stack}</pre>\n    </div>\n  );\n}\n"
  },
  {
    "path": "chapter11/server-components-demo/src/framework/router.js",
    "content": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n */\n\n'use client';\n\n// ---------------------------------------------------------\n// Note: this code would usually be provided by a framework.\n// ---------------------------------------------------------\n\nimport {\n  createContext,\n  startTransition,\n  useContext,\n  useState,\n  use,\n} from 'react';\nimport {createFromFetch, createFromReadableStream} from 'react-server-dom-webpack/client';\n\nconst RouterContext = createContext();\nconst initialCache = new Map();\n\nexport function Router() {\n  const [cache, setCache] = useState(initialCache);\n  const [location, setLocation] = useState({\n    selectedId: null,\n    isEditing: false,\n    searchText: '',\n  });\n\n  const locationKey = JSON.stringify(location);\n  let content = cache.get(locationKey);\n  if (!content) {\n    content = createFromFetch(\n      fetch('/react?location=' + encodeURIComponent(locationKey))\n    );\n    cache.set(locationKey, content);\n  }\n\n  function refresh(response) {\n    startTransition(() => {\n      const nextCache = new Map();\n      if (response != null) {\n        const locationKey = response.headers.get('X-Location');\n        const nextLocation = JSON.parse(locationKey);\n        const nextContent = createFromReadableStream(response.body);\n        nextCache.set(locationKey, nextContent);\n        navigate(nextLocation);\n      }\n      setCache(nextCache);\n    })\n  }\n\n  function navigate(nextLocation) {\n    startTransition(() => {\n      setLocation(loc => ({\n        ...loc,\n        ...nextLocation\n      }));\n    });\n  }\n\n  return (\n    <RouterContext.Provider value={{location, navigate, refresh}}>\n      {use(content)}\n    </RouterContext.Provider>\n  );\n}\n\nexport function useRouter() {\n  return useContext(RouterContext);\n}\n\nexport function useMutation({endpoint, method}) {\n  const {refresh} = useRouter();\n  const [isSaving, setIsSaving] = useState(false);\n  const [didError, setDidError] = useState(false);\n  const [error, setError] = useState(null);\n  if (didError) {\n    // Let the nearest error boundary handle errors while saving.\n    throw error;\n  }\n\n  async function performMutation(payload, requestedLocation) {\n    setIsSaving(true);\n    try {\n      const response = await fetch(\n        `${endpoint}?location=${encodeURIComponent(\n          JSON.stringify(requestedLocation)\n        )}`,\n        {\n          method,\n          body: JSON.stringify(payload),\n          headers: {\n            'Content-Type': 'application/json',\n          },\n        }\n      );\n      if (!response.ok) {\n        throw new Error(await response.text());\n      }\n      refresh(response);\n    } catch (e) {\n      setDidError(true);\n      setError(e);\n    } finally {\n      setIsSaving(false);\n    }\n  }\n\n  return [isSaving, performMutation];\n}\n"
  },
  {
    "path": "chapter2/react/.eslintignore",
    "content": "build\nnode_modules"
  },
  {
    "path": "chapter2/react/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter2/react/.npmrc",
    "content": "auto-install-peers=true"
  },
  {
    "path": "chapter2/react/.prettierignore",
    "content": "build\nnode_modules"
  },
  {
    "path": "chapter2/react/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter2/react/README.md",
    "content": "# Chapter2\n\n챕터2 예제 코드\n"
  },
  {
    "path": "chapter2/react/package.json",
    "content": "{\n  \"name\": \"chapter2\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@babel/plugin-syntax-flow\": \"^7.14.5\",\n    \"@babel/plugin-transform-react-jsx\": \"^7.14.9\",\n    \"@types/node\": \"^16.11.46\",\n    \"@types/react\": \"^18.0.15\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-router-dom\": \"^6.3.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"typescript\": \"^4.7.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"eject\": \"react-scripts eject\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"eslint\": \"^8.38.0\",\n    \"prettier\": \"^2.8.7\"\n  }\n}\n"
  },
  {
    "path": "chapter2/react/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter2/react/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter2/react/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter2/react/src/App.tsx",
    "content": "import { Outlet, Link } from 'react-router-dom'\n\nexport default function App() {\n  return (\n    <div>\n      <h1>Chapter2 예제 코드</h1>\n      <nav\n        style={{\n          borderBottom: 'solid 1px',\n          paddingBottom: '1rem',\n          marginBottom: '1rem',\n        }}\n      >\n        <Link to=\"/1\">예제 2-1</Link> | <Link to=\"/4\">예제 2-4</Link> |\n        <Link to=\"/5\">예제 2-5</Link> | <Link to=\"/7\">예제 2-7</Link> |{' '}\n        <Link to=\"/8\">예제 2-8</Link> |\n      </nav>\n      <Outlet />\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter2/react/src/index.tsx",
    "content": "import { BrowserRouter, Routes, Route } from 'react-router-dom'\nimport ReactDOM from 'react-dom/client'\n\nimport App from './App'\nimport { Component1 } from './routes/2-1'\nimport SampleComponent from './routes/2-4'\nimport SampleComponent2 from './routes/2-5'\nimport CompareComponent from './routes/2-7'\nimport { ReactPureComponent } from './routes/2-8'\n\nconst root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)\n\nroot.render(\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"/\" element={<App />}>\n        <Route path=\"/1\" element={<Component1 />} />\n        <Route path=\"/4\" element={<SampleComponent required text=\"hello\" />} />\n        <Route path=\"/5\" element={<SampleComponent2 />} />\n        <Route path=\"/7\" element={<CompareComponent />} />\n        <Route path=\"/8\" element={<ReactPureComponent />} />\n      </Route>\n    </Routes>\n  </BrowserRouter>,\n)\n"
  },
  {
    "path": "chapter2/react/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "chapter2/react/src/routes/2-1.tsx",
    "content": "import { ReactNode } from 'react'\n\nfunction A({\n  children,\n  required,\n}: {\n  required?: boolean\n  children?: ReactNode\n}) {\n  return (\n    <>\n      <input type=\"text\" required={required} />\n      <div>{children}</div>\n    </>\n  )\n}\n\nfunction B({\n  text,\n  optionalChildren,\n}: {\n  text?: string\n  optionalChildren?: ReactNode\n}) {\n  return (\n    <>\n      <h1>{text}</h1>\n      {optionalChildren}\n    </>\n  )\n}\n\n// 하나의 요소로 구성된 가장 단순한 형태\nconst ComponentA = <A>안녕하세요.</A>\n\n// 자식이 없이 SelfClosingTag로 닫혀있는 형태도 가능하다.\nconst ComponentB = <A />\n\n// 옵션을 { } 와 전개 연산자로 넣을 수 있다.\nconst ComponentC = <A {...{ required: true }} />\n\n// 옵션명만 넣어도 가능하다.\nconst ComponentD = <A required />\n\n// 옵션명과 속성을 넣을 수 있다.\nconst ComponentE = <A required={false} />\n\nconst ComponentF = (\n  <A>\n    {/* 문자열은 쌍따옴표및 홀따옴표 모두 가능하다. */}\n    <B text=\"리액트\" />\n  </A>\n)\n\nconst ComponentG = (\n  <A>\n    {/* 옵선의 값으로 JSXElement를 넣는 것 또한 올바른 문법이다. */}\n    <B optionalChildren={<>안녕하세요.</>} />\n  </A>\n)\n\nconst ComponentH = (\n  <A>\n    {/* 여러개의 자식도 포함할 수 있다. */}\n    안녕하세요\n    <B text=\"리액트\" />\n  </A>\n)\n\nexport function Component1() {\n  return (\n    <>\n      <div>{ComponentA}</div>\n      <div>{ComponentB}</div>\n      <div>{ComponentC}</div>\n      <div>{ComponentD}</div>\n      <div>{ComponentE}</div>\n      <div>{ComponentF}</div>\n      <div>{ComponentG}</div>\n      <div>{ComponentH}</div>\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter2/react/src/routes/2-4.tsx",
    "content": "import React from 'react'\n\n// props 타입을 선언한다.\ninterface SampleProps {\n  required?: boolean\n  text: string\n}\n\n// state 타입을 선언한다.\ninterface SampleState {\n  count: number\n  isLimited?: boolean\n}\n\n// Component에 제네릭으로 props, state를 순서대로 넣어준다.\nclass SampleComponent extends React.Component<SampleProps, SampleState> {\n  // constructor에서 props를 넘겨주고, state의 기본값을 설정한다.\n  private constructor(props: SampleProps) {\n    super(props)\n    this.state = {\n      count: 0,\n      isLimited: false,\n    }\n  }\n\n  // 렌더 내부에서 쓰일 함수를 선언한다.\n  private handleClick = () => {\n    const newValue = this.state.count + 1\n    this.setState({ count: newValue, isLimited: newValue >= 10 })\n  }\n\n  // render에서 이 컴포넌트가 렌더링할 내용을 정의한다.\n  public render() {\n    // props와 state 값을 this, 즉 해당 클래스에서 꺼낸다.\n    const {\n      props: { required, text },\n      state: { count, isLimited },\n    } = this\n\n    return (\n      <h2>\n        Sample Component\n        <div>{required ? '필수' : '필수아님'}</div>\n        <div>문자: {text}</div>\n        <div>count: {count}</div>\n        <button onClick={this.handleClick} disabled={isLimited}>\n          증가\n        </button>\n      </h2>\n    )\n  }\n}\n\nexport default SampleComponent\n"
  },
  {
    "path": "chapter2/react/src/routes/2-5.tsx",
    "content": "import { Component } from 'react'\n\ntype Props = Record<string, never>\n\ninterface State {\n  count: number\n}\n\nclass SampleComponent extends Component<Props, State> {\n  private constructor(props: Props) {\n    super(props)\n    this.state = {\n      count: 1,\n    }\n    // handleClick의 this를 현재 클래스로 바인딩 시킨다.\n    this.handleClick = this.handleClick.bind(this)\n  }\n\n  private handleClick() {\n    this.setState((prev) => ({ count: prev.count + 1 }))\n  }\n\n  public render() {\n    const {\n      state: { count },\n    } = this\n    return (\n      <div>\n        <button onClick={this.handleClick}>증가</button>\n        {count}\n      </div>\n    )\n  }\n}\n\nexport default SampleComponent\n"
  },
  {
    "path": "chapter2/react/src/routes/2-7.tsx",
    "content": "import React from 'react'\n\ninterface State {\n  count: number\n}\n\ntype Props = Record<string, never>\n\nexport class ReactComponent extends React.Component<Props, State> {\n  private renderCounter = 0\n\n  private constructor(props: Props) {\n    super(props)\n    this.state = {\n      count: 1,\n    }\n  }\n\n  private handleClick = () => {\n    this.setState({ count: 1 })\n  }\n\n  public render() {\n    console.log('ReactComponent', ++this.renderCounter) // eslint-disable-line no-console\n    return (\n      <h1>\n        ReactComponent: {this.state.count}{' '}\n        <button onClick={this.handleClick}>+</button>\n      </h1>\n    )\n  }\n}\n\nexport class ReactPureComponent extends React.PureComponent<Props, State> {\n  private renderCounter = 0\n\n  private constructor(props: Props) {\n    super(props)\n    this.state = {\n      count: 1,\n    }\n  }\n\n  private handleClick = () => {\n    this.setState({ count: 1 })\n  }\n\n  public render() {\n    console.log('ReactPureComponent', ++this.renderCounter) // eslint-disable-line no-console\n    return (\n      <h1>\n        ReactPureComponent: {this.state.count}{' '}\n        <button onClick={this.handleClick}>+</button>\n      </h1>\n    )\n  }\n}\n\nexport default function CompareComponent() {\n  return (\n    <>\n      <h2>React.Component</h2>\n      <ReactComponent />\n      <h2>React.PureComponent</h2>\n      <ReactPureComponent />\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter2/react/src/routes/2-8.tsx",
    "content": "import React from 'react'\n\ninterface State {\n  count: number\n}\n\ntype Props = Record<string, never>\n\nexport class ReactPureComponent extends React.PureComponent<Props, State> {\n  private constructor(props: Props) {\n    super(props)\n    this.state = {\n      count: 1,\n    }\n  }\n\n  private handleClick = () => {\n    console.log('handleClick!') // eslint-disable-line no-console\n\n    this.setState({ count: 1 })\n  }\n\n  private handleChange = () => {\n    console.log('handleChanged!') // eslint-disable-line no-console\n  }\n\n  public render() {\n    return (\n      <>\n        <h1>\n          ReactPureComponent: {this.state.count}{' '}\n          <button onClick={this.handleClick}>+</button>\n        </h1>\n        <span>\n          위 빌드 결과가 알고 싶다면, 빌드를 수행한 뒤에 번들링 결과물에서\n          console.log 내용을 검색해보면 된다. 코드가 읽기 어렵다면 전체코드를\n          <a href=\"https://beautifier.io/\">여기</a> 에서 보기 좋게 포맷팅해보자.\n        </span>\n      </>\n    )\n  }\n}\n"
  },
  {
    "path": "chapter2/react/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "chapter4/next-example/.eslintignore",
    "content": ".next\nnode_modules\n"
  },
  {
    "path": "chapter4/next-example/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    'next/core-web-vitals',\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter4/next-example/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter4/next-example/.prettierignore",
    "content": ".next\nnode_modules\n"
  },
  {
    "path": "chapter4/next-example/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter4/next-example/README.md",
    "content": "# next-example\n\n## Introduction\n\n`create-next-app`으로 만들어진 예제 애플리케이션 입니다.\n"
  },
  {
    "path": "chapter4/next-example/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter4/next-example/package.json",
    "content": "{\n  \"name\": \"my-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"dependencies\": {\n    \"@next/font\": \"13.1.6\",\n    \"@types/node\": \"18.13.0\",\n    \"@types/react\": \"18.0.28\",\n    \"@types/react-dom\": \"18.0.11\",\n    \"next\": \"13.1.6\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"typescript\": \"4.9.5\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"eslint\": \"^8.38.0\",\n    \"eslint-config-next\": \"13.1.6\",\n    \"prettier\": \"^2.8.7\"\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/404.tsx",
    "content": "import { useCallback } from 'react'\n\nexport default function My404Page() {\n  const handleClick = useCallback(() => {\n    console.log('hi') // eslint-disable-line no-console\n  }, [])\n  return (\n    <h1>\n      페이지를 찾을 수 없습니다. <button onClick={handleClick}>클릭</button>\n    </h1>\n  )\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/500.tsx",
    "content": "import { useCallback } from 'react'\n\nexport default function My500Page() {\n  const handleClick = useCallback(() => {\n    console.log('hi') // eslint-disable-line no-console\n  }, [])\n\n  return (\n    <h1>\n      (500페이지) 서버에서 에러가 발생했습니다.{' '}\n      <button onClick={handleClick}>클릭</button>\n    </h1>\n  )\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/_app.tsx",
    "content": "import type { AppProps } from 'next/app'\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/_document.tsx",
    "content": "import { Html, Head, Main, NextScript } from 'next/document'\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head />\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  )\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/_error.tsx",
    "content": "import { NextPageContext } from 'next'\n\nfunction Error({ statusCode }: { statusCode: number }) {\n  return (\n    <>\n      {statusCode ? `서버에서 ${statusCode}` : '클라이언트에서'} 에러가\n      발생했습니다.\n    </>\n  )\n}\n\nError.getInitialProps = ({ res, err }: NextPageContext) => {\n  const statusCode = res ? res.statusCode : err ? err.statusCode : ''\n  return { statusCode }\n}\n\nexport default Error\n"
  },
  {
    "path": "chapter4/next-example/src/pages/api/hello.ts",
    "content": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiResponse } from 'next'\n\ninterface Data {\n  name: string\n}\n\nexport default function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<Data>,\n) {\n  res.status(200).json({ name: 'John Doe' })\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/hello/[greeting].tsx",
    "content": "import { NextPageContext } from 'next'\n\nexport default function HelloGreeting({ greeting }: { greeting: string }) {\n  return <div>hello {greeting}</div>\n}\n\nexport const getServerSideProps = (context: NextPageContext) => {\n  const {\n    query: { greeting },\n  } = context\n\n  return {\n    props: {\n      greeting,\n    },\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/hello/world.tsx",
    "content": "export default function HelloWorld() {\n  return <>hello world</>\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/hello.tsx",
    "content": "export default function Hello() {\n  console.log(typeof window === 'undefined' ? '서버' : '클라이언트') // eslint-disable-line no-console\n  return <>hello</>\n}\n\nexport const getServerSideProps = () => {\n  return {\n    props: {},\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/hi/[...props].tsx",
    "content": "import { useRouter } from 'next/router'\nimport { useEffect } from 'react'\nimport { NextPageContext } from 'next'\n\nexport default function HiAll({ props: serverProps }: { props: string[] }) {\n  const {\n    query: { props },\n  } = useRouter()\n\n  useEffect(() => {\n    /* eslint-disable no-console */\n    console.log(props)\n    console.log(JSON.stringify(props) === JSON.stringify(serverProps)) // true\n    /* eslint-enable no-console */\n  }, [props, serverProps])\n\n  return (\n    <>\n      hi{' '}\n      <ul>\n        {serverProps.map((item) => (\n          <li key={item}>{item}</li>\n        ))}\n      </ul>\n    </>\n  )\n}\n\nexport const getServerSideProps = (context: NextPageContext) => {\n  const {\n    query: { props },\n  } = context\n\n  return {\n    props: {\n      props,\n    },\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/pages/index.tsx",
    "content": "import type { NextPage } from 'next'\nimport Link from 'next/link'\n\nconst Home: NextPage = () => {\n  return (\n    <ul>\n      <li>\n        {/* next의 eslint 룰을 잠시 끄기 위해 추가했다. */}\n        {/* eslint-disable-next-line */}\n        <a href=\"/hello\">A 태그로 이동</a>\n      </li>\n      <li>\n        {/* 차이를 극적으로 보여주기 위해 해당 페이지의 리소스를 미리 가져오는 prefetch를 잠시 꺼두었다. */}\n        <Link prefetch={false} href=\"/hello\">\n          next/link로 이동\n        </Link>\n      </li>\n    </ul>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "chapter4/next-example/src/pages/todo/[id].tsx",
    "content": "import Link from 'next/link'\nimport { NextPageContext } from 'next'\n\nexport default function Todo({\n  todo,\n}: {\n  todo: { userId: number; id: number; title: string; completed: boolean }\n}) {\n  return (\n    <>\n      <h1>{todo.title}</h1>\n      <ul>\n        <li>\n          <Link href=\"/todo/1\">1번</Link>\n        </li>\n\n        <li>\n          <Link href=\"/todo/2\">2번</Link>\n        </li>\n\n        <li>\n          <Link href=\"/todo/3\">3번</Link>\n        </li>\n      </ul>\n    </>\n  )\n}\n\nTodo.getInitialProps = async (ctx: NextPageContext) => {\n  const {\n    query: { id = '' },\n    // asPath,\n    // query,\n    // res,\n  } = ctx\n  const response = await fetch(\n    `https://jsonplaceholder.typicode.com/todos/${id}`,\n  )\n  const result = await response.json()\n  return { todo: result }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/styles/Home.module.css",
    "content": ".main {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  align-items: center;\n  padding: 6rem;\n  min-height: 100vh;\n}\n\n.description {\n  display: inherit;\n  justify-content: inherit;\n  align-items: inherit;\n  font-size: 0.85rem;\n  max-width: var(--max-width);\n  width: 100%;\n  z-index: 2;\n  font-family: var(--font-mono);\n}\n\n.description a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n.description p {\n  position: relative;\n  margin: 0;\n  padding: 1rem;\n  background-color: rgba(var(--callout-rgb), 0.5);\n  border: 1px solid rgba(var(--callout-border-rgb), 0.3);\n  border-radius: var(--border-radius);\n}\n\n.code {\n  font-weight: 700;\n  font-family: var(--font-mono);\n}\n\n.grid {\n  display: grid;\n  grid-template-columns: repeat(4, minmax(25%, auto));\n  width: var(--max-width);\n  max-width: 100%;\n}\n\n.card {\n  padding: 1rem 1.2rem;\n  border-radius: var(--border-radius);\n  background: rgba(var(--card-rgb), 0);\n  border: 1px solid rgba(var(--card-border-rgb), 0);\n  transition: background 200ms, border 200ms;\n}\n\n.card span {\n  display: inline-block;\n  transition: transform 200ms;\n}\n\n.card h2 {\n  font-weight: 600;\n  margin-bottom: 0.7rem;\n}\n\n.card p {\n  margin: 0;\n  opacity: 0.6;\n  font-size: 0.9rem;\n  line-height: 1.5;\n  max-width: 30ch;\n}\n\n.center {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  position: relative;\n  padding: 4rem 0;\n}\n\n.center::before {\n  background: var(--secondary-glow);\n  border-radius: 50%;\n  width: 480px;\n  height: 360px;\n  margin-left: -400px;\n}\n\n.center::after {\n  background: var(--primary-glow);\n  width: 240px;\n  height: 180px;\n  z-index: -1;\n}\n\n.center::before,\n.center::after {\n  content: '';\n  left: 50%;\n  position: absolute;\n  filter: blur(45px);\n  transform: translateZ(0);\n}\n\n.logo,\n.thirteen {\n  position: relative;\n}\n\n.thirteen {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 75px;\n  height: 75px;\n  padding: 25px 10px;\n  margin-left: 16px;\n  transform: translateZ(0);\n  border-radius: var(--border-radius);\n  overflow: hidden;\n  box-shadow: 0px 2px 8px -1px #0000001a;\n}\n\n.thirteen::before,\n.thirteen::after {\n  content: '';\n  position: absolute;\n  z-index: -1;\n}\n\n/* Conic Gradient Animation */\n.thirteen::before {\n  animation: 6s rotate linear infinite;\n  width: 200%;\n  height: 200%;\n  background: var(--tile-border);\n}\n\n/* Inner Square */\n.thirteen::after {\n  inset: 0;\n  padding: 1px;\n  border-radius: var(--border-radius);\n  background: linear-gradient(\n    to bottom right,\n    rgba(var(--tile-start-rgb), 1),\n    rgba(var(--tile-end-rgb), 1)\n  );\n  background-clip: content-box;\n}\n\n/* Enable hover only on non-touch devices */\n@media (hover: hover) and (pointer: fine) {\n  .card:hover {\n    background: rgba(var(--card-rgb), 0.1);\n    border: 1px solid rgba(var(--card-border-rgb), 0.15);\n  }\n\n  .card:hover span {\n    transform: translateX(4px);\n  }\n}\n\n@media (prefers-reduced-motion) {\n  .thirteen::before {\n    animation: none;\n  }\n\n  .card:hover span {\n    transform: none;\n  }\n}\n\n/* Mobile */\n@media (max-width: 700px) {\n  .content {\n    padding: 4rem;\n  }\n\n  .grid {\n    grid-template-columns: 1fr;\n    margin-bottom: 120px;\n    max-width: 320px;\n    text-align: center;\n  }\n\n  .card {\n    padding: 1rem 2.5rem;\n  }\n\n  .card h2 {\n    margin-bottom: 0.5rem;\n  }\n\n  .center {\n    padding: 8rem 0 6rem;\n  }\n\n  .center::before {\n    transform: none;\n    height: 300px;\n  }\n\n  .description {\n    font-size: 0.8rem;\n  }\n\n  .description a {\n    padding: 1rem;\n  }\n\n  .description p,\n  .description div {\n    display: flex;\n    justify-content: center;\n    position: fixed;\n    width: 100%;\n  }\n\n  .description p {\n    align-items: center;\n    inset: 0 0 auto;\n    padding: 2rem 1rem 1.4rem;\n    border-radius: 0;\n    border: none;\n    border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);\n    background: linear-gradient(\n      to bottom,\n      rgba(var(--background-start-rgb), 1),\n      rgba(var(--callout-rgb), 0.5)\n    );\n    background-clip: padding-box;\n    backdrop-filter: blur(24px);\n  }\n\n  .description div {\n    align-items: flex-end;\n    pointer-events: none;\n    inset: auto 0 0;\n    padding: 2rem;\n    height: 200px;\n    background: linear-gradient(\n      to bottom,\n      transparent 0%,\n      rgb(var(--background-end-rgb)) 40%\n    );\n    z-index: 1;\n  }\n}\n\n/* Tablet and Smaller Desktop */\n@media (min-width: 701px) and (max-width: 1120px) {\n  .grid {\n    grid-template-columns: repeat(2, 50%);\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .vercelLogo {\n    filter: invert(1);\n  }\n\n  .logo,\n  .thirteen img {\n    filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);\n  }\n}\n\n@keyframes rotate {\n  from {\n    transform: rotate(360deg);\n  }\n  to {\n    transform: rotate(0deg);\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/src/styles/globals.css",
    "content": ":root {\n  --max-width: 1100px;\n  --border-radius: 12px;\n  --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',\n    'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',\n    'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;\n\n  --foreground-rgb: 0, 0, 0;\n  --background-start-rgb: 214, 219, 220;\n  --background-end-rgb: 255, 255, 255;\n\n  --primary-glow: conic-gradient(\n    from 180deg at 50% 50%,\n    #16abff33 0deg,\n    #0885ff33 55deg,\n    #54d6ff33 120deg,\n    #0071ff33 160deg,\n    transparent 360deg\n  );\n  --secondary-glow: radial-gradient(\n    rgba(255, 255, 255, 1),\n    rgba(255, 255, 255, 0)\n  );\n\n  --tile-start-rgb: 239, 245, 249;\n  --tile-end-rgb: 228, 232, 233;\n  --tile-border: conic-gradient(\n    #00000080,\n    #00000040,\n    #00000030,\n    #00000020,\n    #00000010,\n    #00000010,\n    #00000080\n  );\n\n  --callout-rgb: 238, 240, 241;\n  --callout-border-rgb: 172, 175, 176;\n  --card-rgb: 180, 185, 188;\n  --card-border-rgb: 131, 134, 135;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --foreground-rgb: 255, 255, 255;\n    --background-start-rgb: 0, 0, 0;\n    --background-end-rgb: 0, 0, 0;\n\n    --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));\n    --secondary-glow: linear-gradient(\n      to bottom right,\n      rgba(1, 65, 255, 0),\n      rgba(1, 65, 255, 0),\n      rgba(1, 65, 255, 0.3)\n    );\n\n    --tile-start-rgb: 2, 13, 46;\n    --tile-end-rgb: 2, 5, 19;\n    --tile-border: conic-gradient(\n      #ffffff80,\n      #ffffff40,\n      #ffffff30,\n      #ffffff20,\n      #ffffff10,\n      #ffffff10,\n      #ffffff80\n    );\n\n    --callout-rgb: 20, 20, 20;\n    --callout-border-rgb: 108, 108, 108;\n    --card-rgb: 100, 100, 100;\n    --card-border-rgb: 200, 200, 200;\n  }\n}\n\n* {\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0;\n}\n\nhtml,\nbody {\n  max-width: 100vw;\n  overflow-x: hidden;\n}\n\nbody {\n  color: rgb(var(--foreground-rgb));\n  background: linear-gradient(\n      to bottom,\n      transparent,\n      rgb(var(--background-end-rgb))\n    )\n    rgb(var(--background-start-rgb));\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n}\n"
  },
  {
    "path": "chapter4/next-example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter4/ssr-example/.eslintignore",
    "content": "dist/"
  },
  {
    "path": "chapter4/ssr-example/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter4/ssr-example/.prettierignore",
    "content": "public/\ndist/\n\n.eslintrc.js\n.prettierrc\n.eslintignore\n.prettierignore"
  },
  {
    "path": "chapter4/ssr-example/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter4/ssr-example/README.md",
    "content": "# SSR-Example\n\n## Introduction\n\n`react-dom`의 `renderToString`과 `renderToNodeStream`을 활용하여 리액트 서버사이드 애플리케이션을 간단하게 만들어보았습니다. 학습용으로 제작되어 많은 부분이 생략되었으며, 절대로 프로덕션 서비스에 사용해서는 안됩니다.\n\n## How to Start\n\n1. `npm install`\n2. `npm run build`\n3. `npm run start`\n4. 다음 페이지에서 서버사이드 렌더링을 확인해보세요.\n   - http://localhost:3000/\n   - http://localhost:3000/stream\n"
  },
  {
    "path": "chapter4/ssr-example/checkStream.js",
    "content": "// 이 코드는 브라우저에서만 실행된다.\n\n/* eslint-disable no-console */\nconst main = async () => {\n  // chrome에서 발생한 네트워크 요청을 복사해서 가져왔다.\n  const response = await fetch('http://localhost:3000/stream')\n  const reader = response.body.getReader()\n\n  while (true) {\n    const { value, done } = await reader.read()\n    const str = new TextDecoder().decode(value)\n    if (done) {\n      break\n    }\n    console.log(`===================================`)\n    console.log(str)\n  }\n\n  console.log('Response fully received')\n}\n\nmain()\n"
  },
  {
    "path": "chapter4/ssr-example/package.json",
    "content": "{\n  \"name\": \"ssr-example\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"node dist/server.js\",\n    \"build\": \"webpack --mode production\",\n    \"build:dev\": \"webpack --mode development --watch\",\n    \"lint\": \"eslint '**/*.{js,ts,tsx}'\",\n    \"lint:fix\": \"npm run lint -- --fix\",\n    \"prettier\": \"prettier '**/*' --check\",\n    \"prettier:fix\": \"prettier '**/*' --write\"\n  },\n  \"author\": \"yceffort <yceffort@gmail.com>\",\n  \"dependencies\": {\n    \"@types/isomorphic-fetch\": \"^0.0.36\",\n    \"@types/node\": \"^16\",\n    \"@types/react\": \"^17.0.8\",\n    \"@types/react-dom\": \"^17.0.5\",\n    \"@types/webpack\": \"^5.28.0\",\n    \"concurrently\": \"^7.3.0\",\n    \"isomorphic-fetch\": \"^3.0.0\",\n    \"raw-loader\": \"^4.0.2\",\n    \"react\": \"^17.0.0\",\n    \"react-dom\": \"^17.0.0\",\n    \"source-map-loader\": \"^4.0.0\",\n    \"ts-loader\": \"^9.3.1\",\n    \"typescript\": \"^4.5.5\",\n    \"webpack\": \"^5.74.0\",\n    \"webpack-cli\": \"^4.10.0\",\n    \"webpack-node-externals\": \"^3.0.0\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"eslint\": \"^8.38.0\",\n    \"prettier\": \"^2.8.7\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "chapter4/ssr-example/public/index-end.html",
    "content": "<script src=\"https://unpkg.com/react@17.0.2/umd/react.development.js\"></script>\n<script src=\"https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js\"></script>\n<script src=\"/browser.js\"></script>\n</body>\n</html>"
  },
  {
    "path": "chapter4/ssr-example/public/index-front.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>SSR Example</title>\n  </head>\n  <body>\n\n"
  },
  {
    "path": "chapter4/ssr-example/public/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>SSR Example</title>\n  </head>\n  <body>\n    __placeholder__\n    <script src=\"https://unpkg.com/react@17.0.2/umd/react.development.js\"></script>\n    <script src=\"https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js\"></script>\n    <script src=\"/browser.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chapter4/ssr-example/src/components/App.tsx",
    "content": "import React, { useEffect } from 'react'\n\nimport { TodoResponse } from '../fetch'\n\nimport { Todo } from './Todo'\n\nexport default function App({ todos }: { todos: Array<TodoResponse> }) {\n  useEffect(() => {\n    console.log('하이!') // eslint-disable-line no-console\n  }, [])\n\n  return (\n    <>\n      <h1>나의 할일!</h1>\n      <ul>\n        {todos.map((todo, index) => (\n          <Todo key={index} todo={todo} />\n        ))}\n      </ul>\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter4/ssr-example/src/components/Todo.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { TodoResponse } from '../fetch'\n\nexport function Todo({ todo }: { todo: TodoResponse }) {\n  const { title, completed, userId, id } = todo\n  const [finished, setFinished] = useState(completed)\n\n  function handleClick() {\n    setFinished((prev) => !prev)\n  }\n\n  return (\n    <li>\n      <span>\n        {userId}-{id}) {title} {finished ? '완료' : '미완료'}\n        <button onClick={handleClick}>토글</button>\n      </span>\n    </li>\n  )\n}\n"
  },
  {
    "path": "chapter4/ssr-example/src/fetch/index.ts",
    "content": "import fetch from 'isomorphic-fetch'\n\nexport interface TodoResponse {\n  userId: number\n  id: number\n  title: string\n  completed: boolean\n}\n\nexport async function fetchTodo() {\n  const response = await fetch('https://jsonplaceholder.typicode.com/todos')\n  const result: TodoResponse[] = await response.json()\n  return result\n  // 스트림의 극단적인 예제를 보고 싶다면 주석 해제\n  // return Array(10).fill(result).flat()\n}\n"
  },
  {
    "path": "chapter4/ssr-example/src/index.tsx",
    "content": "import React from 'react'\nimport { hydrate } from 'react-dom'\n\nimport App from './components/App'\nimport { fetchTodo } from './fetch'\n\nasync function main() {\n  const result = await fetchTodo()\n\n  const app = <App todos={result} />\n  const el = document.getElementById('root')\n\n  hydrate(app, el)\n}\n\nmain()\n"
  },
  {
    "path": "chapter4/ssr-example/src/server.ts",
    "content": "import { createServer, IncomingMessage, ServerResponse } from 'http'\nimport { createReadStream } from 'fs'\n\nimport { renderToNodeStream, renderToString } from 'react-dom/server'\nimport { createElement } from 'react'\n\nimport html from '../public/index.html'\nimport indexFront from '../public/index-front.html'\nimport indexEnd from '../public/index-end.html'\n\nimport App from './components/App'\nimport { fetchTodo } from './fetch'\n\nconst PORT = process.env.PORT || 3000\n\nasync function serverHandler(req: IncomingMessage, res: ServerResponse) {\n  const { url } = req\n\n  switch (url) {\n    case '/': {\n      const result = await fetchTodo()\n\n      const rootElement = createElement(\n        'div',\n        { id: 'root' },\n        createElement(App, { todos: result }),\n      )\n      const renderResult = renderToString(rootElement)\n\n      const htmlResult = html.replace('__placeholder__', renderResult)\n\n      res.setHeader('Content-Type', 'text/html')\n      res.write(htmlResult)\n      res.end()\n      return\n    }\n\n    case '/stream': {\n      res.setHeader('Content-Type', 'text/html')\n      res.write(indexFront)\n\n      const result = await fetchTodo()\n      const rootElement = createElement(\n        'div',\n        { id: 'root' },\n        createElement(App, { todos: result }),\n      )\n\n      const stream = renderToNodeStream(rootElement)\n      stream.pipe(res, { end: false })\n      stream.on('end', () => {\n        res.write(indexEnd)\n        res.end()\n      })\n      return\n    }\n\n    case '/browser.js': {\n      res.setHeader('Content-Type', 'application/javascript')\n      createReadStream(`./dist/browser.js`).pipe(res)\n      return\n    }\n\n    case '/browser.js.map': {\n      res.setHeader('Content-Type', 'application/javascript')\n      createReadStream(`./dist/browser.js.map`).pipe(res)\n      return\n    }\n\n    default: {\n      res.statusCode = 404\n      res.end('404 Not Found')\n    }\n  }\n}\n\nfunction main() {\n  createServer(serverHandler).listen(PORT, () => {\n    console.log(`Server has been started ${PORT}...`) // eslint-disable-line no-console\n  })\n}\n\nmain()\n"
  },
  {
    "path": "chapter4/ssr-example/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"module\": \"commonjs\",\n    \"target\": \"ES2022\",\n    \"moduleResolution\": \"node\",\n    \"jsx\": \"react\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"noEmitOnError\": true,\n    \"esModuleInterop\": true\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter4/ssr-example/typings.d.ts",
    "content": "declare module '*.html' {\n  const content: string\n  export default content\n}\n"
  },
  {
    "path": "chapter4/ssr-example/watch-stream.js",
    "content": "// fetch가 기본 제공 되는 node 19버전 부터 사용가능하다.\n// 만약 이하 버전에서 사용하고 싶다면 `node-fetch`를 사용하자.\n// const fetch = require('node-fetch')\n;(async () => {\n  const response = await fetch('http://localhost:3000')\n\n  try {\n    for await (const chunk of response.body) {\n      // eslint-disable-next-line no-console\n      console.log('------chunk-----')\n      // eslint-disable-next-line no-console\n      console.log(Buffer.from(chunk).toString())\n    }\n  } catch (err) {\n    // eslint-disable-next-line no-console\n    console.error(err.stack)\n  }\n})()\n"
  },
  {
    "path": "chapter4/ssr-example/webpack.config.js",
    "content": "// @ts-check\n/** @typedef {import('webpack').Configuration} WebpackConfig **/\nconst path = require('path')\n\nconst nodeExternals = require('webpack-node-externals')\n\n/** @type WebpackConfig[] */\nconst configs = [\n  {\n    entry: {\n      browser: './src/index.tsx',\n    },\n    output: {\n      path: path.join(__dirname, '/dist'),\n      filename: '[name].js',\n    },\n    resolve: {\n      extensions: ['.ts', '.tsx'],\n    },\n    devtool: 'source-map',\n    module: {\n      rules: [\n        {\n          test: /\\.tsx?$/,\n          loader: 'ts-loader',\n        },\n      ],\n    },\n    externals: {\n      react: 'React',\n      'react-dom': 'ReactDOM',\n    },\n  },\n  {\n    entry: {\n      server: './src/server.ts',\n    },\n    output: {\n      path: path.join(__dirname, '/dist'),\n      filename: '[name].js',\n    },\n    resolve: {\n      extensions: ['.ts', '.tsx'],\n    },\n    devtool: 'source-map',\n    module: {\n      rules: [\n        {\n          test: /\\.tsx?$/,\n          loader: 'ts-loader',\n        },\n        {\n          test: /\\.html$/,\n          use: 'raw-loader',\n        },\n      ],\n    },\n    target: 'node',\n    externals: [nodeExternals()],\n  },\n]\n\nmodule.exports = configs\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/.eslintrc.js",
    "content": "\"use strict\";\n\nmodule.exports = {\n  root: true,\n  extends: [\n    \"@titicaca/eslint-config-triple\",\n    \"@titicaca/eslint-config-triple/frontend\",\n    \"@titicaca/eslint-config-triple/prettier\"\n  ],\n  env: {\n    node: true,\n  },\n  overrides: [\n    {\n      files: [\"tests/**/*.js\"],\n      env: { mocha: true },\n    },\n  ],\n};\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/.npmrc",
    "content": "registry=https://registry.npmjs.org/"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/README.md",
    "content": "# eslint-plugin-yceffort\n\nyceffort\n\n## Installation\n\nYou'll first need to install [ESLint](https://eslint.org/):\n\n```sh\nnpm i eslint --save-dev\n```\n\nNext, install `eslint-plugin-yceffort`:\n\n```sh\nnpm install eslint-plugin-yceffort --save-dev\n```\n\n## Usage\n\nAdd `yceffort` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:\n\n```json\n{\n    \"plugins\": [\n        \"yceffort\"\n    ]\n}\n```\n\n\nThen configure the rules you want to use under the rules section.\n\n```json\n{\n    \"rules\": {\n        \"yceffort/rule-name\": 2\n    }\n}\n```\n\n## Supported Rules\n\n* Fill in provided rules here\n\n\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/docs/rules/no-new-date.md",
    "content": "# yceffort (no-new-date)\n\n기기 시간에 의존적인 `new Date()`를 사용하지 마세요. 대신 `ServerDate()` 함수를 만들어 사용하세요.\n\n## Rule Details\n\nThis rule aims to...\n\nExamples of **incorrect** code for this rule:\n\n```js\n// ❌\nnew Date()\n```\n\nExamples of **correct** code for this rule:\n\n```js\n// 👌\nnew Date('2022-01-01')\n// 👌\nServerDate()\n```\n\n## When Not To Use It\n\n- `ServerDate()` 함수가 없는 경우\n- 기기 시간에 의존해도 상관없는 경우\n\n## Further Reading\n\nhttps://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/lib/index.js",
    "content": "/**\n * @fileoverview yceffort\n * @author yceffort\n */\n\n\"use strict\";\nconst path = require('path')\n\n// ------------------------------------------------------------------------------\n// Requirements\n// ------------------------------------------------------------------------------\n\nconst requireIndex = require(\"requireindex\");\n\n// ------------------------------------------------------------------------------\n// Plugin Definition\n// ------------------------------------------------------------------------------\n\n\n// import all rules in lib/rules\nmodule.exports.rules = requireIndex(path.resolve(__dirname, \"/rules\"));\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/lib/rules/no-new-date.js",
    "content": "/**\n * @fileoverview yceffort\n * @author yceffort\n */\n\"use strict\";\n\n// ------------------------------------------------------------------------------\n// Rule Definition\n// ------------------------------------------------------------------------------\n\n/**\n *\n * @type {import('eslint').Rule.RuleModule}\n */\n module.exports = {\n  meta: {\n    type: 'suggestion',\n    docs: {\n      description: 'disallow use of the new Date()',\n      recommended: false,\n    },\n    fixable: 'code',\n    schema: [],\n    messages: {\n      message: 'new Date()는 클라이언트에서 실행시 해당 기기의 시간에 의존적이라 정확하지 않습니다. 현재 시간이 필요하다면 ServerDate()를 사용해주세요.'\n    },\n  },\n  create: function (context) {\n    return {\n      NewExpression: function (node) {\n        if (node.callee.name === 'Date' && node.arguments.length === 0) {\n          context.report({\n            node,\n            messageId: 'message',\n            fix: function (fixer) {\n              return fixer.replaceText(node, 'ServerDate()')\n            },\n          })\n        }\n      },\n    }\n  },\n}\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/package.json",
    "content": "{\n  \"name\": \"eslint-plugin-yceffort\",\n  \"version\": \"0.0.0\",\n  \"description\": \"yceffort\",\n  \"keywords\": [\n    \"eslint\",\n    \"eslintplugin\",\n    \"eslint-plugin\"\n  ],\n  \"author\": \"yceffort\",\n  \"main\": \"./lib/index.js\",\n  \"exports\": \"./lib/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\",\n    \"test\": \"mocha tests --recursive\"\n  },\n  \"dependencies\": {\n    \"requireindex\": \"^1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"eslint\": \"^8.38.0\",\n    \"mocha\": \"^10.0.0\"\n  },\n  \"engines\": {\n    \"node\": \"^14.17.0 || ^16.0.0 || >= 18.0.0\"\n  },\n  \"peerDependencies\": {\n    \"eslint\": \">=7\"\n  },\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "chapter8/eslint-plugin-yceffort/tests/lib/rules/no-new-date.js",
    "content": "/**\n * @fileoverview yceffort\n * @author yceffort\n */\n\"use strict\";\n\n// ------------------------------------------------------------------------------\n// Requirements\n// ------------------------------------------------------------------------------\n\nconst RuleTester = require(\"eslint\").RuleTester;\n\nconst rule = require(\"../../../lib/rules/no-new-date\");\n\n// ------------------------------------------------------------------------------\n// Tests\n// ------------------------------------------------------------------------------\nconst ruleTester = new RuleTester();\nruleTester.run(\"no-new-date\", rule, {\n  valid: [\n    {\n      code: 'new Date(2021, 1, 1)',      \n    },\n    {\n      code: 'new Date(\"2022-01-01\")',      \n    },    \n  ],\n\n  invalid: [\n    {\n      code: \"new Date()\",\n      errors: [{ message: rule.meta.messages.message }],\n      output: \"ServerDate()\"\n    },\n  ],\n});\n"
  },
  {
    "path": "chapter8/react-test/.eslintignore",
    "content": "build\nreportWebVitals.ts"
  },
  {
    "path": "chapter8/react-test/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter8/react-test/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter8/react-test/.prettierignore",
    "content": "build"
  },
  {
    "path": "chapter8/react-test/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter8/react-test/README.md",
    "content": "# Getting Started with Create React App\n\nThis project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).\n\n## Available Scripts\n\nIn the project directory, you can run:\n\n### `npm start`\n\nRuns the app in the development mode.\\\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n\nThe page will reload if you make edits.\\\nYou will also see any lint errors in the console.\n\n### `npm test`\n\nLaunches the test runner in the interactive watch mode.\\\nSee the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.\\\nIt correctly bundles React in production mode and optimizes the build for the best performance.\n\nThe build is minified and the filenames include the hashes.\\\nYour app is ready to be deployed!\n\nSee the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.\n\n### `npm run eject`\n\n**Note: this is a one-way operation. Once you `eject`, you can’t go back!**\n\nIf you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.\n\nInstead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.\n\nYou don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.\n\n## Learn More\n\nYou can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).\n\nTo learn React, check out the [React documentation](https://reactjs.org/).\n"
  },
  {
    "path": "chapter8/react-test/package.json",
    "content": "{\n  \"name\": \"react-test\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/jest\": \"^27.5.2\",\n    \"@types/node\": \"^16.11.64\",\n    \"@types/react\": \"^18.0.21\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"^5.0.1\",\n    \"typescript\": \"^4.8.4\",\n    \"web-vitals\": \"^2.1.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"eslint\": \"^8.38.0\",\n    \"msw\": \"^0.47.4\",\n    \"prettier\": \"^2.8.7\"\n  }\n}\n"
  },
  {
    "path": "chapter8/react-test/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter8/react-test/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter8/react-test/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter8/react-test/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter8/react-test/src/App.test.tsx",
    "content": "import { render, screen } from '@testing-library/react'\n\nimport App from './App'\n\ntest('renders learn react link', () => {\n  render(<App />)\n  const linkElement = screen.getByText(/learn react/i)\n  expect(linkElement).toBeInTheDocument()\n})\n"
  },
  {
    "path": "chapter8/react-test/src/App.tsx",
    "content": "import logo from './logo.svg'\nimport './App.css'\nimport StaticComponent from './components/StaticComponent'\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.tsx</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n      <StaticComponent />\n    </div>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "chapter8/react-test/src/components/FetchComponent/index.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react'\nimport { rest } from 'msw'\nimport { setupServer } from 'msw/node'\n\nimport { FetchComponent } from '.'\n\nconst MOCK_TODO_RESPONSE = {\n  userId: 1,\n  id: 1,\n  title: 'delectus aut autem',\n  completed: false,\n}\n\nconst server = setupServer(\n  rest.get('/todos/:id', (req, res, ctx) => {\n    const todoId = req.params.id\n\n    if (Number(todoId)) {\n      return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))\n    } else {\n      return res(ctx.status(404))\n    }\n  }),\n)\n\nbeforeAll(() => server.listen())\n// afterEach(() => server.resetHandlers());\nafterAll(() => server.close())\n\nbeforeEach(() => {\n  render(<FetchComponent />)\n})\n\ndescribe('FetchComponent 테스트', () => {\n  it('데이터를 불러오기 전에는 기본 문구가 뜬다.', async () => {\n    const nowLoading = screen.getByText(/불러온 데이터가 없습니다./)\n    expect(nowLoading).toBeInTheDocument()\n  })\n\n  it('버튼을 클릭하면 데이터를 불러온다.', async () => {\n    const button = screen.getByRole('button', { name: /1번/ })\n    fireEvent.click(button)\n\n    const data = await screen.findByText(MOCK_TODO_RESPONSE.title)\n    expect(data).toBeInTheDocument()\n  })\n\n  it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => {\n    server.use(\n      rest.get('/todos/:id', (req, res, ctx) => {\n        return res(ctx.status(503))\n      }),\n    )\n\n    const button = screen.getByRole('button', { name: /1번/ })\n    fireEvent.click(button)\n\n    const error = await screen.findByText(/에러가 발생했습니다/)\n    expect(error).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "chapter8/react-test/src/components/FetchComponent/index.tsx",
    "content": "import { MouseEvent, useState } from 'react'\n\ninterface TodoResponse {\n  userId: number\n  id: number\n  title: string\n  completed: false\n}\n\nexport function FetchComponent() {\n  const [data, setData] = useState<TodoResponse | null>(null)\n  const [error, setError] = useState<number | null>(null)\n\n  async function handleButtonClick(e: MouseEvent<HTMLButtonElement>) {\n    const id = e.currentTarget.dataset.id\n\n    const response = await fetch(`/todos/${id}`)\n\n    if (response.ok) {\n      const result: TodoResponse = await response.json()\n      setData(result)\n    } else {\n      setError(response.status)\n    }\n  }\n\n  return (\n    <div>\n      <p>{data === null ? '불러온 데이터가 없습니다.' : data.title}</p>\n\n      {error && <p style={{ backgroundColor: 'red' }}>에러가 발생했습니다</p>}\n\n      <ul>\n        {Array.from({ length: 10 }).map((_, index) => {\n          const id = index + 1\n          return (\n            <button key={id} data-id={id} onClick={handleButtonClick}>\n              {`${id}번`}\n            </button>\n          )\n        })}\n      </ul>\n    </div>\n  )\n}\n"
  },
  {
    "path": "chapter8/react-test/src/components/StateComponent/index.test.tsx",
    "content": "import { fireEvent, render } from '@testing-library/react'\nimport userEvent from '@testing-library/user-event'\n\nimport { InputComponent } from '.'\n\ndescribe('InputComponent 테스트', () => {\n  const setup = () => {\n    const screen = render(<InputComponent />)\n    const input = screen.getByLabelText('input') as HTMLInputElement\n    const button = screen.getByText(/제출하기/i) as HTMLButtonElement\n    return {\n      input,\n      button,\n      ...screen,\n    }\n  }\n\n  it('input의 초기값은 빈 문자열이다.', () => {\n    const { input } = setup()\n    expect(input.value).toEqual('')\n  })\n\n  it('input의 최대길이가 20자로 설정되어 있다.', () => {\n    const { input } = setup()\n    expect(input).toHaveAttribute('maxlength', '20')\n  })\n\n  it('영문과 숫자만 입력된다.', () => {\n    const { input } = setup()\n    const inputValue = '안녕하세요123'\n    userEvent.type(input, inputValue)\n    expect(input.value).toEqual('123')\n  })\n\n  it('아이디를 입력하지 않으면 버튼이 활성화 되지 않는다.', () => {\n    const { button } = setup()\n    expect(button).toBeDisabled()\n  })\n\n  it('아이디를 입력하면 버튼이 활성화 된다.', () => {\n    const { button, input } = setup()\n\n    const inputValue = 'helloworld'\n    userEvent.type(input, inputValue)\n\n    expect(input.value).toEqual(inputValue)\n    expect(button).toBeEnabled()\n  })\n\n  it('버튼을 클릭하면 alert가 해당 아이디로 뜬다.', () => {\n    const alertMock = jest\n      .spyOn(window, 'alert')\n      .mockImplementation((_: string) => undefined)\n\n    const { button, input } = setup()\n    const inputValue = 'helloworld'\n\n    userEvent.type(input, inputValue)\n    fireEvent.click(button)\n\n    expect(alertMock).toHaveBeenCalledTimes(1)\n    expect(alertMock).toHaveBeenCalledWith(inputValue)\n  })\n})\n"
  },
  {
    "path": "chapter8/react-test/src/components/StateComponent/index.tsx",
    "content": "import { useState } from 'react'\n\nexport function InputComponent() {\n  const [text, setText] = useState('')\n\n  function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const rawValue = event.target.value\n    const value = rawValue.replace(/[^A-Za-z0-9]/gi, '')\n    setText(value)\n  }\n\n  function handleButtonClick() {\n    alert(text)\n  }\n\n  return (\n    <>\n      <label htmlFor=\"input\">아이디를 입력하세요.</label>\n      <input\n        aria-label=\"input\"\n        id=\"input\"\n        value={text}\n        onChange={handleInputChange}\n        maxLength={20}\n      />\n      <button onClick={handleButtonClick} disabled={text.length === 0}>\n        제출하기\n      </button>\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter8/react-test/src/components/StaticComponent/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react'\n\nimport StaticComponent from './index'\n\nbeforeEach(() => {\n  render(<StaticComponent />)\n})\n\ndescribe('링크 확인', () => {\n  it('링크가 3개 존재한다.', () => {\n    const ul = screen.getByTestId('ul')\n    expect(ul.children.length).toBe(3)\n  })\n\n  it('링크 목록의 스타일이 square다.', () => {\n    const ul = screen.getByTestId('ul')\n    expect(ul).toHaveStyle('list-style-type: square;')\n  })\n})\n\ndescribe('리액트 링크 테스트', () => {\n  it('리액트 링크가 존재한다.', () => {\n    const reactLink = screen.getByText('리액트')\n    expect(reactLink).toBeVisible()\n  })\n\n  it('리액트 링크가 올바른 주소로 존재한다.', () => {\n    const reactLink = screen.getByText('리액트')\n\n    expect(reactLink.tagName).toEqual('A')\n    expect(reactLink).toHaveAttribute('href', 'https://reactjs.org')\n  })\n})\n\ndescribe('네이버 링크 테스트', () => {\n  it('네이버 링크가 존재한다.', () => {\n    const naverLink = screen.getByText('네이버')\n    expect(naverLink).toBeVisible()\n  })\n\n  it('네이버 링크가 올바른 주소로 존재한다.', () => {\n    const naverLink = screen.getByText('네이버')\n\n    expect(naverLink.tagName).toEqual('A')\n    expect(naverLink).toHaveAttribute('href', 'https://www.naver.com')\n  })\n})\n\ndescribe('블로그 링크 테스트', () => {\n  it('블로그 링크가 존재한다.', () => {\n    const blogLink = screen.getByText('블로그')\n    expect(blogLink).toBeVisible()\n  })\n\n  it('블로그 링크가 올바른 주소로 존재한다.', () => {\n    const blogLink = screen.getByText('블로그')\n\n    expect(blogLink.tagName).toEqual('A')\n    expect(blogLink).toHaveAttribute('href', 'https://yceffort.kr')\n  })\n\n  it('블로그는 같은 창에서 열려야 한다.', () => {\n    const blogLink = screen.getByText('블로그')\n    expect(blogLink).not.toHaveAttribute('target')\n  })\n})\n"
  },
  {
    "path": "chapter8/react-test/src/components/StaticComponent/index.tsx",
    "content": "import { memo } from 'react'\n\nconst AnchorTagComponent = memo(function AnchorTagComponent({\n  name,\n  href,\n  targetBlank,\n}: {\n  name: string\n  href: string\n  targetBlank?: boolean\n}) {\n  return (\n    <a\n      href={href}\n      target={targetBlank ? '_blank' : undefined}\n      rel=\"noopener noreferrer\"\n    >\n      {name}\n    </a>\n  )\n})\n\nexport default function StaticComponent() {\n  return (\n    <>\n      <h1>Static Component</h1>\n      <div>유용한 링크</div>\n\n      <ul data-testid=\"ul\" style={{ listStyleType: 'square' }}>\n        <li>\n          <AnchorTagComponent\n            targetBlank\n            name=\"리액트\"\n            href=\"https://reactjs.org\"\n          />\n        </li>\n        <li>\n          <AnchorTagComponent\n            targetBlank\n            name=\"네이버\"\n            href=\"https://www.naver.com\"\n          />\n        </li>\n        <li>\n          <AnchorTagComponent name=\"블로그\" href=\"https://yceffort.kr\" />\n        </li>\n      </ul>\n    </>\n  )\n}\n"
  },
  {
    "path": "chapter8/react-test/src/hooks/useEffectDebugger.test.ts",
    "content": "import { renderHook } from '@testing-library/react'\n\nimport useEffectDebugger, { CONSOLE_PREFIX } from './useEffectDebugger'\n\nconst consoleSpy = jest.spyOn(console, 'log')\nconst componentName = 'TestComponent'\n\ndescribe('useEffectDebugger', () => {\n  afterAll(() => {\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    process.env.NODE_ENV = 'development'\n  })\n\n  it('props가 없으면 호출되지 않는다.', () => {\n    renderHook(() => useEffectDebugger(componentName))\n\n    expect(consoleSpy).not.toHaveBeenCalled()\n  })\n\n  it('최초에는 호출되지 않는다.', () => {\n    const props = { hello: 'world' }\n\n    renderHook(() => useEffectDebugger(componentName, props))\n\n    expect(consoleSpy).not.toHaveBeenCalled()\n  })\n\n  it('props가 변경되지 않으면 호출되지 않는다.', () => {\n    const props = { hello: 'world' }\n\n    const { rerender } = renderHook(() =>\n      useEffectDebugger(componentName, props),\n    )\n\n    expect(consoleSpy).not.toHaveBeenCalled()\n\n    rerender()\n\n    expect(consoleSpy).not.toHaveBeenCalled()\n  })\n\n  it('props가 변경되면 다시 호출한다.', () => {\n    const props = { hello: 'world' }\n\n    const { rerender } = renderHook(\n      ({ componentName, props }) => useEffectDebugger(componentName, props),\n      {\n        initialProps: {\n          componentName,\n          props,\n        },\n      },\n    )\n\n    const newProps = { hello: 'world2' }\n\n    rerender({ componentName, props: newProps })\n\n    expect(consoleSpy).toHaveBeenCalled()\n  })\n\n  it('props가 변경되면 변경된 props를 정확히 출력한다', () => {\n    const props = { hello: 'world' }\n\n    const { rerender } = renderHook(\n      ({ componentName, props }) => useEffectDebugger(componentName, props),\n      {\n        initialProps: {\n          componentName,\n          props,\n        },\n      },\n    )\n\n    const newProps = { hello: 'world2' }\n\n    rerender({ componentName, props: newProps })\n\n    expect(consoleSpy).toHaveBeenCalledWith(CONSOLE_PREFIX, 'TestComponent', {\n      hello: { after: 'world2', before: 'world' },\n    })\n  })\n\n  it('객체는 참조가 다르다면 변경된 것으로 간주한다', () => {\n    const props = { hello: { hello: 'world' } }\n    const newProps = { hello: { hello: 'world' } }\n\n    const { rerender } = renderHook(\n      ({ componentName, props }) => useEffectDebugger(componentName, props),\n      {\n        initialProps: {\n          componentName,\n          props,\n        },\n      },\n    )\n\n    rerender({ componentName, props: newProps })\n\n    // 이후 호출\n    expect(consoleSpy).toHaveBeenCalled()\n  })\n\n  it('process.env.NODE_ENV가 production이면 호출되지 않는다', () => {\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    process.env.NODE_ENV = 'production'\n\n    const props = { hello: 'world' }\n\n    const { rerender } = renderHook(\n      ({ componentName, props }) => useEffectDebugger(componentName, props),\n      {\n        initialProps: {\n          componentName,\n          props,\n        },\n      },\n    )\n\n    const newProps = { hello: 'world2' }\n\n    rerender({ componentName, props: newProps })\n\n    expect(consoleSpy).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "chapter8/react-test/src/hooks/useEffectDebugger.ts",
    "content": "import { useEffect, useRef } from 'react'\n\nexport type Props = Record<string, unknown>\n\nexport const CONSOLE_PREFIX = '[useEffectDebugger]'\n\nexport default function useEffectDebugger(\n  componentName: string,\n  props?: Props,\n) {\n  const prevProps = useRef<Props | undefined>()\n\n  useEffect(() => {\n    if (process.env.NODE_ENV === 'production') {\n      return\n    }\n\n    const prevPropsCurrent = prevProps.current\n\n    if (prevPropsCurrent !== undefined) {\n      const allKeys = Object.keys({ ...prevProps.current, ...props })\n\n      const changedProps: Props = allKeys.reduce<Props>((result, key) => {\n        const prevValue = prevPropsCurrent[key]\n        const currentValue = props ? props[key] : undefined\n\n        if (!Object.is(prevValue, currentValue)) {\n          result[key] = {\n            before: prevValue,\n            after: currentValue,\n          }\n        }\n        return result\n      }, {})\n\n      if (Object.keys(changedProps).length) {\n        // eslint-disable-next-line no-console\n        console.log(CONSOLE_PREFIX, componentName, changedProps)\n      }\n    }\n\n    prevProps.current = props\n  })\n}\n"
  },
  {
    "path": "chapter8/react-test/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter8/react-test/src/index.tsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\n\nimport './index.css'\nimport App from './App'\nimport reportWebVitals from './reportWebVitals'\n\nconst root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n)\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\n// eslint-disable-next-line\nreportWebVitals(console.log)\n"
  },
  {
    "path": "chapter8/react-test/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "chapter8/react-test/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals'\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry)\n      getFID(onPerfEntry)\n      getFCP(onPerfEntry)\n      getLCP(onPerfEntry)\n      getTTFB(onPerfEntry)\n    })\n  }\n}\n\nexport default reportWebVitals\n"
  },
  {
    "path": "chapter8/react-test/src/setupTests.ts",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom'\n"
  },
  {
    "path": "chapter8/react-test/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/.eslintignore",
    "content": "build/\nnode_modules/\n"
  },
  {
    "path": "chapter9/danger-react-app/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter9/danger-react-app/.npmrc",
    "content": "registry=https://registry.npmjs.org/"
  },
  {
    "path": "chapter9/danger-react-app/.prettierignore",
    "content": "build/\nnode_modules/\n"
  },
  {
    "path": "chapter9/danger-react-app/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter9/danger-react-app/README.md",
    "content": "# danger-react-app\n"
  },
  {
    "path": "chapter9/danger-react-app/package.json",
    "content": "{\n  \"name\": \"danger-react-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"axios\": \"^1.1.3\",\n    \"mobx\": \"^5.11.0\",\n    \"mobx-react-lite\": \"^1.4.1\",\n    \"react\": \"^16.8.6\",\n    \"react-dom\": \"^16.8.6\",\n    \"react-router-dom\": \"^5.0.1\",\n    \"react-scripts\": \"^3.4.1\",\n    \"react-swipeable-views\": \"^0.13.3\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"overrides\": {\n    \"ansi-html\": \"^0.0.8\",\n    \"browserslist\": \"^4.21.4\",\n    \"immer\": \"^9.0.6\",\n    \"jsdom\": \"^16.5.0\",\n    \"glob-parent\": \"^5.1.2\",\n    \"minimatch\": \"^3.0.5\",\n    \"node-notifier\": \"^8.0.1\",\n    \"nth-check\": \"^2.1.1\",\n    \"node-forge\": \"^1.3.0\",\n    \"postcss\": \"^7.0.36\",\n    \"react-dev-utils\": \"^11.0.4\",\n    \"shell-quote\": \"^1.7.3\"\n  }\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter9/danger-react-app/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter9/danger-react-app/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/src/App.js",
    "content": "import React from 'react'\n\nimport logo from './logo.svg'\nimport './App.css'\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.js</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n    </div>\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "chapter9/danger-react-app/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter9/danger-react-app/src/index.js",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport { BrowserRouter as Router } from 'react-router-dom'\n\nimport App from './App'\n\nReactDOM.render(\n  <Router>\n    <App />\n  </Router>,\n  document.getElementById('root'),\n)\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/README.md",
    "content": "# react-deep-dive-example-cra\n\nvisit: https://main.drv3mroaqiuz2.amplifyapp.com/\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/package.json",
    "content": "{\n  \"name\": \"cra\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/jest\": \"^27.5.2\",\n    \"@types/node\": \"^16.18.3\",\n    \"@types/react\": \"^18.0.24\",\n    \"@types/react-dom\": \"^18.0.8\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"typescript\": \"^4.8.4\",\n    \"web-vitals\": \"^2.1.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/App.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport App from './App';\n\ntest('renders learn react link', () => {\n  render(<App />);\n  const linkElement = screen.getByText(/learn react/i);\n  expect(linkElement).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/App.tsx",
    "content": "import React from 'react';\nimport logo from './logo.svg';\nimport './App.css';\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.tsx</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nconst root = ReactDOM.createRoot(\n  document.getElementById('root') as HTMLElement\n);\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/src/setupTests.ts",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "chapter9/deploy/aws/cra/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter9/deploy/aws/next/README.md",
    "content": "# react-deep-dive-example-next\n\nvisit: https://main.dtabop25r3q54.amplifyapp.com/\n"
  },
  {
    "path": "chapter9/deploy/aws/next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter9/deploy/aws/next/package.json",
    "content": "{\n  \"name\": \"next\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"next\": \"^11.1.4\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.11.8\",\n    \"@types/react\": \"18.0.1\",\n    \"@types/react-dom\": \"^17.0.18\",\n    \"eslint\": \"8.26.0\",\n    \"eslint-config-next\": \"13.0.0\",\n    \"typescript\": \"4.8.4\"\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/pages/_app.tsx",
    "content": "import '../styles/globals.css'\nimport type { AppProps } from 'next/app'\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "chapter9/deploy/aws/next/pages/api/hello.ts",
    "content": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiResponse } from 'next'\n\ntype Data = {\n  name: string\n}\n\nexport default function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<Data>\n) {\n  res.status(200).json({ name: 'John Doe' })\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/pages/index.tsx",
    "content": "import type { NextPage } from 'next'\nimport Head from 'next/head'\nimport Image from 'next/image'\nimport styles from '../styles/Home.module.css'\n\nconst Home: NextPage = () => {\n  return (\n    <div className={styles.container}>\n      <Head>\n        <title>Create Next App</title>\n        <meta name=\"description\" content=\"Generated by create next app\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n\n      <main className={styles.main}>\n        <h1 className={styles.title}>\n          Welcome to <a href=\"https://nextjs.org\">Next.js!</a>\n        </h1>\n\n        <p className={styles.description}>\n          Get started by editing{' '}\n          <code className={styles.code}>pages/index.tsx</code>\n        </p>\n\n        <div className={styles.grid}>\n          <a href=\"https://nextjs.org/docs\" className={styles.card}>\n            <h2>Documentation &rarr;</h2>\n            <p>Find in-depth information about Next.js features and API.</p>\n          </a>\n\n          <a href=\"https://nextjs.org/learn\" className={styles.card}>\n            <h2>Learn &rarr;</h2>\n            <p>Learn about Next.js in an interactive course with quizzes!</p>\n          </a>\n\n          <a\n            href=\"https://github.com/vercel/next.js/tree/canary/examples\"\n            className={styles.card}\n          >\n            <h2>Examples &rarr;</h2>\n            <p>Discover and deploy boilerplate example Next.js projects.</p>\n          </a>\n\n          <a\n            href=\"https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n            className={styles.card}\n          >\n            <h2>Deploy &rarr;</h2>\n            <p>\n              Instantly deploy your Next.js site to a public URL with Vercel.\n            </p>\n          </a>\n        </div>\n      </main>\n\n      <footer className={styles.footer}>\n        <a\n          href=\"https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Powered by{' '}\n          <span className={styles.logo}>\n            <Image src=\"/vercel.svg\" alt=\"Vercel Logo\" width={72} height={16} />\n          </span>\n        </a>\n      </footer>\n    </div>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "chapter9/deploy/aws/next/styles/Home.module.css",
    "content": ".container {\n  padding: 0 2rem;\n}\n\n.main {\n  min-height: 100vh;\n  padding: 4rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer {\n  display: flex;\n  flex: 1;\n  padding: 2rem 0;\n  border-top: 1px solid #eaeaea;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-grow: 1;\n}\n\n.title a {\n  color: #0070f3;\n  text-decoration: none;\n}\n\n.title a:hover,\n.title a:focus,\n.title a:active {\n  text-decoration: underline;\n}\n\n.title {\n  margin: 0;\n  line-height: 1.15;\n  font-size: 4rem;\n}\n\n.title,\n.description {\n  text-align: center;\n}\n\n.description {\n  margin: 4rem 0;\n  line-height: 1.5;\n  font-size: 1.5rem;\n}\n\n.code {\n  background: #fafafa;\n  border-radius: 5px;\n  padding: 0.75rem;\n  font-size: 1.1rem;\n  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,\n    Bitstream Vera Sans Mono, Courier New, monospace;\n}\n\n.grid {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  max-width: 800px;\n}\n\n.card {\n  margin: 1rem;\n  padding: 1.5rem;\n  text-align: left;\n  color: inherit;\n  text-decoration: none;\n  border: 1px solid #eaeaea;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 300px;\n}\n\n.card:hover,\n.card:focus,\n.card:active {\n  color: #0070f3;\n  border-color: #0070f3;\n}\n\n.card h2 {\n  margin: 0 0 1rem 0;\n  font-size: 1.5rem;\n}\n\n.card p {\n  margin: 0;\n  font-size: 1.25rem;\n  line-height: 1.5;\n}\n\n.logo {\n  height: 1em;\n  margin-left: 0.5rem;\n}\n\n@media (max-width: 600px) {\n  .grid {\n    width: 100%;\n    flex-direction: column;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .card,\n  .footer {\n    border-color: #222;\n  }\n  .code {\n    background: #111;\n  }\n  .logo img {\n    filter: invert(1);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/styles/globals.css",
    "content": "html,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n  body {\n    color: white;\n    background: black;\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/aws/next/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/README.md",
    "content": "# react-deep-dive-example-cra\n\nvisit: https://cra-gbqbi.ondigitalocean.app/\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/package.json",
    "content": "{\n  \"name\": \"cra\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"web-vitals\": \"^2.1.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/App.js",
    "content": "import logo from './logo.svg';\nimport './App.css';\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.js</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/App.test.js",
    "content": "import { render, screen } from '@testing-library/react';\nimport App from './App';\n\ntest('renders learn react link', () => {\n  render(<App />);\n  const linkElement = screen.getByText(/learn react/i);\n  expect(linkElement).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/index.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/reportWebVitals.js",
    "content": "const reportWebVitals = onPerfEntry => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/cra/src/setupTests.js",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/README.md",
    "content": "# react-deep-dive-example-next\n\nvisit: https://next-g6zet.ondigitalocean.app/\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/package.json",
    "content": "{\n  \"name\": \"next\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"next\": \"13.0.0\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.11.7\",\n    \"@types/react\": \"18.0.24\",\n    \"@types/react-dom\": \"18.0.8\",\n    \"eslint\": \"8.26.0\",\n    \"eslint-config-next\": \"13.0.0\",\n    \"typescript\": \"4.8.4\"\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/pages/_app.tsx",
    "content": "import '../styles/globals.css'\nimport type { AppProps } from 'next/app'\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/pages/api/hello.ts",
    "content": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiResponse } from 'next'\n\ntype Data = {\n  name: string\n}\n\nexport default function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<Data>\n) {\n  res.status(200).json({ name: 'John Doe' })\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/pages/index.tsx",
    "content": "import type { NextPage } from 'next'\nimport Head from 'next/head'\nimport Image from 'next/image'\nimport styles from '../styles/Home.module.css'\n\nconst Home: NextPage = () => {\n  return (\n    <div className={styles.container}>\n      <Head>\n        <title>Create Next App</title>\n        <meta name=\"description\" content=\"Generated by create next app\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n\n      <main className={styles.main}>\n        <h1 className={styles.title}>\n          Welcome to <a href=\"https://nextjs.org\">Next.js!</a>\n        </h1>\n\n        <p className={styles.description}>\n          Get started by editing{' '}\n          <code className={styles.code}>pages/index.tsx</code>\n        </p>\n\n        <div className={styles.grid}>\n          <a href=\"https://nextjs.org/docs\" className={styles.card}>\n            <h2>Documentation &rarr;</h2>\n            <p>Find in-depth information about Next.js features and API.</p>\n          </a>\n\n          <a href=\"https://nextjs.org/learn\" className={styles.card}>\n            <h2>Learn &rarr;</h2>\n            <p>Learn about Next.js in an interactive course with quizzes!</p>\n          </a>\n\n          <a\n            href=\"https://github.com/vercel/next.js/tree/canary/examples\"\n            className={styles.card}\n          >\n            <h2>Examples &rarr;</h2>\n            <p>Discover and deploy boilerplate example Next.js projects.</p>\n          </a>\n\n          <a\n            href=\"https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n            className={styles.card}\n          >\n            <h2>Deploy &rarr;</h2>\n            <p>\n              Instantly deploy your Next.js site to a public URL with Vercel.\n            </p>\n          </a>\n        </div>\n      </main>\n\n      <footer className={styles.footer}>\n        <a\n          href=\"https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Powered by{' '}\n          <span className={styles.logo}>\n            <Image src=\"/vercel.svg\" alt=\"Vercel Logo\" width={72} height={16} />\n          </span>\n        </a>\n      </footer>\n    </div>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/styles/Home.module.css",
    "content": ".container {\n  padding: 0 2rem;\n}\n\n.main {\n  min-height: 100vh;\n  padding: 4rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer {\n  display: flex;\n  flex: 1;\n  padding: 2rem 0;\n  border-top: 1px solid #eaeaea;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-grow: 1;\n}\n\n.title a {\n  color: #0070f3;\n  text-decoration: none;\n}\n\n.title a:hover,\n.title a:focus,\n.title a:active {\n  text-decoration: underline;\n}\n\n.title {\n  margin: 0;\n  line-height: 1.15;\n  font-size: 4rem;\n}\n\n.title,\n.description {\n  text-align: center;\n}\n\n.description {\n  margin: 4rem 0;\n  line-height: 1.5;\n  font-size: 1.5rem;\n}\n\n.code {\n  background: #fafafa;\n  border-radius: 5px;\n  padding: 0.75rem;\n  font-size: 1.1rem;\n  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,\n    Bitstream Vera Sans Mono, Courier New, monospace;\n}\n\n.grid {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  max-width: 800px;\n}\n\n.card {\n  margin: 1rem;\n  padding: 1.5rem;\n  text-align: left;\n  color: inherit;\n  text-decoration: none;\n  border: 1px solid #eaeaea;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 300px;\n}\n\n.card:hover,\n.card:focus,\n.card:active {\n  color: #0070f3;\n  border-color: #0070f3;\n}\n\n.card h2 {\n  margin: 0 0 1rem 0;\n  font-size: 1.5rem;\n}\n\n.card p {\n  margin: 0;\n  font-size: 1.25rem;\n  line-height: 1.5;\n}\n\n.logo {\n  height: 1em;\n  margin-left: 0.5rem;\n}\n\n@media (max-width: 600px) {\n  .grid {\n    width: 100%;\n    flex-direction: column;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .card,\n  .footer {\n    border-color: #222;\n  }\n  .code {\n    background: #111;\n  }\n  .logo img {\n    filter: invert(1);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/styles/globals.css",
    "content": "html,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n  body {\n    color: white;\n    background: black;\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/digitalocean/next/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/README.md",
    "content": "# react-deep-dive-example-cra\n\nvisit: https://create-react-app-yceffort.netlify.app/"
  },
  {
    "path": "chapter9/deploy/netlify/cra/package.json",
    "content": "{\n  \"name\": \"cra\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/jest\": \"^27.5.2\",\n    \"@types/node\": \"^16.18.2\",\n    \"@types/react\": \"^18.0.23\",\n    \"@types/react-dom\": \"^18.0.7\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"typescript\": \"^4.8.4\",\n    \"web-vitals\": \"^2.1.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/App.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport App from './App';\n\ntest('renders learn react link', () => {\n  render(<App />);\n  const linkElement = screen.getByText(/learn react/i);\n  expect(linkElement).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/App.tsx",
    "content": "import React from 'react';\nimport logo from './logo.svg';\nimport './App.css';\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.tsx</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nconst root = ReactDOM.createRoot(\n  document.getElementById('root') as HTMLElement\n);\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/src/setupTests.ts",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "chapter9/deploy/netlify/cra/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/README.md",
    "content": "# react-deep-dive-example-next\n\nvisit: https://create-next-app-yceffort.netlify.app/"
  },
  {
    "path": "chapter9/deploy/netlify/next/netlify.toml",
    "content": "[[plugins]]\npackage = \"@netlify/plugin-nextjs\""
  },
  {
    "path": "chapter9/deploy/netlify/next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/package.json",
    "content": "{\n  \"name\": \"next\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"next\": \"13.0.0\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.11.7\",\n    \"@types/react\": \"18.0.23\",\n    \"@types/react-dom\": \"18.0.7\",\n    \"eslint\": \"8.26.0\",\n    \"eslint-config-next\": \"13.0.0\",\n    \"typescript\": \"4.8.4\"\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/pages/_app.tsx",
    "content": "import '../styles/globals.css'\nimport type { AppProps } from 'next/app'\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/pages/api/hello.ts",
    "content": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiResponse } from 'next'\n\ntype Data = {\n  name: string\n}\n\nexport default function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<Data>\n) {\n  res.status(200).json({ name: 'John Doe' })\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/pages/hello.tsx",
    "content": "import { GetServerSideProps } from \"next\"\n\nexport default function Hello({hello}: {hello: string}) {\n    return <>Hello {hello}!</>\n}\n\nexport const getServerSideProps: GetServerSideProps = async () => {\n    return {\n        props: {\n            hello:'world'\n        }\n    }\n}"
  },
  {
    "path": "chapter9/deploy/netlify/next/pages/index.tsx",
    "content": "import type { NextPage } from 'next'\nimport Head from 'next/head'\nimport Image from 'next/image'\nimport styles from '../styles/Home.module.css'\n\nconst Home: NextPage = () => {\n  return (\n    <div className={styles.container}>\n      <Head>\n        <title>Create Next App</title>\n        <meta name=\"description\" content=\"Generated by create next app\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n\n      <main className={styles.main}>\n        <h1 className={styles.title}>\n          Welcome to <a href=\"https://nextjs.org\">Next.js!</a>\n        </h1>\n\n        <p className={styles.description}>\n          Get started by editing{' '}\n          <code className={styles.code}>pages/index.tsx</code>\n        </p>\n\n        <div className={styles.grid}>\n          <a href=\"https://nextjs.org/docs\" className={styles.card}>\n            <h2>Documentation &rarr;</h2>\n            <p>Find in-depth information about Next.js features and API.</p>\n          </a>\n\n          <a href=\"https://nextjs.org/learn\" className={styles.card}>\n            <h2>Learn &rarr;</h2>\n            <p>Learn about Next.js in an interactive course with quizzes!</p>\n          </a>\n\n          <a\n            href=\"https://github.com/vercel/next.js/tree/canary/examples\"\n            className={styles.card}\n          >\n            <h2>Examples &rarr;</h2>\n            <p>Discover and deploy boilerplate example Next.js projects.</p>\n          </a>\n\n          <a\n            href=\"https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n            className={styles.card}\n          >\n            <h2>Deploy &rarr;</h2>\n            <p>\n              Instantly deploy your Next.js site to a public URL with Vercel.\n            </p>\n          </a>\n        </div>\n      </main>\n\n      <footer className={styles.footer}>\n        <a\n          href=\"https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Powered by{' '}\n          <span className={styles.logo}>\n            <Image src=\"/vercel.svg\" alt=\"Vercel Logo\" width={72} height={16} />\n          </span>\n        </a>\n      </footer>\n    </div>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/styles/Home.module.css",
    "content": ".container {\n  padding: 0 2rem;\n}\n\n.main {\n  min-height: 100vh;\n  padding: 4rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer {\n  display: flex;\n  flex: 1;\n  padding: 2rem 0;\n  border-top: 1px solid #eaeaea;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-grow: 1;\n}\n\n.title a {\n  color: #0070f3;\n  text-decoration: none;\n}\n\n.title a:hover,\n.title a:focus,\n.title a:active {\n  text-decoration: underline;\n}\n\n.title {\n  margin: 0;\n  line-height: 1.15;\n  font-size: 4rem;\n}\n\n.title,\n.description {\n  text-align: center;\n}\n\n.description {\n  margin: 4rem 0;\n  line-height: 1.5;\n  font-size: 1.5rem;\n}\n\n.code {\n  background: #fafafa;\n  border-radius: 5px;\n  padding: 0.75rem;\n  font-size: 1.1rem;\n  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,\n    Bitstream Vera Sans Mono, Courier New, monospace;\n}\n\n.grid {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  max-width: 800px;\n}\n\n.card {\n  margin: 1rem;\n  padding: 1.5rem;\n  text-align: left;\n  color: inherit;\n  text-decoration: none;\n  border: 1px solid #eaeaea;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 300px;\n}\n\n.card:hover,\n.card:focus,\n.card:active {\n  color: #0070f3;\n  border-color: #0070f3;\n}\n\n.card h2 {\n  margin: 0 0 1rem 0;\n  font-size: 1.5rem;\n}\n\n.card p {\n  margin: 0;\n  font-size: 1.25rem;\n  line-height: 1.5;\n}\n\n.logo {\n  height: 1em;\n  margin-left: 0.5rem;\n}\n\n@media (max-width: 600px) {\n  .grid {\n    width: 100%;\n    flex-direction: column;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .card,\n  .footer {\n    border-color: #222;\n  }\n  .code {\n    background: #111;\n  }\n  .logo img {\n    filter: invert(1);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/styles/globals.css",
    "content": "html,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n  body {\n    color: white;\n    background: black;\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/netlify/next/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/README.md",
    "content": "# react-deep-dive-example-cra\n\nvisit: https://react-deep-dive-example-cra.vercel.app/"
  },
  {
    "path": "chapter9/deploy/vercel/cra/package.json",
    "content": "{\n  \"name\": \"cra\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"@testing-library/react\": \"^13.4.0\",\n    \"@testing-library/user-event\": \"^13.5.0\",\n    \"@types/jest\": \"^27.5.2\",\n    \"@types/node\": \"^16.18.1\",\n    \"@types/react\": \"^18.0.23\",\n    \"@types/react-dom\": \"^18.0.7\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"typescript\": \"^4.8.4\",\n    \"web-vitals\": \"^2.1.4\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <!--\n      manifest.json provides metadata used when your web app is installed on a\n      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/App.css",
    "content": ".App {\n  text-align: center;\n}\n\n.App-logo {\n  height: 40vmin;\n  pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n  .App-logo {\n    animation: App-logo-spin infinite 20s linear;\n  }\n}\n\n.App-header {\n  background-color: #282c34;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  font-size: calc(10px + 2vmin);\n  color: white;\n}\n\n.App-link {\n  color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/App.test.tsx",
    "content": "import React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport App from './App';\n\ntest('renders learn react link', () => {\n  render(<App />);\n  const linkElement = screen.getByText(/learn react/i);\n  expect(linkElement).toBeInTheDocument();\n});\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/App.tsx",
    "content": "import React from 'react';\nimport logo from './logo.svg';\nimport './App.css';\n\nfunction App() {\n  return (\n    <div className=\"App\">\n      <header className=\"App-header\">\n        <img src={logo} className=\"App-logo\" alt=\"logo\" />\n        <p>\n          Edit <code>src/App.tsx</code> and save to reload.\n        </p>\n        <a\n          className=\"App-link\"\n          href=\"https://reactjs.org\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Learn React\n        </a>\n      </header>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimport reportWebVitals from './reportWebVitals';\n\nconst root = ReactDOM.createRoot(\n  document.getElementById('root') as HTMLElement\n);\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);\n\n// If you want to start measuring performance in your app, pass a function\n// to log results (for example: reportWebVitals(console.log))\n// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals\nreportWebVitals();\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/reportWebVitals.ts",
    "content": "import { ReportHandler } from 'web-vitals';\n\nconst reportWebVitals = (onPerfEntry?: ReportHandler) => {\n  if (onPerfEntry && onPerfEntry instanceof Function) {\n    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {\n      getCLS(onPerfEntry);\n      getFID(onPerfEntry);\n      getFCP(onPerfEntry);\n      getLCP(onPerfEntry);\n      getTTFB(onPerfEntry);\n    });\n  }\n};\n\nexport default reportWebVitals;\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/src/setupTests.ts",
    "content": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).toHaveTextContent(/react/i)\n// learn more: https://github.com/testing-library/jest-dom\nimport '@testing-library/jest-dom';\n"
  },
  {
    "path": "chapter9/deploy/vercel/cra/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/README.md",
    "content": "# react-deep-dive-example-next\n\nvisit: https://react-deep-dive-example-next.vercel.app/"
  },
  {
    "path": "chapter9/deploy/vercel/next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/package.json",
    "content": "{\n  \"name\": \"next\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"next\": \"13.0.0\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.11.6\",\n    \"@types/react\": \"18.0.23\",\n    \"@types/react-dom\": \"18.0.7\",\n    \"eslint\": \"8.26.0\",\n    \"eslint-config-next\": \"13.0.0\",\n    \"typescript\": \"4.8.4\"\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/pages/_app.tsx",
    "content": "import '../styles/globals.css'\nimport type { AppProps } from 'next/app'\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/pages/api/hello.ts",
    "content": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiResponse } from 'next'\n\ntype Data = {\n  name: string\n}\n\nexport default function handler(\n  req: NextApiRequest,\n  res: NextApiResponse<Data>\n) {\n  res.status(200).json({ name: 'John Doe' })\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/pages/index.tsx",
    "content": "import type { NextPage } from 'next'\nimport Head from 'next/head'\nimport Image from 'next/image'\nimport styles from '../styles/Home.module.css'\n\nconst Home: NextPage = () => {\n  return (\n    <div className={styles.container}>\n      <Head>\n        <title>Create Next App</title>\n        <meta name=\"description\" content=\"Generated by create next app\" />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n\n      <main className={styles.main}>\n        <h1 className={styles.title}>\n          Welcome to <a href=\"https://nextjs.org\">Next.js!</a>\n        </h1>\n\n        <p className={styles.description}>\n          Get started by editing{' '}\n          <code className={styles.code}>pages/index.tsx</code>\n        </p>\n\n        <div className={styles.grid}>\n          <a href=\"https://nextjs.org/docs\" className={styles.card}>\n            <h2>Documentation &rarr;</h2>\n            <p>Find in-depth information about Next.js features and API.</p>\n          </a>\n\n          <a href=\"https://nextjs.org/learn\" className={styles.card}>\n            <h2>Learn &rarr;</h2>\n            <p>Learn about Next.js in an interactive course with quizzes!</p>\n          </a>\n\n          <a\n            href=\"https://github.com/vercel/next.js/tree/canary/examples\"\n            className={styles.card}\n          >\n            <h2>Examples &rarr;</h2>\n            <p>Discover and deploy boilerplate example Next.js projects.</p>\n          </a>\n\n          <a\n            href=\"https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n            className={styles.card}\n          >\n            <h2>Deploy &rarr;</h2>\n            <p>\n              Instantly deploy your Next.js site to a public URL with Vercel.\n            </p>\n          </a>\n        </div>\n      </main>\n\n      <footer className={styles.footer}>\n        <a\n          href=\"https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          Powered by{' '}\n          <span className={styles.logo}>\n            <Image src=\"/vercel.svg\" alt=\"Vercel Logo\" width={72} height={16} />\n          </span>\n        </a>\n      </footer>\n    </div>\n  )\n}\n\nexport default Home\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/styles/Home.module.css",
    "content": ".container {\n  padding: 0 2rem;\n}\n\n.main {\n  min-height: 100vh;\n  padding: 4rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer {\n  display: flex;\n  flex: 1;\n  padding: 2rem 0;\n  border-top: 1px solid #eaeaea;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-grow: 1;\n}\n\n.title a {\n  color: #0070f3;\n  text-decoration: none;\n}\n\n.title a:hover,\n.title a:focus,\n.title a:active {\n  text-decoration: underline;\n}\n\n.title {\n  margin: 0;\n  line-height: 1.15;\n  font-size: 4rem;\n}\n\n.title,\n.description {\n  text-align: center;\n}\n\n.description {\n  margin: 4rem 0;\n  line-height: 1.5;\n  font-size: 1.5rem;\n}\n\n.code {\n  background: #fafafa;\n  border-radius: 5px;\n  padding: 0.75rem;\n  font-size: 1.1rem;\n  font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,\n    Bitstream Vera Sans Mono, Courier New, monospace;\n}\n\n.grid {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  max-width: 800px;\n}\n\n.card {\n  margin: 1rem;\n  padding: 1.5rem;\n  text-align: left;\n  color: inherit;\n  text-decoration: none;\n  border: 1px solid #eaeaea;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 300px;\n}\n\n.card:hover,\n.card:focus,\n.card:active {\n  color: #0070f3;\n  border-color: #0070f3;\n}\n\n.card h2 {\n  margin: 0 0 1rem 0;\n  font-size: 1.5rem;\n}\n\n.card p {\n  margin: 0;\n  font-size: 1.25rem;\n  line-height: 1.5;\n}\n\n.logo {\n  height: 1em;\n  margin-left: 0.5rem;\n}\n\n@media (max-width: 600px) {\n  .grid {\n    width: 100%;\n    flex-direction: column;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .card,\n  .footer {\n    border-color: #222;\n  }\n  .code {\n    background: #111;\n  }\n  .logo img {\n    filter: invert(1);\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/styles/globals.css",
    "content": "html,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n  body {\n    color: white;\n    background: black;\n  }\n}\n"
  },
  {
    "path": "chapter9/deploy/vercel/next/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/.eslintignore",
    "content": ".next/\nnode_modules/\n"
  },
  {
    "path": "chapter9/zero-to-next/.eslintrc.js",
    "content": "module.exports = {\n  extends: [\n    'next/core-web-vitals',\n    '@titicaca/eslint-config-triple',\n    '@titicaca/eslint-config-triple/frontend',\n    '@titicaca/eslint-config-triple/prettier',\n  ],\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts"
  },
  {
    "path": "chapter9/zero-to-next/.prettierignore",
    "content": ".next/\nnode_modules/\n"
  },
  {
    "path": "chapter9/zero-to-next/.prettierrc",
    "content": "\"@titicaca/prettier-config-triple\"\n"
  },
  {
    "path": "chapter9/zero-to-next/README.md",
    "content": "# 7장 예제 애플리케이션\n\n7장의 예제 애플리케이션입니다.\n"
  },
  {
    "path": "chapter9/zero-to-next/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  poweredByHeader: false,\n  eslint: {\n    ignoreDuringBuilds: true,\n  },\n}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "chapter9/zero-to-next/package.json",
    "content": "{\n  \"name\": \"my-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"start\": \"next start\",\n    \"build\": \"next build\",\n    \"lint\": \"eslint . --fix\",\n    \"prettier\": \"prettier . --write\"\n  },\n  \"author\": \"yceffort\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"next\": \"^12.3.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"styled-components\": \"^5.3.6\"\n  },\n  \"devDependencies\": {\n    \"@titicaca/eslint-config-triple\": \"^5.0.0\",\n    \"@titicaca/prettier-config-triple\": \"^1.0.2\",\n    \"@types/node\": \"^18.8.5\",\n    \"@types/react\": \"^18.0.21\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@types/styled-components\": \"^5.1.26\",\n    \"eslint\": \"^8.38.0\",\n    \"prettier\": \"^2.8.7\",\n    \"eslint-config-next\": \"^12.3.1\",\n    \"typescript\": \"^4.8.4\"\n  }\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/_app.tsx",
    "content": "import { AppProps, NextWebVitalsMetric } from 'next/app'\n\nexport function reportWebVitals(metric: NextWebVitalsMetric) {\n  // eslint-disable-next-line no-console\n  console.log(metric)\n}\n\nfunction MyApp({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />\n}\n\nexport default MyApp\n"
  },
  {
    "path": "chapter9/zero-to-next/src/components/common/title.tsx",
    "content": "import styled from 'styled-components'\n\nexport const Title = styled.h1`\n  font-size: 20px;\n  color: red;\n`\n"
  },
  {
    "path": "chapter9/zero-to-next/src/components/todo/todo.tsx",
    "content": "import { useMemo } from 'react'\nimport Link from 'next/link'\nimport styled from 'styled-components'\n\nimport useToggle from '#hooks/useToggle'\nimport { Todo } from '#types/todo'\n\nconst Anchor = styled.a`\n  text-decoration: none;\n`\n\nexport function TodoComponent({ todos }: { todos: Array<Todo> }) {\n  return (\n    <ul>\n      {todos.map((todo) => (\n        <TodoItem key={todo.id} todo={todo} />\n      ))}\n    </ul>\n  )\n}\n\nexport function TodoItem({ todo }: { todo: Todo }) {\n  const { id, userId, complete, title } = todo\n  const [completed, toggle] = useToggle(complete)\n\n  const toggleId = useMemo(() => {\n    return `toggle_${id}`\n  }, [id])\n\n  return (\n    <li>\n      <input\n        id={toggleId}\n        type=\"checkbox\"\n        checked={completed}\n        onChange={toggle}\n      />\n\n      <label htmlFor={toggleId}>\n        <Link href={{ pathname: '/todos/[id]', query: { id } }} passHref>\n          <Anchor>{title}</Anchor>\n        </Link>{' '}\n        by {userId}\n      </label>\n    </li>\n  )\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/hooks/useToggle.ts",
    "content": "import { Reducer, useReducer } from 'react'\n\nconst toggleReducer = (value: boolean, nextValue?: unknown) =>\n  typeof nextValue === 'boolean' ? nextValue : !value\n\nexport default function useToggle(\n  initialValue = false,\n): [boolean, (nextValue?: unknown) => void] {\n  return useReducer<Reducer<boolean, unknown>>(toggleReducer, initialValue)\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/pages/_document.tsx",
    "content": "import Document, {\n  Html,\n  Head,\n  Main,\n  NextScript,\n  DocumentContext,\n  DocumentInitialProps,\n} from 'next/document'\nimport { ServerStyleSheet } from 'styled-components'\n\nexport default function MyDocument() {\n  return (\n    <Html lang=\"ko\">\n      <Head />\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  )\n}\n\nMyDocument.getInitialProps = async (\n  ctx: DocumentContext,\n): Promise<DocumentInitialProps> => {\n  const sheet = new ServerStyleSheet()\n  const originalRenderPage = ctx.renderPage\n\n  try {\n    ctx.renderPage = () =>\n      originalRenderPage({\n        enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),\n      })\n\n    const initialProps = await Document.getInitialProps(ctx)\n    return {\n      ...initialProps,\n      styles: (\n        <>\n          {initialProps.styles}\n          {sheet.getStyleElement()}\n        </>\n      ),\n    }\n  } finally {\n    sheet.seal()\n  }\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/pages/index.tsx",
    "content": "import { GetServerSideProps } from 'next'\n\nimport { Todo } from '#types/todo'\nimport { Title } from '#components/common/title'\nimport { TodoComponent } from '#components/todo/todo'\n\nexport default function Index({ todos }: { todos: Array<Todo> }) {\n  return (\n    <div>\n      <Title>Hello NextJs!</Title>\n      <TodoComponent todos={todos} />\n    </div>\n  )\n}\n\nexport const getServerSideProps: GetServerSideProps = async () => {\n  const response = await fetch('https://jsonplaceholder.typicode.com/todos')\n  const result = await response.json()\n\n  return {\n    props: {\n      todos: result,\n    },\n  }\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/pages/todos/[id].tsx",
    "content": "import { GetServerSideProps } from 'next'\n\nimport { Todo } from '#types/todo'\nimport withGetServerSideProps, { NotFoundError } from '#utils/errors'\n\nexport default function TodoItem({ todo }: { todo: Todo }) {\n  return <div>Todo: {todo.title}</div>\n}\n\nexport const getServerSideProps: GetServerSideProps = withGetServerSideProps(\n  async (ctx) => {\n    const id = ctx.params?.id\n\n    if (!id) {\n      throw new NotFoundError(id)\n    }\n\n    const response = await fetch(\n      `https://jsonplaceholder.typicode.com/posts/${id}`,\n    )\n\n    if (response.status === 404) {\n      throw new NotFoundError(id)\n    }\n\n    return {\n      props: {\n        todo: await response.json(),\n      },\n    }\n  },\n)\n"
  },
  {
    "path": "chapter9/zero-to-next/src/types/todo.ts",
    "content": "export interface Todo {\n  userId: number\n  id: number\n  title: string\n  complete: boolean\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/src/utils/errors/index.ts",
    "content": "import { GetServerSideProps, GetServerSidePropsContext } from 'next'\n\nexport class NotFoundError extends Error {\n  public constructor(private resourceId: unknown) {\n    super()\n  }\n\n  public get message() {\n    return `${this.resourceId} not found`\n  }\n}\n\nexport default function withGetServerSideProps(\n  getServerSideProps: GetServerSideProps,\n): GetServerSideProps {\n  return async (context: GetServerSidePropsContext) => {\n    try {\n      return await getServerSideProps(context)\n    } catch (error) {\n      if (error instanceof NotFoundError) {\n        return {\n          notFound: true,\n        }\n      }\n\n      throw error\n    }\n  }\n}\n"
  },
  {
    "path": "chapter9/zero-to-next/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"#pages/*\": [\"pages/*\"],\n      \"#hooks/*\": [\"hooks/*\"],\n      \"#types/*\": [\"types/*\"],\n      \"#components/*\": [\"components/*\"],\n      \"#utils/*\": [\"utils/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]