Repository: wikibook/react-deep-dive-example
Branch: main
Commit: 5012f2bd0a21
Files: 394
Total size: 253.5 KB
Directory structure:
gitextract_dby4eova/
├── .github/
│ └── workflows/
│ └── build.yaml
├── .gitignore
├── README.md
├── chapter10/
│ └── react-gradual-demo/
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierrc
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ └── index.html
│ └── src/
│ ├── index.js
│ ├── legacy/
│ │ ├── Greeting.js
│ │ ├── README.md
│ │ ├── createLegacyRoot.js
│ │ └── package.json
│ ├── modern/
│ │ ├── AboutPage.js
│ │ ├── App.js
│ │ ├── HomePage.js
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── lazyLegacyRoot.js
│ │ └── package.json
│ └── shared/
│ ├── Clock.js
│ ├── README.md
│ ├── ThemeContext.js
│ └── useTime.js
├── chapter11/
│ ├── next13/
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .npmrc
│ │ ├── .prettierignore
│ │ ├── .prettierrc
│ │ ├── README.md
│ │ ├── app/
│ │ │ ├── api/
│ │ │ │ ├── hello/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── posts/
│ │ │ │ │ ├── [id]/
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── route.ts
│ │ │ │ └── users/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── [id].ts
│ │ │ │ └── route.ts
│ │ │ ├── context/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── error/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── error.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── grouped-layouts/
│ │ │ │ ├── (main)/
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── (todos)/
│ │ │ │ │ ├── hello/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── todos/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── (users)/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── users/
│ │ │ │ └── page.tsx
│ │ │ ├── head/
│ │ │ │ ├── [userId]/
│ │ │ │ │ ├── head.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ └── sub.tsx
│ │ │ │ ├── head.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── head.tsx
│ │ │ ├── internal-api/
│ │ │ │ └── hello/
│ │ │ │ └── route.ts
│ │ │ ├── isr/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── layouts/
│ │ │ │ ├── [userId]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── loading/
│ │ │ │ ├── [userId]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ ├── server-action/
│ │ │ │ ├── form/
│ │ │ │ │ ├── [id]/
│ │ │ │ │ │ ├── loading.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── start-transition/
│ │ │ │ └── [id]/
│ │ │ │ └── page.tsx
│ │ │ ├── ssg/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── ssr/
│ │ │ │ ├── [id]/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── streaming/
│ │ │ │ ├── [id]/
│ │ │ │ │ ├── components.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ └── styles/
│ │ │ ├── css-modules/
│ │ │ │ ├── page.tsx
│ │ │ │ └── styles.module.css
│ │ │ ├── global-css/
│ │ │ │ ├── page.tsx
│ │ │ │ └── style.css
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── styled-components/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ └── styled-jsx/
│ │ │ ├── StyledRegistry.tsx
│ │ │ ├── components.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── middleware.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── Counter.tsx
│ │ │ │ ├── DefaultHeader.tsx
│ │ │ │ ├── ErrorButton.tsx
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ ├── StyledComponentsRegistry.tsx
│ │ │ │ ├── Tab.tsx
│ │ │ │ ├── TabGroup.tsx
│ │ │ │ ├── components.ts
│ │ │ │ └── server-action/
│ │ │ │ └── client-component.tsx
│ │ │ ├── constant/
│ │ │ │ └── menu.ts
│ │ │ ├── context/
│ │ │ │ └── counter.tsx
│ │ │ ├── lib/
│ │ │ │ └── utils.ts
│ │ │ ├── server-action/
│ │ │ │ └── index.ts
│ │ │ └── services/
│ │ │ ├── constant.ts
│ │ │ └── server.ts
│ │ ├── tailwind.config.js
│ │ └── tsconfig.json
│ └── server-components-demo/
│ ├── .gitignore
│ ├── .nvmrc
│ ├── .prettierignore
│ ├── .prettierrc.js
│ ├── CODE_OF_CONDUCT.md
│ ├── Dockerfile
│ ├── LICENSE
│ ├── README.md
│ ├── credentials.js
│ ├── docker-compose.yml
│ ├── notes/
│ │ └── .gitkeep
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ └── style.css
│ ├── scripts/
│ │ ├── build.js
│ │ ├── init_db.sh
│ │ └── seed.js
│ ├── server/
│ │ ├── api.server.js
│ │ └── package.json
│ └── src/
│ ├── App.js
│ ├── EditButton.js
│ ├── Note.js
│ ├── NoteEditor.js
│ ├── NoteList.js
│ ├── NoteListSkeleton.js
│ ├── NotePreview.js
│ ├── NoteSkeleton.js
│ ├── SearchField.js
│ ├── SidebarNote.js
│ ├── SidebarNoteContent.js
│ ├── Spinner.js
│ ├── TextWithMarkdown.js
│ ├── db.js
│ └── framework/
│ ├── bootstrap.js
│ └── router.js
├── chapter2/
│ └── react/
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .npmrc
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src/
│ │ ├── App.tsx
│ │ ├── index.tsx
│ │ ├── react-app-env.d.ts
│ │ └── routes/
│ │ ├── 2-1.tsx
│ │ ├── 2-4.tsx
│ │ ├── 2-5.tsx
│ │ ├── 2-7.tsx
│ │ └── 2-8.tsx
│ └── tsconfig.json
├── chapter4/
│ ├── next-example/
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── .prettierignore
│ │ ├── .prettierrc
│ │ ├── README.md
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── pages/
│ │ │ │ ├── 404.tsx
│ │ │ │ ├── 500.tsx
│ │ │ │ ├── _app.tsx
│ │ │ │ ├── _document.tsx
│ │ │ │ ├── _error.tsx
│ │ │ │ ├── api/
│ │ │ │ │ └── hello.ts
│ │ │ │ ├── hello/
│ │ │ │ │ ├── [greeting].tsx
│ │ │ │ │ └── world.tsx
│ │ │ │ ├── hello.tsx
│ │ │ │ ├── hi/
│ │ │ │ │ └── [...props].tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── todo/
│ │ │ │ └── [id].tsx
│ │ │ └── styles/
│ │ │ ├── Home.module.css
│ │ │ └── globals.css
│ │ └── tsconfig.json
│ └── ssr-example/
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── README.md
│ ├── checkStream.js
│ ├── package.json
│ ├── public/
│ │ ├── index-end.html
│ │ ├── index-front.html
│ │ └── index.html
│ ├── src/
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ └── Todo.tsx
│ │ ├── fetch/
│ │ │ └── index.ts
│ │ ├── index.tsx
│ │ └── server.ts
│ ├── tsconfig.json
│ ├── typings.d.ts
│ ├── watch-stream.js
│ └── webpack.config.js
├── chapter8/
│ ├── eslint-plugin-yceffort/
│ │ ├── .eslintrc.js
│ │ ├── .npmrc
│ │ ├── .prettierrc
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── rules/
│ │ │ └── no-new-date.md
│ │ ├── lib/
│ │ │ ├── index.js
│ │ │ └── rules/
│ │ │ └── no-new-date.js
│ │ ├── package.json
│ │ └── tests/
│ │ └── lib/
│ │ └── rules/
│ │ └── no-new-date.js
│ └── react-test/
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src/
│ │ ├── App.css
│ │ ├── App.test.tsx
│ │ ├── App.tsx
│ │ ├── components/
│ │ │ ├── FetchComponent/
│ │ │ │ ├── index.test.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── StateComponent/
│ │ │ │ ├── index.test.tsx
│ │ │ │ └── index.tsx
│ │ │ └── StaticComponent/
│ │ │ ├── index.test.tsx
│ │ │ └── index.tsx
│ │ ├── hooks/
│ │ │ ├── useEffectDebugger.test.ts
│ │ │ └── useEffectDebugger.ts
│ │ ├── index.css
│ │ ├── index.tsx
│ │ ├── react-app-env.d.ts
│ │ ├── reportWebVitals.ts
│ │ └── setupTests.ts
│ └── tsconfig.json
└── chapter9/
├── danger-react-app/
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .npmrc
│ ├── .prettierignore
│ ├── .prettierrc
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ ├── manifest.json
│ │ └── robots.txt
│ └── src/
│ ├── App.css
│ ├── App.js
│ ├── index.css
│ └── index.js
├── deploy/
│ ├── aws/
│ │ ├── cra/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── public/
│ │ │ │ ├── index.html
│ │ │ │ ├── manifest.json
│ │ │ │ └── robots.txt
│ │ │ ├── src/
│ │ │ │ ├── App.css
│ │ │ │ ├── App.test.tsx
│ │ │ │ ├── App.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.tsx
│ │ │ │ ├── react-app-env.d.ts
│ │ │ │ ├── reportWebVitals.ts
│ │ │ │ └── setupTests.ts
│ │ │ └── tsconfig.json
│ │ └── next/
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── _app.tsx
│ │ │ ├── api/
│ │ │ │ └── hello.ts
│ │ │ └── index.tsx
│ │ ├── styles/
│ │ │ ├── Home.module.css
│ │ │ └── globals.css
│ │ └── tsconfig.json
│ ├── digitalocean/
│ │ ├── cra/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── public/
│ │ │ │ ├── index.html
│ │ │ │ ├── manifest.json
│ │ │ │ └── robots.txt
│ │ │ └── src/
│ │ │ ├── App.css
│ │ │ ├── App.js
│ │ │ ├── App.test.js
│ │ │ ├── index.css
│ │ │ ├── index.js
│ │ │ ├── reportWebVitals.js
│ │ │ └── setupTests.js
│ │ └── next/
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── _app.tsx
│ │ │ ├── api/
│ │ │ │ └── hello.ts
│ │ │ └── index.tsx
│ │ ├── styles/
│ │ │ ├── Home.module.css
│ │ │ └── globals.css
│ │ └── tsconfig.json
│ ├── netlify/
│ │ ├── cra/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── public/
│ │ │ │ ├── index.html
│ │ │ │ ├── manifest.json
│ │ │ │ └── robots.txt
│ │ │ ├── src/
│ │ │ │ ├── App.css
│ │ │ │ ├── App.test.tsx
│ │ │ │ ├── App.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.tsx
│ │ │ │ ├── react-app-env.d.ts
│ │ │ │ ├── reportWebVitals.ts
│ │ │ │ └── setupTests.ts
│ │ │ └── tsconfig.json
│ │ └── next/
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── netlify.toml
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── _app.tsx
│ │ │ ├── api/
│ │ │ │ └── hello.ts
│ │ │ ├── hello.tsx
│ │ │ └── index.tsx
│ │ ├── styles/
│ │ │ ├── Home.module.css
│ │ │ └── globals.css
│ │ └── tsconfig.json
│ └── vercel/
│ ├── cra/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── index.html
│ │ │ ├── manifest.json
│ │ │ └── robots.txt
│ │ ├── src/
│ │ │ ├── App.css
│ │ │ ├── App.test.tsx
│ │ │ ├── App.tsx
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── react-app-env.d.ts
│ │ │ ├── reportWebVitals.ts
│ │ │ └── setupTests.ts
│ │ └── tsconfig.json
│ └── next/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── next.config.js
│ ├── package.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── api/
│ │ │ └── hello.ts
│ │ └── index.tsx
│ ├── styles/
│ │ ├── Home.module.css
│ │ └── globals.css
│ └── tsconfig.json
└── zero-to-next/
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── README.md
├── next.config.js
├── package.json
├── src/
│ ├── _app.tsx
│ ├── components/
│ │ ├── common/
│ │ │ └── title.tsx
│ │ └── todo/
│ │ └── todo.tsx
│ ├── hooks/
│ │ └── useToggle.ts
│ ├── pages/
│ │ ├── _document.tsx
│ │ ├── index.tsx
│ │ └── todos/
│ │ └── [id].tsx
│ ├── types/
│ │ └── todo.ts
│ └── utils/
│ └── errors/
│ └── index.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yaml
================================================
name: chapter9 build
run-name: ${{ github. actor }} has been added new commit.
on:
push:
branches-ignore:
- 'main'
paths:
- ./chapter9/zero-to-next
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: 'install dependencies'
working-directory: ./chapter9/zero-to-next
run: npm ci
- name: 'build'
working-directory: ./chapter9/zero-to-next
run: npm run build
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
build/
.vscode
.DS_Store
.idea
================================================
FILE: README.md
================================================
# react-deep-dive-example
《모던 리액트 Deep Dive》 예제 코드입니다.
## Table of Contents
### 2장 리액트 핵심 요소 깊게 살펴보기 [📁](./chapter2)
#### react [📁](./chapter2/react)
2장에서 다룬 리액트와 관련된 리액트 예제를 모아두었습니다.
### chapter4 서버 사이드 렌더링 [📁](./chapter4)
#### ssr-example [📁](chapter4/ssr-example)
4-2 장에서 다룬 리액트 서버사이드 렌더링 api를 바탕으로 실제 리액트 api 를 기반으로 바닐라 서버사이드 렌더링 애플리케이션을 만든 예제 애플리케이션입니다.
#### next-example [📁](chapter4/next-example)
4-3 장에서 다룬 nextjs 에 대한 예제 애플리케이션입니다.
### chapter8 좋은 리액트 코드 작성을 위한 환경 구축하기 [📁](./chapter8)
#### eslint-plugin-yceffort [📁](chapter8/eslint-plugin-yceffort)
8-1 장에서 다룬 사용자 정의 `eslint-plugin`을 만들어본 예제 입니다. `eslint-plugin-yceffort`라는 이름으로 만들어졌습니다.
#### react-test [📁](chapter8/react-test)
8-2 장에서 다룬 리액트 테스트 코드관련 예제 코드를 모아두었습니다.
### chapter9 모던 리액트 개발 도구로 개발 및 배포 환경 구축하기 [📁](./chapter9)
#### zero-to-next [📁](chapter9/zero-to-next)
9-1장 에서 다룬 빈 폴더에서 부터 nextjs 애플리케이션을 만들어본 예제입니다.
#### danger-react-app [📁](chapter9/danger-react-app)
9-2장 에서 다룬 보안 취약점이 있는 리액트 애플리케이션을 수정한 예제 입니다. 주요 보안 이슈를 수정했지만 여전히 잠재적인 보안 취약점이 있을 수 있으므로 사용하는 것을 권장하지 않습니다.
#### deploy [📁](chapter9/deploy)
9-3장에서 다뤘던 여러 SaaS 서비스에 배포 해본 예제입니다.
### chapter10 리액트 17과 18의 변경사항 살펴보기 [📁](./chapter10)
#### react-gradual-demo [📁](chapter10/react-gradual-demo)
10-1장 리액트 17의 변경에 대해서 다룬 내용 중 하나인 리액트의 점진적인 업데이트를 구현해본 예제 애플리케이션입니다.
### chapter11 Next.js13과 리액트 18 [📁](./chapter11)
#### server-components-demo [📁](chapter11/server-components-demo)
11-2 장에서 소개한 서버 컴포넌트에 대한 예제 애플리케이션입니다. nextjs와 같은 프레임워크를 사용하지 않은 순수 리액트 서버 컴포넌트 예제 입니다.
#### next13 [📁](chapter11/next13)
11-3 장에서 다룬 next@13 이상 버전에서 제공 되고 있는 app directory 를 활용한 예제 애플리케이션입니다.
================================================
FILE: chapter10/react-gradual-demo/.eslintignore
================================================
node_modules
build
src/*/shared
================================================
FILE: chapter10/react-gradual-demo/.eslintrc.js
================================================
module.exports = {
extends: [
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter10/react-gradual-demo/.gitignore
================================================
node_modules
build
.DS_Store
src/*/shared
================================================
FILE: chapter10/react-gradual-demo/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter10/react-gradual-demo/LICENSE
================================================
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: chapter10/react-gradual-demo/README.md
================================================
# Demo of Gradual React Upgrades
https://github.com/reactjs/react-gradual-upgrade-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 프로젝트입니다.
## 프로젝트 구조
### modern
react@17.x 로 구성된 프로젝트이며, gradual demo를 실행하는 기본 리액트 프로젝트 입니다.
### legacy
react@16.x로 구성된 프로젝트이며, modern에서 import 하여 사용되는 프로젝트 입니다.
### shared
`modern`, `legacy` 두 개의 프로젝트에서 모두 사용되는 파일로 구성되어 있으며, 훅과 Context를 제공합니다. 훅과 context 가 `modern`과 `legacy` 모두에서 사용될 수 있음을 보여주기 위해 만들어진 예제 파일입니다.
## 동작 방식
1. 프로젝트를 빌드 합니다.
2. 빌드와 동시에 `shared`에 있는 내용을 각각 `modern`과 `legacy`로 복사합니다. 이는 마치 `npm`에서 업로드된 라이브러리를 사용하는 것과 비슷한 구조로 동작합니다.
3. 프로젝트를 시작합니다.
================================================
FILE: chapter10/react-gradual-demo/package.json
================================================
{
"name": "react-gradual-demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"react-scripts": "^5.0.1"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"cpx": "1.5.0",
"eslint": "^8.38.0",
"npm-run-all": "4.1.5",
"prettier": "^2.8.7"
},
"scripts": {
"//": "watch:* 명령어와 함께 react-script로 앱 시작",
"postinstall": "run-s install:*",
"install:legacy": "cd src/legacy && npm install",
"install:modern": "cd src/modern && npm install",
"copy:legacy": "cpx 'src/shared/**' 'src/legacy/shared/'",
"copy:modern": "cpx 'src/shared/**' 'src/modern/shared/'",
"watch:legacy": "cpx 'src/shared/**' 'src/legacy/shared/' --watch --no-initial",
"watch:modern": "cpx 'src/shared/**' 'src/modern/shared/' --watch --no-initial",
"prebuild": "run-p copy:*",
"prestart": "run-p copy:*",
"start": "run-p start-app watch:*",
"start-app": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: chapter10/react-gradual-demo/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter10/react-gradual-demo/src/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import './modern/index';
================================================
FILE: chapter10/react-gradual-demo/src/legacy/Greeting.js
================================================
import React, {Component} from 'react';
import {findDOMNode} from 'react-dom';
import ThemeContext from './shared/ThemeContext';
import Clock from './shared/Clock';
export default class AboutSection extends Component {
componentDidMount() {
// eslint-disable-next-line react/no-find-dom-node
const legacyDomNode = findDOMNode(this);
// eslint-disable-next-line no-console
console.log(legacyDomNode)
}
handleClick = () => {
// eslint-disable-next-line no-console
console.log('hello')
}
render() {
return (
{theme => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
src/legacy/Greeting.js
This component is rendered by the nested React ({React.version}).
)}
);
}
}
================================================
FILE: chapter10/react-gradual-demo/src/legacy/README.md
================================================
# legacy
`react@16`을 기반으로 작성된 컴포넌트
================================================
FILE: chapter10/react-gradual-demo/src/legacy/createLegacyRoot.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import ThemeContext from './shared/ThemeContext';
export default function createLegacyRoot(container) {
return {
// 렌더링
render(Component, props, context) {
ReactDOM.render(
,
container
);
},
// 이 컴포넌트의 부모 컴포넌트가 제거될 때 호출될 unmount
unmount() {
ReactDOM.unmountComponentAtNode(container);
},
};
}
================================================
FILE: chapter10/react-gradual-demo/src/legacy/package.json
================================================
{
"private": true,
"name": "react@16 application",
"dependencies": {
"react": "16.8",
"react-dom": "16.8"
}
}
================================================
FILE: chapter10/react-gradual-demo/src/modern/AboutPage.js
================================================
import React, {useContext} from 'react';
import Clock from './shared/Clock';
import ThemeContext from './shared/ThemeContext';
import lazyLegacyRoot from './lazyLegacyRoot';
const Greeting = lazyLegacyRoot(() => import('../legacy/Greeting'));
export default function AboutPage() {
const theme = useContext(ThemeContext);
return (
<>
src/modern/AboutPage.js
This component is rendered by the outer React ({React.version}).
>
);
}
================================================
FILE: chapter10/react-gradual-demo/src/modern/App.js
================================================
import React, {useState, Suspense} from 'react';
import AboutPage from './AboutPage';
import ThemeContext from './shared/ThemeContext';
export default function App() {
const [theme, setTheme] = useState('slategrey');
function handleToggleClick() {
if (theme === 'slategrey') {
setTheme('hotpink');
} else {
setTheme('slategrey');
}
}
return (
);
}
function Spinner() {
return null;
}
================================================
FILE: chapter10/react-gradual-demo/src/modern/HomePage.js
================================================
import React, {useContext} from 'react';
import ThemeContext from './shared/ThemeContext';
import Clock from './shared/Clock';
export default function HomePage() {
const theme = useContext(ThemeContext);
return (
<>
src/modern/HomePage.js
This component is rendered by the outer React ({React.version}).
>
);
}
================================================
FILE: chapter10/react-gradual-demo/src/modern/README.md
================================================
# modern
`react@17`을 기반으로 작성된 컴포넌트가 모여있으며, 애플리케이션의 시작점이다. `react@17` 을 루트에 선언해 두면, 자식 컴포넌트의 리액트 버전은 17 외에도 가능하다.
================================================
FILE: chapter10/react-gradual-demo/src/modern/index.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {StrictMode} from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
,
document.getElementById('root')
);
================================================
FILE: chapter10/react-gradual-demo/src/modern/lazyLegacyRoot.js
================================================
import React, {useContext, useMemo, useRef, useLayoutEffect} from 'react';
import ThemeContext from './shared/ThemeContext';
const rendererModule = {
status: 'pending',
promise: null,
result: null,
};
export default function lazyLegacyRoot(getLegacyComponent) {
const componentModule = {
status: 'pending',
promise: null,
result: null,
};
return function Wrapper(props) {
// legacy/createLegacyRoot 를 promise 로 layzy 하게 불러온다.
const createLegacyRoot = readModule(rendererModule, () =>
import('../legacy/createLegacyRoot')
).default;
const Component = readModule(componentModule, getLegacyComponent).default;
// 구 리액트를 렌더링할 위치
const containerRef = useRef(null);
// 구 리액트의 루트 컴포넌트
const rootRef = useRef(null);
const theme = useContext(ThemeContext);
const context = useMemo(
() => ({
theme,
}),
[theme]
);
useLayoutEffect(() => {
// 루트 컴포넌트가 없다면
if (!rootRef.current) {
// 루트 컴포넌트를 만든다.
rootRef.current = createLegacyRoot(containerRef.current);
}
const root = rootRef.current;
// cleanUp 시에 unmount
return () => {
root.unmount();
};
}, [createLegacyRoot]);
useLayoutEffect(() => {
if (rootRef.current) {
// 루트 컴포넌트가 존재하면 적절한 props와 context로 렌더링한다.
rootRef.current.render(Component, props, context);
}
}, [Component, props, context]);
return
;
};
}
function readModule(record, importStatement) {
// promise가 없으면 아직 import 하지 못한 것이므로 import 를 실행한다.
if (!record.promise) {
/* eslint-disable */
record.promise = importStatement().then(
value => {
if (record.status === 'pending') {
record.status = 'fulfilled';
record.promise = null;
// 성공시 import 반환 값
record.result = value;
return value
}
},
error => {
if (record.status === 'pending') {
record.status = 'rejected';
record.promise = null;
// 실패시 에러
record.result = error;
}
}
);
}
// 성공 또는 실패시에 결과를 반환한다.
if (record.status === 'fulfilled' || record.status === 'rejected') {
return record.result;
}
throw record.promise;
}
================================================
FILE: chapter10/react-gradual-demo/src/modern/package.json
================================================
{
"private": true,
"name": "react@17 application",
"dependencies": {
"react": "17.0.0",
"react-dom": "17.0.0"
}
}
================================================
FILE: chapter10/react-gradual-demo/src/shared/Clock.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import useTime from './useTime';
export default function Clock() {
const time = useTime();
return Time: {time}
;
}
================================================
FILE: chapter10/react-gradual-demo/src/shared/README.md
================================================
# @shared
`react@16`과 `react@17`에서 공통으로 사용하는 패키지. npm 에서 제공하는 리액트 라이브러리와 비슷하게 보면 된다.
================================================
FILE: chapter10/react-gradual-demo/src/shared/ThemeContext.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {createContext} from 'react';
const ThemeContext = createContext(null);
export default ThemeContext;
================================================
FILE: chapter10/react-gradual-demo/src/shared/useTime.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useState, useEffect} from 'react';
export default function useTimer() {
const [value, setValue] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => {
setValue(new Date());
}, 1000);
return () => clearInterval(id);
}, []);
return value.toLocaleTimeString();
}
================================================
FILE: chapter11/next13/.eslintignore
================================================
.next
node_modules
================================================
FILE: chapter11/next13/.eslintrc.js
================================================
module.exports = {
extends: [
'next/core-web-vitals',
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
rules: {
'@typescript-eslint/naming-convention': ['off'],
},
}
================================================
FILE: chapter11/next13/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter11/next13/.npmrc
================================================
legacy-peer-deps=true
================================================
FILE: chapter11/next13/.prettierignore
================================================
.next
node_modules
================================================
FILE: chapter11/next13/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter11/next13/README.md
================================================
# Chapter8 react@18 nextjs@13 예제
https://react-deep-dive-example-six.vercel.app/
https://github.com/vercel/app-playground 저장소에서 제공하는 예제를 조금 더 간결하고 이해하기 쉽도록 재구성한 저장소입니다. 스타일과 예제 내역을 참고했으며, 구체적인 예제는 코드 설명을 위해 조금씩 수정을 가미했습니다. 원본 예제를 알고 싶다면 vercel의 원래 저장소를 참고해주세요.
## warning
- 스타일이 대부분 [tailwindcss](https://tailwindcss.com/)를 기반으로 작성되어 있기 때문에 `className`이 조금 지저분 할 수 있습니다. `className`은 대부분 스타일을 위해 사용되고 있으니 굳이 `className`을 이해하실 필요는 없습니다.
- 2023년 5월 기준 `typescript@5.1.0-beta` 와 `styled-components@6.0.0-rc.1`가 일부 리액트 서버 컴포넌트 관련한 코드를 지원하기 시작하여 release candidate 버전임에도 설치했습니다.
- `typescript@5.1.0-beta` 설치로 인해 아래와 같이 `peerDependencies`의 버전을 제대로 추론하지 못하는 문제를 해결하기 위해 `.npmrc`에 `legacy-peer-deps=true` 옵션을 추가했습니다. 이 문제는 향후에 `typescript@5.1.0`이 정식으로 출시되면 해결 될 것입니다.
```text
npm ERR! Could not resolve dependency:
npm ERR! peerOptional typescript@">=3.3.1" from eslint-config-next@13.4.1
npm ERR! node_modules/eslint-config-next
npm ERR! dev eslint-config-next@"^13.4.0" from the root project
npm ERR!
npm ERR! Conflicting peer dependency: typescript@5.0.4
npm ERR! node_modules/typescript
npm ERR! peerOptional typescript@">=3.3.1" from eslint-config-next@13.4.1
npm ERR! node_modules/eslint-config-next
npm ERR! dev eslint-config-next@"^13.4.0" from the root project
```
================================================
FILE: chapter11/next13/app/api/hello/route.ts
================================================
export async function GET() {
return new Response(JSON.stringify({ name: 'John Doe' }), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/api/posts/[id]/route.ts
================================================
import type { NextRequest } from 'next/server'
export async function GET(
req: NextRequest,
context: { params: { id: string } },
) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${context.params.id}`,
)
console.log(context)
const result = await response.json()
const now = new Date()
const nowStr = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`
// eslint-disable-next-line no-console
console.log('request has been resolved', nowStr)
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/api/posts/route.ts
================================================
import type { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const result = await response.json()
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/api/users/[id]/route.ts
================================================
import type { NextRequest } from 'next/server'
export async function GET(
request: NextRequest,
context: { params: { id: string } },
) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${context.params.id}`,
)
const result = await response.json()
const now = new Date()
const nowStr = `${now.toLocaleDateString()} ${now.toLocaleTimeString()}`
// eslint-disable-next-line no-console
console.log('request has been resolved', nowStr)
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/api/users/[id].ts
================================================
================================================
FILE: chapter11/next13/app/api/users/route.ts
================================================
export async function GET() {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const result = await response.json()
return new Response(JSON.stringify(result), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/context/[id]/page.tsx
================================================
import Counter from '#components/Counter'
import { fetchUserById } from '#services/server'
export default async function Page({ params }: { params: { id: string } }) {
const user = await fetchUserById(params.id)
return (
이름: {user.name}
)
}
================================================
FILE: chapter11/next13/app/context/layout.tsx
================================================
import { ReactNode } from 'react'
import { fetchUsers } from '#services/server'
import { CounterProvider } from '#context/counter'
import { TabGroup } from '#components/TabGroup'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/context/page.tsx
================================================
export default function Page() {
return (
Client Context
Context는 클라이언트 컴포넌트로, 기존에 리액트에서 사용하던 Context
문법을 그대로 사용하면 서버와 클라이언트 컴포넌트 모두에서 사용할 수
있다.
Context는 상태를 가지고 있어야 하므로 클라이언트 컴포넌트가 될 수
밖에 없으며, 반드시 파일 상단에 "use client"를 선언해주어야 한다.
Context.Provider로 하위 라우팅을 감싸주면, 라우팅 내부에서 이동이
발생하더라도 Context 내부의 값잉 유지되는 것을 볼 수 있다.
)
}
================================================
FILE: chapter11/next13/app/error/[id]/page.tsx
================================================
import { notFound } from 'next/navigation'
import { ReactNode } from 'react'
export default function Page(): ReactNode | Promise {
if (true.toString() === 'true') {
notFound()
}
return <>렌더링 되지 않습니다.>
}
================================================
FILE: chapter11/next13/app/error/error.tsx
================================================
'use client'
import { useEffect } from 'react'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function Error({ error, reset }: any) {
useEffect(() => {
// eslint-disable-next-line no-console
console.log('logging error:', error)
}, [error])
return (
Error: {error?.message}
reset()}
>
에러 리셋
)
}
================================================
FILE: chapter11/next13/app/error/layout.tsx
================================================
import { ReactNode } from 'react'
import { fetchUsers } from '#services/server'
import { TabGroup } from '#components/TabGroup'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/error/not-found.tsx
================================================
export default function NotFound() {
return '404 입니다😭'
}
================================================
FILE: chapter11/next13/app/error/page.tsx
================================================
import ErrorButton from '#components/ErrorButton'
export default function Page() {
return (
Error Handling
`error`는 nextjs의 또 다른 예약어로, 해당 라우트 내부에서 사용가능한
에러 바운더리를 정의할 수 있는 파일이다.
에러 버튼을 눌러보자. 해당 라우트 내부의 레이아웃과 페이지에만 영향을
미치며, 여전히 다른 페이지는 상호작용이 가능하다.
`not-found`파일을 활용하면 해당 라우트 내부의 404 페이지를 정의할 수
있다.
유저 버튼을 누르면 /error/{'{id}'}로 이동하는데 이 페이지는
`notFound()`를 실행하여 404페이지로 보낸다.
주의: `error`는 반드시 클라이언트 컴포넌트여야 한다.
)
}
================================================
FILE: chapter11/next13/app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: chapter11/next13/app/grouped-layouts/(main)/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
export default async function Layout({ children }: { children: ReactNode }) {
return (
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(main)/page.tsx
================================================
export default function Page() {
return (
Route Groups
라우팅 그룹은 URL 구조에 영향을 주지 않으면서, 주소에 따라 서로다른
레이아웃을 적용할 수 있는 방법이다.
만약 대표 루트 URL 페이지가 선언되어 있지 않다면 `(main)`을 해당
페이지의 루트로 간주하여 라우팅한다.
서로 다른 탭을 네비게이션 해보면, 주소에는 영향이 없지만 주소에
따라서 서로 다른 레이아웃을 적용할 수 있다는 것을 알 수 있다.
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(todos)/hello/page.tsx
================================================
export default async function Page() {
return (
url: /groped-layouts/hello
안녕하세요.
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(todos)/layout.tsx
================================================
import { ReactNode } from 'react'
export default async function Layout({ children }: { children: ReactNode }) {
return (
여기는 /groped-layout/(todos) 입니다.
{children}
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(todos)/todos/page.tsx
================================================
import { fetchTodos } from '#services/server'
export default async function Page() {
const todos = await fetchTodos()
return (
url: /groped-layouts/todos
{todos.map((todo) => (
{todo.title}
))}
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(users)/layout.tsx
================================================
import { ReactNode } from 'react'
export default async function Layout({ children }: { children: ReactNode }) {
return (
여기는 /groped-layout/(user) 입니다.
{children}
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(users)/users/page.tsx
================================================
import { fetchUsers } from '#services/server'
export default async function Page() {
const users = await fetchUsers()
return (
url: /groped-layouts/users
{users.map((user) => (
{user.name}
))}
)
}
================================================
FILE: chapter11/next13/app/head/[userId]/head.tsx
================================================
import DefaultHeader from '#components/DefaultHeader'
import { API_URL_BASE } from '#services/constant'
export default async function Head({ params }: { params: { userId: string } }) {
const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)
const user = await response.json()
return (
<>
{user.name}
>
)
}
================================================
FILE: chapter11/next13/app/head/[userId]/page.tsx
================================================
import SubPage from './sub'
import { API_URL_BASE } from '#services/constant'
export default async function Page({ params }: { params: { userId: string } }) {
const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)
const user = await response.json()
return (
<>
이름: {user.name}
{/* @ts-expect-error Async Server Component */}
>
)
}
================================================
FILE: chapter11/next13/app/head/[userId]/sub.tsx
================================================
import { API_URL_BASE } from '#services/constant'
export default async function SubPage({
params,
}: {
params: { userId: string }
}) {
const response = await fetch(`${API_URL_BASE}/api/users/${params.userId}`)
const user = await response.json()
return (
서브페이지 이름: {user.name}
)
}
================================================
FILE: chapter11/next13/app/head/head.tsx
================================================
import DefaultHeader from '#components/DefaultHeader'
export default function Head() {
return (
<>
라우트 내부에서 head를 재정의할 수 있습니다.
>
)
}
================================================
FILE: chapter11/next13/app/head/layout.tsx
================================================
import { ReactNode } from 'react'
import { fetchUsers } from '#services/server'
import { TabGroup } from '#components/TabGroup'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/head/page.tsx
================================================
export default function Page() {
return (
head 태그 설정하기
`head`를 활용하면 라우트 내부의 {''}를 원하는 대로 커스텀할 수
있다.
또한 `head`내부에서 데이터를 불러와서 동적으로 결정하는 것 또한
가능하다.
Next will dedupe requests for the same data across `layout.js`,
`page.js` and `head.js` when rendering a route.
nextjs는 `head` 내부에 데이터 요청이 있다면 이 요청이 완료되고{' '}
{''}가 렌더링이 완료될 때 까지 기다린다. 이는 첫번째 스트리밍
응답에 무조건 {''}가 포함될 수 있도록 보장해준다.
추가로 같은 라우트 내부에서 발생하는 같은 중복 api 요청에 대한
처리도 잘되어 있는 것을 확인할 수 있다. 이는 프로덕션 모드에서만
확인가능하며, {'/head/{id}'}페이지에서 같은 api를 3차례 부르지만
한번만 요청이 가는 것을 확인할 수 있다.
클라이언트에서의 중복처리도 원래 가능한 것으로 알려져 있지만, 현재
클라이언트 요청에 대한 중복처리는 아직 개발중인 것으로 보인다.
(2023년 1월 기준)
관련 링크 보기
)
}
================================================
FILE: chapter11/next13/app/head.tsx
================================================
import DefaultHeader from '../src/components/DefaultHeader'
export default function Head() {
return (
<>
Next@13 예제
>
)
}
================================================
FILE: chapter11/next13/app/internal-api/hello/route.ts
================================================
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
return new Response(JSON.stringify({ name: 'hello' }), {
status: 200,
headers: {
'content-type': 'application/json',
},
})
}
================================================
FILE: chapter11/next13/app/isr/[id]/page.tsx
================================================
import { fetchPostById } from '#services/server'
export const dynamicParams = true
export const revalidate = 15 // revalidate this page every 60 seconds
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]
}
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetchPostById(params.id)
console.log(`generate page ${params.id}`)
return (
마지막 렌더링 시간 (프로덕션 모드만 확인 가능): UTC{' '}
{new Date().toLocaleTimeString()}
{data.title}
{data.body}
)
}
================================================
FILE: chapter11/next13/app/isr/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
const ids = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]
export default function Layout({ children }: { children: ReactNode }) {
return (
({
text: `Post ${x.id}`,
slug: x.id,
})),
]}
/>
{children}
)
}
================================================
FILE: chapter11/next13/app/isr/page.tsx
================================================
export default function Page() {
return (
Incremental Static Regeneration
이 예제에서는 과거 `getStaticProps`와 `revalidate`의 조합으로
제공하는 `incremental static regeneration`을 구현한 예제다.
이 하위 페이지들은 15초 간격으로 페이지를 재생성하며, 15초 이내에
방문한 사용자에 대해서는 기존에 캐싱된 페이지를 보여준다.
먼저 최초에 페이지를 방문하면 캐싱된 페이지를 보여준다. 그리고 만약
그 방문 시점이 revalidate시간, 즉 생성후 15초를 지났다면 새로
페이지를 다시 만들고, 그 이후에 방문한 사용자에게는 재생성한
페이지를 보여준다.
우측 상단의 마지막으로 렌더링된 시간을 주목해서 살펴보자.
)
}
================================================
FILE: chapter11/next13/app/layout.tsx
================================================
import './globals.css'
import { ReactNode } from 'react'
import SideBar from '#components/Sidebar'
export default function Layout({ children }: { children: ReactNode }) {
return (
)
}
================================================
FILE: chapter11/next13/app/layouts/[userId]/page.tsx
================================================
import { fetchUserById } from '#services/server'
export default async function Page({ params }: { params: { userId: string } }) {
const user = await fetchUserById(params.userId)
if (!user) {
return null
}
return (
이름: {user.name}
)
}
================================================
FILE: chapter11/next13/app/layouts/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
import { fetchUsers } from '#services/server'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/layouts/page.tsx
================================================
export default function Page() {
return (
Layouts
레이아웃은 특정 주소 내부에 공유할 수 있는 UI 를 말한다.
네비게이션이 발생하더라도 레이아웃은 그 상태를 유지하고, 다시
렌더링하지 않는다.
레이아웃은 여러 페이지에 걸쳐 중첩하는 것 또한 가능하다.
)
}
================================================
FILE: chapter11/next13/app/loading/[userId]/page.tsx
================================================
import { sleep } from '#lib/utils'
import { fetchUserById } from '#services/server'
export default async function Page({ params }: { params: { userId: string } }) {
await sleep(5 * 1000)
const user = await fetchUserById(params.userId)
if (!user) {
return null
}
return (
이름: {user.name}
)
}
================================================
FILE: chapter11/next13/app/loading/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
import { fetchUsers } from '#services/server'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/loading/loading.tsx
================================================
export default function Loading() {
return (
)
}
================================================
FILE: chapter11/next13/app/loading/page.tsx
================================================
export default function Page() {
return (
Loading
파일명 loading은 nextjs에서 사용하는 예약어로, 페이지가 아직 렌더링
준비가 되지 않았을 때 노출되는 컴포넌트다.
Streaming 예제의 Suspense와 다르게, 별도로 Suspense로 감싸지 않아도
하위 라우팅 내부에서 공통으로 사용할 수 있다는 장점이 있다.
유저를 클릭하면, 유저에 해당하는 컴포넌트가 렌더링 되기 전까지
loading.tsx가 잠깐 노출되는 것을 확인할 수 있다.
)
}
================================================
FILE: chapter11/next13/app/page.tsx
================================================
import Link from 'next/link'
import { demos } from '../src/constant/menu'
export default function Page() {
return (
Examples
{demos.map((section) => {
return (
{section.name}
{section.items.map((item) => {
return (
{item.name}
{item.description ? (
{item.description}
) : null}
)
})}
)
})}
)
}
================================================
FILE: chapter11/next13/app/server-action/form/[id]/loading.tsx
================================================
export default function Loading() {
return (
)
}
================================================
FILE: chapter11/next13/app/server-action/form/[id]/page.tsx
================================================
import kv from '@vercel/kv'
import { revalidatePath } from 'next/cache'
interface Data {
name: string
age: number
}
export default async function Page({ params }: { params: { id: string } }) {
const key = `test:${params.id}`
const data = await kv.get(key)
async function handleSubmit(formData: FormData) {
'use server'
const name = formData.get('name')
const age = formData.get('age')
await kv.set(key, {
name,
age,
})
revalidatePath(`/server-action/form/${params.id}`)
}
return (
form with data
서버에 저장된 정보: {data?.name} {data?.age}
아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.
)
}
================================================
FILE: chapter11/next13/app/server-action/form/page.tsx
================================================
export default function Page() {
async function handleSubmit() {
'use server'
console.log('해당 작업은 서버에서 수행합니다. 따라서 CORS 이슈가 없습니다.')
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'post',
body: JSON.stringify({
title: 'foo',
body: 'bar',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const result = await response.json()
console.log(result)
}
return (
form
아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.
)
}
================================================
FILE: chapter11/next13/app/server-action/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
export default async function Layout({ children }: { children: ReactNode }) {
const items = [
{
text: 'Home',
},
]
return (
)
}
================================================
FILE: chapter11/next13/app/server-action/page.tsx
================================================
export default function Page() {
return (
Server action (alpha)
서버 액션은 컴포넌트에서 직접 서버사이드 데이터 조작을 할 수 있게
해주는 nextjs의 새로운 기능이다.
13.4.0 기준으로 실험 기능이므로, `next.config.js`에서
`experimental.serverActions = true`로 설정해두어야 한다.
서버 액션에서 할 수 있는 것들을 서브 메뉴로 확인해보자.
)
}
================================================
FILE: chapter11/next13/app/server-action/start-transition/[id]/page.tsx
================================================
import kv from '@vercel/kv'
import { ClientButtonComponent } from '#components/server-action/client-component'
interface Data {
name: string
age: number
}
export default async function Page({ params }: { params: { id: string } }) {
const key = `test:${params.id}`
const data = await kv.get(key)
return (
form with data
서버에 저장된 정보: {data?.name} {data?.age}
아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.
이 작업은 useTransition을 기반으로 실행됩니다.
)
}
================================================
FILE: chapter11/next13/app/ssg/[id]/page.tsx
================================================
import { fetchPostById } from '#services/server'
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]
}
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetchPostById(params.id)
return (
)
}
================================================
FILE: chapter11/next13/app/ssg/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
const ids = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }]
export default function Layout({ children }: { children: ReactNode }) {
return (
({
text: `Post ${x.id}`,
slug: x.id,
})),
]}
/>
마지막 렌더링 시간 (프로덕션 모드만 확인 가능)
{new Date().toLocaleTimeString()}
{children}
)
}
================================================
FILE: chapter11/next13/app/ssg/page.tsx
================================================
export default function Page() {
return (
Static-Site Generation
이 예제는 과거 `getStaticProps`와 `getStaticPaths`를 구현한 예제다.
`getStaticPaths`는 `generateStaticParams`으로 대체되었으며, 데이터를
불러오는 것은 `fetch`를 사용하는 것으로 동일하다. 최초 빌드시에 미리
데이터를 불러오고, 이후 재요청이 있으면 계속 해당 데이터를 사용한다.
미리 빌드된 페이지를 확인하고 싶다면, `./next/server/app/ssg`로
이동해서 확인해보면 된다. 미리 빌드된 html 파일이 준비되어 있을
것이다.
)
}
================================================
FILE: chapter11/next13/app/ssr/[id]/page.tsx
================================================
import { fetchPostById } from '#services/server'
export default async function Page({ params }: { params: { id: string } }) {
const data = await fetchPostById(params.id, { cache: 'no-cache' })
return (
)
}
================================================
FILE: chapter11/next13/app/ssr/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
import { fetchUsers } from '#services/server'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/ssr/page.tsx
================================================
export default function Page() {
return (
Static-Site Generation
서버사이드 렌더링을 수행하면, HTML 페이지를 매 요청이 있을 때 마다
새로 만들게 된다. 서버에서는 HTML과 요청의 결과에 따른 JSON 데이터와
함께 클라이언트에 필요한 자바스크립트 파일이 전송된다.
`./next/server/app/ssr`를 확인해보면, `/ssg` 페이지와는 다르게 미리
빌드된 결과물 없이 항상 데이터를 `fetch`할 준비만 되어 있는 것을 볼
수 있다.
클라이언트에서는 이벤트 핸들러 등이 추가되지 않은 정적인 HTML을
받아서 페이지를 미리 보여주고, 리액트는 이 정적인 페이지에 JSON
데이터와 자바스크립트를 받아 컴포넌트를 상호작용 가능한 페이지로
만들어 준다. 이러한 일련의 과정을 hydration 이라고 한다.
)
}
================================================
FILE: chapter11/next13/app/streaming/[id]/components.tsx
================================================
import { sleep } from '#lib/utils'
import { fetchPosts, fetchUsers } from '#services/server'
export async function Users() {
// Suspense를 보기 위해 강제로 지연시킵니다.
await sleep(3 * 1000)
const users = await fetchUsers()
return (
{users.map((user) => (
{user.name}
))}
)
}
export async function PostByUserId({ userId }: { userId: string }) {
await sleep(5 * 1000)
const allPosts = await fetchPosts()
const posts = allPosts.filter((post) => post.userId === parseInt(userId, 10))
return (
{posts.map((post) => (
{post.title}
))}
)
}
================================================
FILE: chapter11/next13/app/streaming/[id]/page.tsx
================================================
import { Suspense } from 'react'
import { PostByUserId, Users } from './components'
export default async function Page({ params }: { params: { id: string } }) {
return (
유저 목록을 로딩중입니다.
}>
{/* 타입스크립트에서 Promise 컴포넌트에 대해 에러를 내기 때문에 임시 처리 */}
{/* @ts-expect-error Async Server Component */}
유저 {params.id}의 작성 글을 로딩중입니다.}
>
{/* @ts-expect-error Async Server Component */}
)
}
================================================
FILE: chapter11/next13/app/streaming/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
import { fetchUsers } from '#services/server'
export default async function Layout({ children }: { children: ReactNode }) {
const users = await fetchUsers()
const items = [
{
text: 'Home',
},
...users.map((user) => ({
text: user.name,
slug: user.id.toString(),
})),
]
return (
)
}
================================================
FILE: chapter11/next13/app/streaming/page.tsx
================================================
export default async function Page() {
return (
Streaming with Suspense
스트리밍을 활용하면 서버에서 클라이언트로 UI 컴포넌트를 점진적으로
조금씩 보내는 것(스트리밍)이 가능해진다.
스트리밍을 활용하면 서버사이드렌더링과 다르게, 전체 페이지를 모두
보여줄 때 까지 기다리게 하는 것이 아니라 필요한 부분 부터 먼저
렌더링을 마치고 인터랙션할 수 있는 상태로 제공하는 것이
가능해진다.
위 유저 목록 중 하나를 누르면 유저 컴포넌트로 가는데, 이
컴포넌트는 각각 유저목록과 유저의 작성 글을 서로다른 Suspense
내부에서 불러온다. 이를 활용하면 `loading` 컴포넌트를 사용했을 때
보다 더 세밀하게 로딩을 보여줄 수 있다.
{[1, 2, 3, 4, 5].map((id) => (
))}
)
}
================================================
FILE: chapter11/next13/app/styles/css-modules/page.tsx
================================================
import styles from './styles.module.css'
const SkeletonCard = () => (
)
export default function Page() {
return (
)
}
================================================
FILE: chapter11/next13/app/styles/css-modules/styles.module.css
================================================
.container {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1.5rem /* 24px */;
}
@media (min-width: 1024px) {
.container {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.skeleton {
padding: 1rem /* 16px */;
border-radius: 1rem /* 16px */;
background-color: rgb(24 24 27 / 0.8);
}
.skeleton-img,
.skeleton-btn,
.skeleton-line-one,
.skeleton-line-two {
border-radius: 0.5rem /* 8px */;
}
.skeleton-img {
height: 3.5rem /* 56px */;
background-color: rgb(63 63 70 / 1);
}
.skeleton-btn,
.skeleton-line-one,
.skeleton-line-two {
margin-top: 0.75rem /* 12px */;
height: 0.75rem /* 12px */;
}
.skeleton-btn {
background-color: rgb(121 40 202 / 1);
width: 25%;
}
.skeleton-line-one,
.skeleton-line-two {
background-color: rgb(63 63 70 / 1);
}
.skeleton-line-one {
width: 91.666667%;
}
.skeleton-line-two {
width: 66.666667%;
}
================================================
FILE: chapter11/next13/app/styles/global-css/page.tsx
================================================
import './style.css'
const SkeletonCard = () => (
)
export default function Page() {
return (
Styled with a Global CSS Stylesheet
)
}
================================================
FILE: chapter11/next13/app/styles/global-css/style.css
================================================
.container {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 1.5rem /* 24px */;
}
@media (min-width: 1024px) {
.container {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.skeleton {
padding: 1rem /* 16px */;
border-radius: 1rem /* 16px */;
background-color: rgb(24 24 27 / 0.8);
}
.skeleton-img,
.skeleton-btn,
.skeleton-line-one,
.skeleton-line-two {
border-radius: 0.5rem /* 8px */;
}
.skeleton-img {
height: 3.5rem /* 56px */;
background-color: rgb(63 63 70 / 1);
}
.skeleton-btn,
.skeleton-line-one,
.skeleton-line-two {
margin-top: 0.75rem /* 12px */;
height: 0.75rem /* 12px */;
}
.skeleton-btn {
background-color: rgb(245 166 35 / 1);
width: 25%;
}
.skeleton-line-one,
.skeleton-line-two {
background-color: rgb(63 63 70 / 1);
}
.skeleton-line-one {
width: 91.666667%;
}
.skeleton-line-two {
width: 66.666667%;
}
================================================
FILE: chapter11/next13/app/styles/layout.tsx
================================================
import { ReactNode } from 'react'
import { TabGroup } from '#components/TabGroup'
const items = [
{
text: 'Global CSS',
slug: 'global-css',
},
{
text: 'CSS Modules',
slug: 'css-modules',
},
{
text: 'Styled Components',
slug: 'styled-components',
},
{
text: 'Styled JSX',
slug: 'styled-jsx',
},
]
export default function Layout({ children }: { children: ReactNode }) {
return (
)
}
================================================
FILE: chapter11/next13/app/styles/page.tsx
================================================
export default function Page() {
return (
)
}
================================================
FILE: chapter11/next13/app/styles/styled-components/layout.tsx
================================================
import { ReactNode } from 'react'
import StyledComponentsRegistry from '#components/StyledComponentsRegistry'
export default function Layout({ children }: { children: ReactNode }) {
return {children}
}
================================================
FILE: chapter11/next13/app/styles/styled-components/page.tsx
================================================
import {
SkeletonInner,
SkeletonImg,
SkeletonBtn,
SkeletonLineOne,
SkeletonLineTwo,
Container,
} from '#components/components'
const Skeleton = () => (
)
export default function Page() {
return (
Styled Components (styled로 만들어진 컴포넌트는 반드시 클라이언트
컴포넌트 여야 합니다.)
)
}
================================================
FILE: chapter11/next13/app/styles/styled-jsx/StyledRegistry.tsx
================================================
'use client'
import { ReactNode, useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
export default function StyledJsxRegistry({
children,
}: {
children: ReactNode
}) {
// Only create stylesheet once with lazy initial state
// x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
const [jsxStyleRegistry] = useState(() => createStyleRegistry())
useServerInsertedHTML(() => {
const styles = jsxStyleRegistry.styles()
jsxStyleRegistry.flush()
return <>{styles}>
})
return {children}
}
================================================
FILE: chapter11/next13/app/styles/styled-jsx/components.tsx
================================================
'use client'
export const SkeletonCard = () => (
<>
{/* eslint-disable-next-line react/no-unknown-property */}
>
)
================================================
FILE: chapter11/next13/app/styles/styled-jsx/layout.tsx
================================================
import { ReactNode } from 'react'
import StyledJsxRegistry from './StyledRegistry'
export default function Layout({ children }: { children: ReactNode }) {
return {children}
}
================================================
FILE: chapter11/next13/app/styles/styled-jsx/page.tsx
================================================
'use client'
import { SkeletonCard } from './components'
export default function Page() {
return (
Styled JSX ({'
)
}
================================================
FILE: chapter11/next13/middleware.ts
================================================
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-middleware-request', 'request')
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set('x-middleware-response', 'response')
return response
}
================================================
FILE: chapter11/next13/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
================================================
FILE: chapter11/next13/package.json
================================================
{
"name": "next13",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev:turbo": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"prettier": "prettier . --check",
"prettier:fix": "prettier . --write"
},
"dependencies": {
"@types/node": "18.11.18",
"@vercel/kv": "^0.1.2",
"clsx": "^1.2.1",
"next": "^13.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "6.0.0-rc.1",
"typescript": "5.1.0-beta"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"autoprefixer": "^10.4.13",
"eslint": "^8.38.0",
"eslint-config-next": "^13.4.0",
"postcss": "^8.4.21",
"prettier": "^2.8.7",
"tailwindcss": "^3.2.4"
}
}
================================================
FILE: chapter11/next13/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: chapter11/next13/src/components/Counter.tsx
================================================
'use client'
import { useCallback } from 'react'
import { useCounter } from '../context/counter'
const Counter = () => {
const [count, setCount] = useCounter()
const handleClick = useCallback(
() => setCount((prev) => prev + 1),
[setCount],
)
return (
{count} Clicks
)
}
export default Counter
================================================
FILE: chapter11/next13/src/components/DefaultHeader.tsx
================================================
import { memo } from 'react'
function DefaultHeader() {
return (
<>
>
)
}
export default memo(DefaultHeader)
================================================
FILE: chapter11/next13/src/components/ErrorButton.tsx
================================================
'use client'
import { useCallback, useState } from 'react'
export default function ErrorButton() {
const [clicked, setClicked] = useState(false)
const handleButtonClick = useCallback(() => {
setClicked(true)
}, [])
if (clicked) {
// 임의로 발생시킨 에러
throw new Error('clicked 로 인해 발생한 에러')
}
return (
에러 던지기!
)
}
================================================
FILE: chapter11/next13/src/components/Sidebar.tsx
================================================
'use client'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
import { clsx } from 'clsx'
import { useCallback, useState } from 'react'
import { demos, type Item } from '#constant/menu'
export default function SideBar() {
const [open, setOpen] = useState(false)
const handleClose = useCallback(() => setOpen(false), [])
const handleButtonClick = useCallback(() => setOpen((prev) => !prev), [])
return (
Next@13 App Directory 예제
Menu
{demos.map((section) => {
return (
{section.items.map((item) => (
))}
)
})}
)
}
function GlobalNavItem({
item,
close,
}: {
item: Item
close: () => false | void
}) {
const segment = useSelectedLayoutSegment()
const isActive = item.slug === segment
return (
{item.name}
)
}
================================================
FILE: chapter11/next13/src/components/StyledComponentsRegistry.tsx
================================================
'use client'
import { ReactNode, useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: ReactNode
}) {
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}>
})
if (typeof window !== 'undefined') {
return <>{children}>
}
return (
<>{children}>
)
}
================================================
FILE: chapter11/next13/src/components/Tab.tsx
================================================
'use client'
import { clsx } from 'clsx'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
import { type Item } from './TabGroup'
export const Tab = ({
path,
item: { slug, text },
}: {
path: string
item: Item
}) => {
const segment = useSelectedLayoutSegment()
const href = slug ? path + '/' + slug : path
const isActive =
// Example home pages e.g. `/layouts`
(!slug && segment === null) ||
// Nested pages e.g. `/layouts/electronics`
segment === slug
return (
{text}
)
}
================================================
FILE: chapter11/next13/src/components/TabGroup.tsx
================================================
import { Tab } from './Tab'
export interface Item {
text: string
slug?: string
}
export const TabGroup = ({ path, items }: { path: string; items: Item[] }) => {
return (
{items.map((item) => (
))}
)
}
================================================
FILE: chapter11/next13/src/components/components.ts
================================================
'use client'
import styled from 'styled-components'
export const Container = styled.div`
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.5rem /* 24px */;
`
export const SkeletonInner = styled.div`
padding: 1rem /* 16px */;
background-color: rgb(24 24 27 / 0.8);
border-radius: 1rem /* 16px */;
`
export const SkeletonImg = styled.div`
height: 3.5rem /* 56px */;
border-radius: 0.5rem /* 8px */;
background-color: rgb(63 63 70 / 1);
`
export const SkeletonBtn = styled.div`
margin-top: 0.75rem /* 12px */;
width: 25%;
height: 0.75rem /* 12px */;
border-radius: 0.5rem /* 8px */;
background-color: rgb(255 0 128 / 1);
`
export const SkeletonLineOne = styled.div`
margin-top: 0.75rem /* 12px */;
height: 0.75rem /* 12px */;
width: 91.666667%;
border-radius: 0.5rem /* 8px */;
background-color: rgb(63 63 70 / 1);
`
export const SkeletonLineTwo = styled.div`
margin-top: 0.75rem /* 12px */;
height: 0.75rem /* 12px */;
width: 66.666667%;
border-radius: 0.5rem /* 8px */;
background-color: rgb(63 63 70 / 1);
`
================================================
FILE: chapter11/next13/src/components/server-action/client-component.tsx
================================================
'use client'
import { useCallback, useTransition } from 'react'
import { updateData } from '#server-action'
import { SkeletonBtn } from '#components/components'
export function ClientButtonComponent({ id }: { id: string }) {
const [isPending, startTransition] = useTransition()
const handleClick = useCallback(() => {
startTransition(() => updateData(id, { name: '기본값', age: 0 }))
}, [])
return isPending ? (
) : (
기본값으로 돌리기
)
}
================================================
FILE: chapter11/next13/src/constant/menu.ts
================================================
export interface Item {
name: string
slug: string
description: string
}
export const demos: Array<{ name: string; items: Item[] }> = [
{
name: 'Layouts',
items: [
{
name: 'Nested Layouts',
slug: 'layouts',
description: '중첩 레이아웃 - 주소에 따라 적용할 수 있는 레이아웃',
},
{
name: 'Grouped Layouts',
slug: 'grouped-layouts',
description:
'그룹 레이아웃 - 주소에 영향을 미치지 않고 특정 주소에 따라 그룹화',
},
],
},
{
name: 'File Conventions',
items: [
{
name: 'loading.js',
slug: 'loading',
description:
'데이터를 불러오거나 렌더링하는 동안 표시할 수 있는 로딩 컴포넌트',
},
{
name: 'error.js',
slug: 'error',
description: '에러 발생시 렌더링할 수 있는 에러 컴포넌트',
},
{
name: 'head.js',
slug: 'head',
description: 'URL에 따라 보여줄 수 있는 head',
},
],
},
{
name: 'Data Fetching',
items: [
{
name: 'Static-Site Generation',
slug: 'ssg',
description: '기존 getStaticProps을 nextjs@13에서 구현하는 방법',
},
{
name: 'Server-Side Rendering',
slug: 'ssr',
description: '기존 `getServerSideProps`를 nextjs@13에서 구현하는 방법',
},
{
name: 'Incremental Static Regeneration',
slug: 'isr',
description: '기존 `getStaticProps`와 revalidate 옵션을 구현하는 방법',
},
{
name: 'Streaming with Suspense',
slug: 'streaming',
description: 'React Suspense를 활용한 서버 스트리밍 데이터 불러오기',
},
],
},
{
name: 'Components',
items: [
{
name: 'Client context',
slug: 'context',
description:
'`Context`는 상태를 가지고 있으므로 반드시 클라이언트 컴포넌트여야 한다.',
},
],
},
{
name: 'Styles',
items: [
{
name: 'CSS and CSS-in-JS',
slug: 'styles',
description: '스타일을 적용하는 다양한 방법',
},
],
},
{
name: 'Server Action',
items: [
{
name: 'form action',
slug: 'server-action/form',
description: '서버액션을 form과 함께 사용해보기',
},
{
name: 'form action with data',
slug: 'server-action/form/1',
description: '서버액션을 form과 데이터를 기반으로 사용해보기',
},
{
name: 'form action with useTransition',
slug: 'server-action/start-transition/1',
description: '서버액션을 useTransition과 함께 사용해보기',
},
],
},
]
================================================
FILE: chapter11/next13/src/context/counter.tsx
================================================
'use client'
import {
createContext,
Dispatch,
SetStateAction,
useState,
useContext,
ReactNode,
} from 'react'
const CounterContext = createContext<
[number, Dispatch>] | undefined
>(undefined)
export function CounterProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
return (
{children}
)
}
export function useCounter() {
const context = useContext(CounterContext)
if (context === undefined) {
throw new Error('useCounter must be used within a CounterProvider')
}
return context
}
================================================
FILE: chapter11/next13/src/lib/utils.ts
================================================
export async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
================================================
FILE: chapter11/next13/src/server-action/index.ts
================================================
'use server'
import kv from '@vercel/kv'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
export async function updateData(
id: string,
data: { name: string; age: number },
) {
const key = `test:${id}`
await kv.set(key, {
name: data.name,
age: data.age,
})
revalidatePath(`/server-action/form/${id}`)
}
================================================
FILE: chapter11/next13/src/services/constant.ts
================================================
export const API_URL_BASE = process.env.VERCEL_URL
? 'https://' + process.env.VERCEL_URL
: 'http://localhost:3000'
================================================
FILE: chapter11/next13/src/services/server.ts
================================================
interface User {
id: number
name: string
email: string
address: {
street: string
suite: string
city: string
zipcode: string
geo: {
lat: string
lng: string
}
}
phone: string
website: string
company: {
name: string
catchPhrase: string
bs: string
}
}
export async function fetchUsers(): Promise> {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const result: Array = await response.json()
return result
}
export async function fetchUserById(id: string | number): Promise {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`,
)
const result: User = await response.json()
return result
}
interface Todo {
userId: number
id: number
title: string
completed: boolean
}
export async function fetchTodos(): Promise> {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const result: Array = await response.json()
return result
}
interface Post {
userId: number
id: number
title: string
body: string
}
export async function fetchPosts(): Promise> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const result: Array = await response.json()
return result
}
export async function fetchPostById(
id: number | string,
options?: RequestInit,
): Promise {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
options,
)
const result: Post = await response.json()
return result
}
================================================
FILE: chapter11/next13/tailwind.config.js
================================================
const colors = require('tailwindcss/colors')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./src/components/**/*.{js,ts,jsx,tsx}',
],
future: {
hoverOnlyWhenSupported: true,
},
theme: {
extend: {
colors: {
gray: colors.zinc,
'gray-1000': 'rgb(17,17,19)',
'gray-1100': 'rgb(10,10,11)',
vercel: {
pink: '#FF0080',
blue: '#0070F3',
cyan: '#50E3C2',
orange: '#F5A623',
violet: '#7928CA',
},
},
backgroundImage: ({ theme }) => ({
'vc-border-gradient': `radial-gradient(at left top, ${theme(
'colors.gray.500',
)}, 50px, ${theme('colors.gray.800')} 50%)`,
}),
keyframes: ({ theme }) => ({
rerender: {
'0%': {
'border-color': theme('colors.vercel.pink'),
},
'40%': {
'border-color': theme('colors.vercel.pink'),
},
},
highlight: {
'0%': {
background: theme('colors.vercel.pink'),
color: theme('colors.white'),
},
'40%': {
background: theme('colors.vercel.pink'),
color: theme('colors.white'),
},
},
shimmer: {
'100%': {
transform: 'translateX(100%)',
},
},
translateXReset: {
'100%': {
transform: 'translateX(0)',
},
},
fadeToTransparent: {
'0%': {
opacity: 1,
},
'40%': {
opacity: 1,
},
'100%': {
opacity: 0,
},
},
}),
},
},
}
================================================
FILE: chapter11/next13/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"#app/*": ["app/*"],
"#components/*": ["src/components/*"],
"#constant/*": ["src/constant/*"],
"#context/*": ["src/context/*"],
"#lib/*": ["src/lib/*"],
"#server-action/*": ["src/server-action/*"],
"#server-action": ["src/server-action/index.ts"],
"#services/*": ["src/services/*"]
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter11/server-components-demo/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
# notes
notes/*.md
# misc
.DS_Store
.eslintcache
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# vscode
.vscode
================================================
FILE: chapter11/server-components-demo/.nvmrc
================================================
lts/hydrogen
================================================
FILE: chapter11/server-components-demo/.prettierignore
================================================
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
# misc
.DS_Store
.eslintcache
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.html
*.json
*.md
================================================
FILE: chapter11/server-components-demo/.prettierrc.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
module.exports = {
arrowParens: 'always',
bracketSpacing: false,
singleQuote: true,
jsxBracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
};
================================================
FILE: chapter11/server-components-demo/CODE_OF_CONDUCT.md
================================================
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: chapter11/server-components-demo/Dockerfile
================================================
FROM node:lts-hydrogen
WORKDIR /opt/notes-app
COPY package.json package-lock.json ./
RUN npm install --legacy-peer-deps
COPY . .
ENTRYPOINT [ "npm", "run" ]
CMD [ "start" ]
================================================
FILE: chapter11/server-components-demo/LICENSE
================================================
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: chapter11/server-components-demo/README.md
================================================
# Demo of sever components
https://github.com/reactjs/server-components-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 프로젝트입니다.
## 프로젝트 구조
TBD
## 실행하기
### 빠르게 실행하기
- 데이터 생성하기
- `docker-compose up -d` 로 detach 모드로 실행
- `docker-compose exec notes-app npm run seed`로 데이터 생성
- 애플리케이션 실행하기
- `docker-compose up`
================================================
FILE: chapter11/server-components-demo/credentials.js
================================================
module.exports = {
host: process.env.DB_HOST || 'localhost',
database: 'notesapi',
user: 'notesadmin',
password: 'password',
port: '5432',
};
================================================
FILE: chapter11/server-components-demo/docker-compose.yml
================================================
version: "3.8"
services:
postgres:
image: postgres:13
environment:
POSTGRES_USER: notesadmin
POSTGRES_PASSWORD: password
POSTGRES_DB: notesapi
ports:
- '5432:5432'
volumes:
- ./scripts/init_db.sh:/docker-entrypoint-initdb.d/init_db.sh
- db:/var/lib/postgresql/data
notes-app:
build:
context: .
depends_on:
- postgres
ports:
- '4000:4000'
environment:
DB_HOST: postgres
PORT: 4000
volumes:
- ./notes:/opt/notes-app/notes
- ./public:/opt/notes-app/public
- ./scripts:/opt/notes-app/scripts
- ./server:/opt/notes-app/server
- ./src:/opt/notes-app/src
- ./credentials.js:/opt/notes-app/credentials.js
volumes:
db:
================================================
FILE: chapter11/server-components-demo/notes/.gitkeep
================================================
================================================
FILE: chapter11/server-components-demo/package.json
================================================
{
"name": "react-notes",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=14.9.0"
},
"license": "MIT",
"dependencies": {
"@babel/core": "7.21.3",
"@babel/plugin-transform-modules-commonjs": "^7.21.2",
"@babel/preset-react": "^7.18.6",
"@babel/register": "^7.21.0",
"acorn-jsx": "^5.3.2",
"acorn-loose": "^8.3.0",
"babel-loader": "8.3.0",
"compression": "^1.7.4",
"concurrently": "^7.6.0",
"date-fns": "^2.29.3",
"excerpts": "^0.0.3",
"express": "^4.18.2",
"html-webpack-plugin": "5.5.0",
"marked": "^4.2.12",
"nodemon": "^2.0.21",
"pg": "^8.10.0",
"react": "18.3.0-next-1308e49a6-20230330",
"react-dom": "18.3.0-next-1308e49a6-20230330",
"react-error-boundary": "^4.0.9",
"react-server-dom-webpack": "18.3.0-next-1308e49a6-20230330",
"resolve": "1.22.1",
"rimraf": "^4.4.0",
"sanitize-html": "^2.10.0",
"server-only": "^0.0.1",
"webpack": "5.76.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"prettier": "1.19.1"
},
"scripts": {
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
"server:dev": "cross-env NODE_ENV=development nodemon -- --conditions=react-server server",
"server:prod": "cross-env NODE_ENV=production nodemon -- --conditions=react-server server",
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js",
"prettier": "prettier --write **/*.js",
"seed": "node ./scripts/seed.js"
},
"babel": {
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
},
"nodemonConfig": {
"ignore": [
"build/*"
]
},
"overrides": {
"react": "18.3.0-next-1308e49a6-20230330",
"react-dom": "18.3.0-next-1308e49a6-20230330"
}
}
================================================
FILE: chapter11/server-components-demo/public/index.html
================================================
React Notes
================================================
FILE: chapter11/server-components-demo/public/style.css
================================================
/* -------------------------------- CSSRESET --------------------------------*/
/* CSS Reset adapted from https://dev.to/hankchizljaw/a-modern-css-reset-6p3 */
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default padding */
ul[class],
ol[class] {
padding: 0;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul[class],
ol[class],
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}
/* Set core body defaults */
body {
min-height: 100vh;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
/* Remove list styles on ul, ol elements with a class attribute */
ul[class],
ol[class] {
list-style: none;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
img {
max-width: 100%;
display: block;
}
/* Natural flow and rhythm in articles by default */
article > * + * {
margin-block-start: 1em;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* -------------------------------- /CSSRESET --------------------------------*/
:root {
/* Colors */
--main-border-color: #ddd;
--primary-border: #037dba;
--gray-20: #404346;
--gray-60: #8a8d91;
--gray-70: #bcc0c4;
--gray-80: #c9ccd1;
--gray-90: #e4e6eb;
--gray-95: #f0f2f5;
--gray-100: #f5f7fa;
--primary-blue: #037dba;
--secondary-blue: #0396df;
--tertiary-blue: #c6efff;
--flash-blue: #4cf7ff;
--outline-blue: rgba(4, 164, 244, 0.6);
--navy-blue: #035e8c;
--red-25: #bd0d2a;
--secondary-text: #65676b;
--white: #fff;
--yellow: #fffae1;
--outline-box-shadow: 0 0 0 2px var(--outline-blue);
--outline-box-shadow-contrast: 0 0 0 2px var(--navy-blue);
/* Fonts */
--sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto,
Ubuntu, Helvetica, sans-serif;
--monospace: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console,
monospace;
}
html {
font-size: 100%;
}
body {
font-family: var(--sans-serif);
background: var(--gray-100);
font-weight: 400;
line-height: 1.75;
}
h1,
h2,
h3,
h4,
h5 {
margin: 0;
font-weight: 700;
line-height: 1.3;
}
h1 {
font-size: 3.052rem;
}
h2 {
font-size: 2.441rem;
}
h3 {
font-size: 1.953rem;
}
h4 {
font-size: 1.563rem;
}
h5 {
font-size: 1.25rem;
}
small,
.text_small {
font-size: 0.8rem;
}
pre,
code {
font-family: var(--monospace);
border-radius: 6px;
}
pre {
background: var(--gray-95);
padding: 12px;
line-height: 1.5;
}
code {
background: var(--yellow);
padding: 0 3px;
font-size: 0.94rem;
word-break: break-word;
}
pre code {
background: none;
}
a {
color: var(--primary-blue);
}
.text-with-markdown h1,
.text-with-markdown h2,
.text-with-markdown h3,
.text-with-markdown h4,
.text-with-markdown h5 {
margin-block: 2rem 0.7rem;
margin-inline: 0;
}
.text-with-markdown blockquote {
font-style: italic;
color: var(--gray-20);
border-left: 3px solid var(--gray-80);
padding-left: 10px;
}
hr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
/* ---------------------------------------------------------------------------*/
.main {
display: flex;
height: 100vh;
width: 100%;
overflow: hidden;
}
.col {
height: 100%;
}
.col:last-child {
flex-grow: 1;
}
.logo {
height: 20px;
width: 22px;
margin-inline-end: 10px;
}
.edit-button {
border-radius: 100px;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 6px 20px 8px;
cursor: pointer;
font-weight: 700;
outline-style: none;
}
.edit-button--solid {
background: var(--primary-blue);
color: var(--white);
border: none;
margin-inline-start: 6px;
transition: all 0.2s ease-in-out;
}
.edit-button--solid:hover {
background: var(--secondary-blue);
}
.edit-button--solid:focus {
box-shadow: var(--outline-box-shadow-contrast);
}
.edit-button--outline {
background: var(--white);
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
margin-inline-start: 12px;
transition: all 0.1s ease-in-out;
}
.edit-button--outline:disabled {
opacity: 0.5;
}
.edit-button--outline:hover:not([disabled]) {
background: var(--primary-blue);
color: var(--white);
}
.edit-button--outline:focus {
box-shadow: var(--outline-box-shadow);
}
ul.notes-list {
padding: 16px 0;
}
.notes-list > li {
padding: 0 16px;
}
.notes-empty {
padding: 16px;
}
.sidebar {
background: var(--white);
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.1), 0px 2px 2px rgba(0, 0, 0, 0.1);
overflow-y: scroll;
z-index: 1000;
flex-shrink: 0;
max-width: 350px;
min-width: 250px;
width: 30%;
}
.sidebar-header {
letter-spacing: 0.15em;
text-transform: uppercase;
padding: 36px 16px 16px;
display: flex;
align-items: center;
}
.sidebar-menu {
padding: 0 16px 16px;
display: flex;
justify-content: space-between;
}
.sidebar-menu > .search {
position: relative;
flex-grow: 1;
}
.sidebar-note-list-item {
position: relative;
margin-bottom: 12px;
padding: 16px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
max-height: 100px;
transition: max-height 250ms ease-out;
transform: scale(1);
}
.sidebar-note-list-item.note-expanded {
max-height: 300px;
transition: max-height 0.5s ease;
}
.sidebar-note-list-item.flash {
animation-name: flash;
animation-duration: 0.6s;
}
.sidebar-note-open {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
z-index: 0;
border: none;
border-radius: 6px;
text-align: start;
background: var(--gray-95);
cursor: pointer;
outline-style: none;
color: transparent;
font-size: 0px;
}
.sidebar-note-open:focus {
box-shadow: var(--outline-box-shadow);
}
.sidebar-note-open:hover {
background: var(--gray-90);
}
.sidebar-note-header {
z-index: 1;
max-width: 85%;
pointer-events: none;
}
.sidebar-note-header > strong {
display: block;
font-size: 1.25rem;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-note-toggle-expand {
z-index: 2;
border-radius: 50%;
height: 24px;
border: 1px solid var(--gray-60);
cursor: pointer;
flex-shrink: 0;
visibility: hidden;
opacity: 0;
cursor: default;
transition: visibility 0s linear 20ms, opacity 300ms;
outline-style: none;
}
.sidebar-note-toggle-expand:focus {
box-shadow: var(--outline-box-shadow);
}
.sidebar-note-open:hover + .sidebar-note-toggle-expand,
.sidebar-note-open:focus + .sidebar-note-toggle-expand,
.sidebar-note-toggle-expand:hover,
.sidebar-note-toggle-expand:focus {
visibility: visible;
opacity: 1;
transition: visibility 0s linear 0s, opacity 300ms;
}
.sidebar-note-toggle-expand img {
width: 10px;
height: 10px;
}
.sidebar-note-excerpt {
pointer-events: none;
z-index: 2;
flex: 1 1 250px;
color: var(--secondary-text);
position: relative;
animation: slideIn 100ms;
}
.search input {
padding: 0 16px;
border-radius: 100px;
border: 1px solid var(--gray-90);
width: 100%;
height: 100%;
outline-style: none;
}
.search input:focus {
box-shadow: var(--outline-box-shadow);
}
.search .spinner {
position: absolute;
right: 10px;
top: 10px;
}
.note-viewer {
display: flex;
align-items: center;
justify-content: center;
}
.note {
background: var(--white);
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1), 0px 0px 1px rgba(0, 0, 0, 0.1);
border-radius: 8px;
height: 95%;
width: 95%;
min-width: 400px;
padding: 8%;
overflow-y: auto;
}
.note--empty-state {
margin-inline: 20px 20px;
}
.note-text--empty-state {
font-size: 1.5rem;
}
.note-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap-reverse;
margin-inline-start: -12px;
}
.note-menu {
display: flex;
justify-content: space-between;
align-items: center;
flex-grow: 1;
}
.note-title {
line-height: 1.3;
flex-grow: 1;
overflow-wrap: break-word;
margin-inline-start: 12px;
}
.note-updated-at {
color: var(--secondary-text);
white-space: nowrap;
margin-inline-start: 12px;
}
.note-preview {
margin-block-start: 50px;
}
.note-editor {
background: var(--white);
display: flex;
height: 100%;
width: 100%;
padding: 58px;
overflow-y: auto;
}
.note-editor .label {
margin-bottom: 20px;
}
.note-editor-form {
display: flex;
flex-direction: column;
width: 400px;
flex-shrink: 0;
position: sticky;
top: 0;
}
.note-editor-form input,
.note-editor-form textarea {
background: none;
border: 1px solid var(--gray-70);
border-radius: 2px;
font-family: var(--monospace);
font-size: 0.8rem;
padding: 12px;
outline-style: none;
}
.note-editor-form input:focus,
.note-editor-form textarea:focus {
box-shadow: var(--outline-box-shadow);
}
.note-editor-form input {
height: 44px;
margin-bottom: 16px;
}
.note-editor-form textarea {
height: 100%;
max-width: 400px;
}
.note-editor-menu {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 12px;
}
.note-editor-preview {
margin-inline-start: 40px;
width: 100%;
}
.note-editor-done,
.note-editor-delete {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 100px;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 6px 20px 8px;
cursor: pointer;
font-weight: 700;
margin-inline-start: 12px;
outline-style: none;
transition: all 0.2s ease-in-out;
}
.note-editor-done:disabled,
.note-editor-delete:disabled {
opacity: 0.5;
}
.note-editor-done {
border: none;
background: var(--primary-blue);
color: var(--white);
}
.note-editor-done:focus {
box-shadow: var(--outline-box-shadow-contrast);
}
.note-editor-done:hover:not([disabled]) {
background: var(--secondary-blue);
}
.note-editor-delete {
border: 1px solid var(--red-25);
background: var(--white);
color: var(--red-25);
}
.note-editor-delete:focus {
box-shadow: var(--outline-box-shadow);
}
.note-editor-delete:hover:not([disabled]) {
background: var(--red-25);
color: var(--white);
}
/* Hack to color our svg */
.note-editor-delete:hover:not([disabled]) img {
filter: grayscale(1) invert(1) brightness(2);
}
.note-editor-done > img {
width: 14px;
}
.note-editor-delete > img {
width: 10px;
}
.note-editor-done > img,
.note-editor-delete > img {
margin-inline-end: 12px;
}
.note-editor-done[disabled],
.note-editor-delete[disabled] {
opacity: 0.5;
}
.label {
display: inline-block;
border-radius: 100px;
letter-spacing: 0.05em;
text-transform: uppercase;
font-weight: 700;
padding: 4px 14px;
}
.label--preview {
background: rgba(38, 183, 255, 0.15);
color: var(--primary-blue);
}
.text-with-markdown p {
margin-bottom: 16px;
}
.text-with-markdown img {
width: 100%;
}
/* https://codepen.io/mandelid/pen/vwKoe */
.spinner {
display: inline-block;
transition: opacity linear 0.1s 0.2s;
width: 20px;
height: 20px;
border: 3px solid rgba(80, 80, 80, 0.5);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
opacity: 0;
}
.spinner--active {
opacity: 1;
}
.skeleton::after {
content: 'Loading...';
}
.skeleton {
height: 100%;
background-color: #eee;
background-image: linear-gradient(90deg, #eee, #f5f5f5, #eee);
background-size: 200px 100%;
background-repeat: no-repeat;
border-radius: 4px;
display: block;
line-height: 1;
width: 100%;
animation: shimmer 1.2s ease-in-out infinite;
color: transparent;
}
.skeleton:first-of-type {
margin: 0;
}
.skeleton--button {
border-radius: 100px;
padding: 6px 20px 8px;
width: auto;
}
.v-stack + .v-stack {
margin-block-start: 0.8em;
}
.offscreen {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
position: absolute;
}
/* ---------------------------------------------------------------------------*/
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes shimmer {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
@keyframes slideIn {
0% {
top: -10px;
opacity: 0;
}
100% {
top: 0;
opacity: 1;
}
}
@keyframes flash {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.9;
}
100% {
transform: scale(1);
opacity: 1;
}
}
================================================
FILE: chapter11/server-components-demo/scripts/build.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
const path = require('path');
const rimraf = require('rimraf');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactServerWebpackPlugin = require('react-server-dom-webpack/plugin');
const isProduction = process.env.NODE_ENV === 'production';
rimraf.sync(path.resolve(__dirname, '../build'));
webpack(
{
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
entry: [path.resolve(__dirname, '../src/framework/bootstrap.js')],
output: {
path: path.resolve(__dirname, '../build'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, '../public/index.html'),
}),
new ReactServerWebpackPlugin({isServer: false}),
],
},
(err, stats) => {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
process.exit(1);
return;
}
const info = stats.toJson();
if (stats.hasErrors()) {
console.log('Finished running webpack with errors.');
info.errors.forEach((e) => console.error(e));
process.exit(1);
} else {
console.log('Finished running webpack.');
}
}
);
================================================
FILE: chapter11/server-components-demo/scripts/init_db.sh
================================================
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
DROP TABLE IF EXISTS notes;
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
title TEXT,
body TEXT
);
EOSQL
================================================
FILE: chapter11/server-components-demo/scripts/seed.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
const fs = require('fs');
const path = require('path');
const {Pool} = require('pg');
const {readdir, unlink, writeFile} = require('fs/promises');
const startOfYear = require('date-fns/startOfYear');
const credentials = require('../credentials');
const NOTES_PATH = './notes';
const pool = new Pool(credentials);
const now = new Date();
const startOfThisYear = startOfYear(now);
// Thanks, https://stackoverflow.com/a/9035732
function randomDateBetween(start, end) {
return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime())
);
}
const dropTableStatement = 'DROP TABLE IF EXISTS notes;';
const createTableStatement = `CREATE TABLE notes (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
title TEXT,
body TEXT
);`;
const insertNoteStatement = `INSERT INTO notes(title, body, created_at, updated_at)
VALUES ($1, $2, $3, $3)
RETURNING *`;
const seedData = [
[
'Meeting Notes',
'This is an example note. It contains **Markdown**!',
randomDateBetween(startOfThisYear, now),
],
[
'Make a thing',
`It's very easy to make some words **bold** and other words *italic* with
Markdown. You can even [link to React's website!](https://www.reactjs.org).`,
randomDateBetween(startOfThisYear, now),
],
[
'A note with a very long title because sometimes you need more words',
`You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing)
notes in this app! These note live on the server in the \`notes\` folder.
`,
randomDateBetween(startOfThisYear, now),
],
['I wrote this note today', 'It was an excellent note.', now],
];
async function seed() {
await pool.query(dropTableStatement);
await pool.query(createTableStatement);
const res = await Promise.all(
seedData.map((row) => pool.query(insertNoteStatement, row))
);
const oldNotes = await readdir(path.resolve(NOTES_PATH));
await Promise.all(
oldNotes
.filter((filename) => filename.endsWith('.md'))
.map((filename) => unlink(path.resolve(NOTES_PATH, filename)))
);
await Promise.all(
res.map(({rows}) => {
const id = rows[0].id;
const content = rows[0].body;
const data = new Uint8Array(Buffer.from(content));
return writeFile(path.resolve(NOTES_PATH, `${id}.md`), data, (err) => {
if (err) {
throw err;
}
});
})
);
}
seed();
================================================
FILE: chapter11/server-components-demo/server/api.server.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
const register = require('react-server-dom-webpack/node-register');
register();
const babelRegister = require('@babel/register');
babelRegister({
ignore: [/[\\\/](build|server|node_modules)[\\\/]/],
presets: [['@babel/preset-react', {runtime: 'automatic'}]],
plugins: ['@babel/transform-modules-commonjs'],
});
const express = require('express');
const compress = require('compression');
const {readFileSync} = require('fs');
const {unlink, writeFile} = require('fs').promises;
const {renderToPipeableStream} = require('react-server-dom-webpack/server');
const path = require('path');
const {Pool} = require('pg');
const React = require('react');
const ReactApp = require('../src/App').default;
// Don't keep credentials in the source tree in a real app!
const pool = new Pool(require('../credentials'));
const PORT = process.env.PORT || 4000;
const app = express();
app.use(compress());
app.use(express.json());
app
.listen(PORT, () => {
console.log(`React Notes listening at ${PORT}...`);
})
.on('error', function(error) {
if (error.syscall !== 'listen') {
throw error;
}
const isPipe = (portOrPipe) => Number.isNaN(portOrPipe);
const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT;
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
function handleErrors(fn) {
return async function(req, res, next) {
try {
return await fn(req, res);
} catch (x) {
next(x);
}
};
}
app.get(
'/',
handleErrors(async function(_req, res) {
await waitForWebpack();
const html = readFileSync(
path.resolve(__dirname, '../build/index.html'),
'utf8'
);
// Note: this is sending an empty HTML shell, like a client-side-only app.
// However, the intended solution (which isn't built out yet) is to read
// from the Server endpoint and turn its response into an HTML stream.
res.send(html);
})
);
async function renderReactTree(res, props) {
await waitForWebpack();
const manifest = readFileSync(
path.resolve(__dirname, '../build/react-client-manifest.json'),
'utf8'
);
const moduleMap = JSON.parse(manifest);
const {pipe} = renderToPipeableStream(
React.createElement(ReactApp, props),
moduleMap
);
pipe(res);
}
function sendResponse(req, res, redirectToId) {
const location = JSON.parse(req.query.location);
if (redirectToId) {
location.selectedId = redirectToId;
}
res.set('X-Location', JSON.stringify(location));
renderReactTree(res, {
selectedId: location.selectedId,
isEditing: location.isEditing,
searchText: location.searchText,
});
}
app.get('/react', function(req, res) {
sendResponse(req, res, null);
});
const NOTES_PATH = path.resolve(__dirname, '../notes');
app.post(
'/notes',
handleErrors(async function(req, res) {
const now = new Date();
const result = await pool.query(
'insert into notes (title, body, created_at, updated_at) values ($1, $2, $3, $3) returning id',
[req.body.title, req.body.body, now]
);
const insertedId = result.rows[0].id;
await writeFile(
path.resolve(NOTES_PATH, `${insertedId}.md`),
req.body.body,
'utf8'
);
sendResponse(req, res, insertedId);
})
);
app.put(
'/notes/:id',
handleErrors(async function(req, res) {
const now = new Date();
const updatedId = Number(req.params.id);
await pool.query(
'update notes set title = $1, body = $2, updated_at = $3 where id = $4',
[req.body.title, req.body.body, now, updatedId]
);
await writeFile(
path.resolve(NOTES_PATH, `${updatedId}.md`),
req.body.body,
'utf8'
);
sendResponse(req, res, null);
})
);
app.delete(
'/notes/:id',
handleErrors(async function(req, res) {
await pool.query('delete from notes where id = $1', [req.params.id]);
await unlink(path.resolve(NOTES_PATH, `${req.params.id}.md`));
sendResponse(req, res, null);
})
);
app.get(
'/notes',
handleErrors(async function(_req, res) {
const {rows} = await pool.query('select * from notes order by id desc');
res.json(rows);
})
);
app.get(
'/notes/:id',
handleErrors(async function(req, res) {
const {rows} = await pool.query('select * from notes where id = $1', [
req.params.id,
]);
res.json(rows[0]);
})
);
app.get('/sleep/:ms', function(req, res) {
setTimeout(() => {
res.json({ok: true});
}, req.params.ms);
});
app.use(express.static('build'));
app.use(express.static('public'));
async function waitForWebpack() {
while (true) {
try {
readFileSync(path.resolve(__dirname, '../build/index.html'));
return;
} catch (err) {
console.log(
'Could not find webpack build output. Will retry in a second...'
);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
================================================
FILE: chapter11/server-components-demo/server/package.json
================================================
{
"type": "commonjs",
"main": "./api.server.js"
}
================================================
FILE: chapter11/server-components-demo/src/App.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {Suspense} from 'react';
import Note from './Note';
import NoteList from './NoteList';
import EditButton from './EditButton';
import SearchField from './SearchField';
import NoteSkeleton from './NoteSkeleton';
import NoteListSkeleton from './NoteListSkeleton';
export default function App({selectedId, isEditing, searchText}) {
return (
React Notes
}>
);
}
================================================
FILE: chapter11/server-components-demo/src/EditButton.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';
import {useTransition} from 'react';
import {useRouter} from './framework/router';
export default function EditButton({noteId, children}) {
const [isPending, startTransition] = useTransition();
const {navigate} = useRouter();
const isDraft = noteId == null;
return (
{
startTransition(() => {
navigate({
selectedId: noteId,
isEditing: true,
});
});
}}
role="menuitem">
{children}
);
}
================================================
FILE: chapter11/server-components-demo/src/Note.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {format} from 'date-fns';
// Uncomment if you want to read from a file instead.
// import {readFile} from 'fs/promises';
// import {resolve} from 'path';
import NotePreview from './NotePreview';
import EditButton from './EditButton';
import NoteEditor from './NoteEditor';
export default async function Note({selectedId, isEditing}) {
if (selectedId === null) {
if (isEditing) {
return (
);
} else {
return (
Click a note on the left to view something! 🥺
);
}
}
const noteResponse = await fetch(`http://localhost:4000/notes/${selectedId}`);
const note = await noteResponse.json();
let {id, title, body, updated_at} = note;
const updatedAt = new Date(updated_at);
// We could also read from a file instead.
// body = await readFile(resolve(`./notes/${note.id}.md`), 'utf8');
// Now let's see how the Suspense boundary above lets us not block on this.
// await fetch('http://localhost:4000/sleep/3000');
if (isEditing) {
return ;
} else {
return (
{title}
Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")}
Edit
);
}
}
================================================
FILE: chapter11/server-components-demo/src/NoteEditor.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';
import {useState, useTransition} from 'react';
import {useRouter, useMutation} from './framework/router';
import NotePreview from './NotePreview';
export default function NoteEditor({noteId, initialTitle, initialBody}) {
const [title, setTitle] = useState(initialTitle);
const [body, setBody] = useState(initialBody);
const {location} = useRouter();
const [isNavigating, startNavigating] = useTransition();
const [isSaving, saveNote] = useMutation({
endpoint: noteId !== null ? `/notes/${noteId}` : `/notes`,
method: noteId !== null ? 'PUT' : 'POST',
});
const [isDeleting, deleteNote] = useMutation({
endpoint: `/notes/${noteId}`,
method: 'DELETE',
});
async function handleSave() {
const payload = {title, body};
const requestedLocation = {
selectedId: noteId,
isEditing: false,
searchText: location.searchText,
};
await saveNote(payload, requestedLocation);
}
async function handleDelete() {
const payload = {};
const requestedLocation = {
selectedId: null,
isEditing: false,
searchText: location.searchText,
};
await deleteNote(payload, requestedLocation);
}
const isDraft = noteId === null;
return (
handleSave()}
role="menuitem">
Done
{!isDraft && (
handleDelete()}
role="menuitem">
Delete
)}
Preview
{title}
);
}
================================================
FILE: chapter11/server-components-demo/src/NoteList.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {db} from './db';
import SidebarNote from './SidebarNote';
export default async function NoteList({searchText}) {
// const notes = await (await fetch('http://localhost:4000/notes')).json();
// WARNING: This is for demo purposes only.
// We don't encourage this in real apps. There are far safer ways to access
// data in a real application!
const notes = (await db.query(
`select * from notes where title ilike $1 order by id desc`,
['%' + searchText + '%']
)).rows;
// Now let's see how the Suspense boundary above lets us not block on this.
// await fetch('http://localhost:4000/sleep/3000');
return notes.length > 0 ? (
{notes.map((note) => (
))}
) : (
{searchText
? `Couldn't find any notes titled "${searchText}".`
: 'No notes created yet!'}{' '}
);
}
================================================
FILE: chapter11/server-components-demo/src/NoteListSkeleton.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export default function NoteListSkeleton() {
return (
);
}
================================================
FILE: chapter11/server-components-demo/src/NotePreview.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import TextWithMarkdown from './TextWithMarkdown';
export default function NotePreview({body}) {
return (
);
}
================================================
FILE: chapter11/server-components-demo/src/NoteSkeleton.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export default function NoteSkeleton({isEditing}) {
return isEditing ? : ;
}
function NoteEditorSkeleton() {
return (
);
}
function NotePreviewSkeleton() {
return (
);
}
================================================
FILE: chapter11/server-components-demo/src/SearchField.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';
import {useState, useTransition} from 'react';
import {useRouter} from './framework/router';
import Spinner from './Spinner';
export default function SearchField() {
const [text, setText] = useState('');
const [isSearching, startSearching] = useTransition();
const {navigate} = useRouter();
return (
e.preventDefault()}>
Search for a note by title
{
const newText = e.target.value;
setText(newText);
startSearching(() => {
navigate({
searchText: newText,
});
});
}}
/>
);
}
================================================
FILE: chapter11/server-components-demo/src/SidebarNote.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {format, isToday} from 'date-fns';
import excerpts from 'excerpts';
import {marked} from 'marked';
import SidebarNoteContent from './SidebarNoteContent';
export default function SidebarNote({note}) {
const updatedAt = new Date(note.updated_at);
const lastUpdatedAt = isToday(updatedAt)
? format(updatedAt, 'h:mm bb')
: format(updatedAt, 'M/d/yy');
const summary = excerpts(marked(note.body), {words: 20});
return (
{summary || (No content) }
}>
{note.title}
{lastUpdatedAt}
);
}
================================================
FILE: chapter11/server-components-demo/src/SidebarNoteContent.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';
import {useState, useRef, useEffect, useTransition} from 'react';
import {useRouter} from './framework/router';
export default function SidebarNoteContent({id, title, children, expandedChildren}) {
const {location, navigate} = useRouter();
const [isPending, startTransition] = useTransition();
const [isExpanded, setIsExpanded] = useState(false);
const isActive = id === location.selectedId;
// Animate after title is edited.
const itemRef = useRef(null);
const prevTitleRef = useRef(title);
useEffect(() => {
if (title !== prevTitleRef.current) {
prevTitleRef.current = title;
itemRef.current.classList.add('flash');
}
}, [title]);
return (
{
itemRef.current.classList.remove('flash');
}}
className={[
'sidebar-note-list-item',
isExpanded ? 'note-expanded' : '',
].join(' ')}>
{children}
{
startTransition(() => {
navigate({
selectedId: id,
isEditing: false,
});
});
}}>
Open note for preview
{
e.stopPropagation();
setIsExpanded(!isExpanded);
}}>
{isExpanded ? (
) : (
)}
{isExpanded && expandedChildren}
);
}
================================================
FILE: chapter11/server-components-demo/src/Spinner.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export default function Spinner({active = true}) {
return (
);
}
================================================
FILE: chapter11/server-components-demo/src/TextWithMarkdown.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {marked} from 'marked';
import sanitizeHtml from 'sanitize-html';
const allowedTags = sanitizeHtml.defaults.allowedTags.concat([
'img',
'h1',
'h2',
'h3',
]);
const allowedAttributes = Object.assign(
{},
sanitizeHtml.defaults.allowedAttributes,
{
img: ['alt', 'src'],
}
);
export default function TextWithMarkdown({text}) {
return (
);
}
================================================
FILE: chapter11/server-components-demo/src/db.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// Error early if this is accidentally imported on the client.
import 'server-only';
import {Pool} from 'pg';
import credentials from '../credentials';
// Don't keep credentials in the source tree in a real app!
export const db = new Pool(credentials);
================================================
FILE: chapter11/server-components-demo/src/framework/bootstrap.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// ---------------------------------------------------------
// Note: this code would usually be provided by a framework.
// ---------------------------------------------------------
import {createRoot} from 'react-dom/client';
import {ErrorBoundary} from 'react-error-boundary';
import {Router} from './router'
const root = createRoot(document.getElementById('root'));
root.render( );
function Root() {
return (
);
}
function Error({error}) {
return (
Application Error
{error.stack}
);
}
================================================
FILE: chapter11/server-components-demo/src/framework/router.js
================================================
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use client';
// ---------------------------------------------------------
// Note: this code would usually be provided by a framework.
// ---------------------------------------------------------
import {
createContext,
startTransition,
useContext,
useState,
use,
} from 'react';
import {createFromFetch, createFromReadableStream} from 'react-server-dom-webpack/client';
const RouterContext = createContext();
const initialCache = new Map();
export function Router() {
const [cache, setCache] = useState(initialCache);
const [location, setLocation] = useState({
selectedId: null,
isEditing: false,
searchText: '',
});
const locationKey = JSON.stringify(location);
let content = cache.get(locationKey);
if (!content) {
content = createFromFetch(
fetch('/react?location=' + encodeURIComponent(locationKey))
);
cache.set(locationKey, content);
}
function refresh(response) {
startTransition(() => {
const nextCache = new Map();
if (response != null) {
const locationKey = response.headers.get('X-Location');
const nextLocation = JSON.parse(locationKey);
const nextContent = createFromReadableStream(response.body);
nextCache.set(locationKey, nextContent);
navigate(nextLocation);
}
setCache(nextCache);
})
}
function navigate(nextLocation) {
startTransition(() => {
setLocation(loc => ({
...loc,
...nextLocation
}));
});
}
return (
{use(content)}
);
}
export function useRouter() {
return useContext(RouterContext);
}
export function useMutation({endpoint, method}) {
const {refresh} = useRouter();
const [isSaving, setIsSaving] = useState(false);
const [didError, setDidError] = useState(false);
const [error, setError] = useState(null);
if (didError) {
// Let the nearest error boundary handle errors while saving.
throw error;
}
async function performMutation(payload, requestedLocation) {
setIsSaving(true);
try {
const response = await fetch(
`${endpoint}?location=${encodeURIComponent(
JSON.stringify(requestedLocation)
)}`,
{
method,
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
}
);
if (!response.ok) {
throw new Error(await response.text());
}
refresh(response);
} catch (e) {
setDidError(true);
setError(e);
} finally {
setIsSaving(false);
}
}
return [isSaving, performMutation];
}
================================================
FILE: chapter2/react/.eslintignore
================================================
build
node_modules
================================================
FILE: chapter2/react/.eslintrc.js
================================================
module.exports = {
extends: [
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter2/react/.npmrc
================================================
auto-install-peers=true
================================================
FILE: chapter2/react/.prettierignore
================================================
build
node_modules
================================================
FILE: chapter2/react/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter2/react/README.md
================================================
# Chapter2
챕터2 예제 코드
================================================
FILE: chapter2/react/package.json
================================================
{
"name": "chapter2",
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/plugin-syntax-flow": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.14.9",
"@types/node": "^16.11.46",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"typescript": "^4.7.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"eslint": "^8.38.0",
"prettier": "^2.8.7"
}
}
================================================
FILE: chapter2/react/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter2/react/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter2/react/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter2/react/src/App.tsx
================================================
import { Outlet, Link } from 'react-router-dom'
export default function App() {
return (
Chapter2 예제 코드
예제 2-1 | 예제 2-4 |
예제 2-5 | 예제 2-7 |{' '}
예제 2-8 |
)
}
================================================
FILE: chapter2/react/src/index.tsx
================================================
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import ReactDOM from 'react-dom/client'
import App from './App'
import { Component1 } from './routes/2-1'
import SampleComponent from './routes/2-4'
import SampleComponent2 from './routes/2-5'
import CompareComponent from './routes/2-7'
import { ReactPureComponent } from './routes/2-8'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
}>
} />
} />
} />
} />
} />
,
)
================================================
FILE: chapter2/react/src/react-app-env.d.ts
================================================
///
================================================
FILE: chapter2/react/src/routes/2-1.tsx
================================================
import { ReactNode } from 'react'
function A({
children,
required,
}: {
required?: boolean
children?: ReactNode
}) {
return (
<>
{children}
>
)
}
function B({
text,
optionalChildren,
}: {
text?: string
optionalChildren?: ReactNode
}) {
return (
<>
{text}
{optionalChildren}
>
)
}
// 하나의 요소로 구성된 가장 단순한 형태
const ComponentA = 안녕하세요.
// 자식이 없이 SelfClosingTag로 닫혀있는 형태도 가능하다.
const ComponentB =
// 옵션을 { } 와 전개 연산자로 넣을 수 있다.
const ComponentC =
// 옵션명만 넣어도 가능하다.
const ComponentD =
// 옵션명과 속성을 넣을 수 있다.
const ComponentE =
const ComponentF = (
{/* 문자열은 쌍따옴표및 홀따옴표 모두 가능하다. */}
)
const ComponentG = (
{/* 옵선의 값으로 JSXElement를 넣는 것 또한 올바른 문법이다. */}
안녕하세요.>} />
)
const ComponentH = (
{/* 여러개의 자식도 포함할 수 있다. */}
안녕하세요
)
export function Component1() {
return (
<>
{ComponentA}
{ComponentB}
{ComponentC}
{ComponentD}
{ComponentE}
{ComponentF}
{ComponentG}
{ComponentH}
>
)
}
================================================
FILE: chapter2/react/src/routes/2-4.tsx
================================================
import React from 'react'
// props 타입을 선언한다.
interface SampleProps {
required?: boolean
text: string
}
// state 타입을 선언한다.
interface SampleState {
count: number
isLimited?: boolean
}
// Component에 제네릭으로 props, state를 순서대로 넣어준다.
class SampleComponent extends React.Component {
// constructor에서 props를 넘겨주고, state의 기본값을 설정한다.
private constructor(props: SampleProps) {
super(props)
this.state = {
count: 0,
isLimited: false,
}
}
// 렌더 내부에서 쓰일 함수를 선언한다.
private handleClick = () => {
const newValue = this.state.count + 1
this.setState({ count: newValue, isLimited: newValue >= 10 })
}
// render에서 이 컴포넌트가 렌더링할 내용을 정의한다.
public render() {
// props와 state 값을 this, 즉 해당 클래스에서 꺼낸다.
const {
props: { required, text },
state: { count, isLimited },
} = this
return (
Sample Component
{required ? '필수' : '필수아님'}
문자: {text}
count: {count}
증가
)
}
}
export default SampleComponent
================================================
FILE: chapter2/react/src/routes/2-5.tsx
================================================
import { Component } from 'react'
type Props = Record
interface State {
count: number
}
class SampleComponent extends Component {
private constructor(props: Props) {
super(props)
this.state = {
count: 1,
}
// handleClick의 this를 현재 클래스로 바인딩 시킨다.
this.handleClick = this.handleClick.bind(this)
}
private handleClick() {
this.setState((prev) => ({ count: prev.count + 1 }))
}
public render() {
const {
state: { count },
} = this
return (
증가
{count}
)
}
}
export default SampleComponent
================================================
FILE: chapter2/react/src/routes/2-7.tsx
================================================
import React from 'react'
interface State {
count: number
}
type Props = Record
export class ReactComponent extends React.Component {
private renderCounter = 0
private constructor(props: Props) {
super(props)
this.state = {
count: 1,
}
}
private handleClick = () => {
this.setState({ count: 1 })
}
public render() {
console.log('ReactComponent', ++this.renderCounter) // eslint-disable-line no-console
return (
ReactComponent: {this.state.count}{' '}
+
)
}
}
export class ReactPureComponent extends React.PureComponent {
private renderCounter = 0
private constructor(props: Props) {
super(props)
this.state = {
count: 1,
}
}
private handleClick = () => {
this.setState({ count: 1 })
}
public render() {
console.log('ReactPureComponent', ++this.renderCounter) // eslint-disable-line no-console
return (
ReactPureComponent: {this.state.count}{' '}
+
)
}
}
export default function CompareComponent() {
return (
<>
React.Component
React.PureComponent
>
)
}
================================================
FILE: chapter2/react/src/routes/2-8.tsx
================================================
import React from 'react'
interface State {
count: number
}
type Props = Record
export class ReactPureComponent extends React.PureComponent {
private constructor(props: Props) {
super(props)
this.state = {
count: 1,
}
}
private handleClick = () => {
console.log('handleClick!') // eslint-disable-line no-console
this.setState({ count: 1 })
}
private handleChange = () => {
console.log('handleChanged!') // eslint-disable-line no-console
}
public render() {
return (
<>
ReactPureComponent: {this.state.count}{' '}
+
위 빌드 결과가 알고 싶다면, 빌드를 수행한 뒤에 번들링 결과물에서
console.log 내용을 검색해보면 된다. 코드가 읽기 어렵다면 전체코드를
여기 에서 보기 좋게 포맷팅해보자.
>
)
}
}
================================================
FILE: chapter2/react/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
================================================
FILE: chapter4/next-example/.eslintignore
================================================
.next
node_modules
================================================
FILE: chapter4/next-example/.eslintrc.js
================================================
module.exports = {
extends: [
'next/core-web-vitals',
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter4/next-example/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter4/next-example/.prettierignore
================================================
.next
node_modules
================================================
FILE: chapter4/next-example/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter4/next-example/README.md
================================================
# next-example
## Introduction
`create-next-app`으로 만들어진 예제 애플리케이션 입니다.
================================================
FILE: chapter4/next-example/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig
================================================
FILE: chapter4/next-example/package.json
================================================
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"dependencies": {
"@next/font": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"next": "13.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"eslint": "^8.38.0",
"eslint-config-next": "13.1.6",
"prettier": "^2.8.7"
}
}
================================================
FILE: chapter4/next-example/src/pages/404.tsx
================================================
import { useCallback } from 'react'
export default function My404Page() {
const handleClick = useCallback(() => {
console.log('hi') // eslint-disable-line no-console
}, [])
return (
페이지를 찾을 수 없습니다. 클릭
)
}
================================================
FILE: chapter4/next-example/src/pages/500.tsx
================================================
import { useCallback } from 'react'
export default function My500Page() {
const handleClick = useCallback(() => {
console.log('hi') // eslint-disable-line no-console
}, [])
return (
(500페이지) 서버에서 에러가 발생했습니다.{' '}
클릭
)
}
================================================
FILE: chapter4/next-example/src/pages/_app.tsx
================================================
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return
}
================================================
FILE: chapter4/next-example/src/pages/_document.tsx
================================================
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
)
}
================================================
FILE: chapter4/next-example/src/pages/_error.tsx
================================================
import { NextPageContext } from 'next'
function Error({ statusCode }: { statusCode: number }) {
return (
<>
{statusCode ? `서버에서 ${statusCode}` : '클라이언트에서'} 에러가
발생했습니다.
>
)
}
Error.getInitialProps = ({ res, err }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : ''
return { statusCode }
}
export default Error
================================================
FILE: chapter4/next-example/src/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
interface Data {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: chapter4/next-example/src/pages/hello/[greeting].tsx
================================================
import { NextPageContext } from 'next'
export default function HelloGreeting({ greeting }: { greeting: string }) {
return hello {greeting}
}
export const getServerSideProps = (context: NextPageContext) => {
const {
query: { greeting },
} = context
return {
props: {
greeting,
},
}
}
================================================
FILE: chapter4/next-example/src/pages/hello/world.tsx
================================================
export default function HelloWorld() {
return <>hello world>
}
================================================
FILE: chapter4/next-example/src/pages/hello.tsx
================================================
export default function Hello() {
console.log(typeof window === 'undefined' ? '서버' : '클라이언트') // eslint-disable-line no-console
return <>hello>
}
export const getServerSideProps = () => {
return {
props: {},
}
}
================================================
FILE: chapter4/next-example/src/pages/hi/[...props].tsx
================================================
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { NextPageContext } from 'next'
export default function HiAll({ props: serverProps }: { props: string[] }) {
const {
query: { props },
} = useRouter()
useEffect(() => {
/* eslint-disable no-console */
console.log(props)
console.log(JSON.stringify(props) === JSON.stringify(serverProps)) // true
/* eslint-enable no-console */
}, [props, serverProps])
return (
<>
hi{' '}
{serverProps.map((item) => (
{item}
))}
>
)
}
export const getServerSideProps = (context: NextPageContext) => {
const {
query: { props },
} = context
return {
props: {
props,
},
}
}
================================================
FILE: chapter4/next-example/src/pages/index.tsx
================================================
import type { NextPage } from 'next'
import Link from 'next/link'
const Home: NextPage = () => {
return (
{/* next의 eslint 룰을 잠시 끄기 위해 추가했다. */}
{/* eslint-disable-next-line */}
A 태그로 이동
{/* 차이를 극적으로 보여주기 위해 해당 페이지의 리소스를 미리 가져오는 prefetch를 잠시 꺼두었다. */}
next/link로 이동
)
}
export default Home
================================================
FILE: chapter4/next-example/src/pages/todo/[id].tsx
================================================
import Link from 'next/link'
import { NextPageContext } from 'next'
export default function Todo({
todo,
}: {
todo: { userId: number; id: number; title: string; completed: boolean }
}) {
return (
<>
{todo.title}
>
)
}
Todo.getInitialProps = async (ctx: NextPageContext) => {
const {
query: { id = '' },
// asPath,
// query,
// res,
} = ctx
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`,
)
const result = await response.json()
return { todo: result }
}
================================================
FILE: chapter4/next-example/src/styles/Home.module.css
================================================
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo,
.thirteen {
position: relative;
}
.thirteen {
display: flex;
justify-content: center;
align-items: center;
width: 75px;
height: 75px;
padding: 25px 10px;
margin-left: 16px;
transform: translateZ(0);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0px 2px 8px -1px #0000001a;
}
.thirteen::before,
.thirteen::after {
content: '';
position: absolute;
z-index: -1;
}
/* Conic Gradient Animation */
.thirteen::before {
animation: 6s rotate linear infinite;
width: 200%;
height: 200%;
background: var(--tile-border);
}
/* Inner Square */
.thirteen::after {
inset: 0;
padding: 1px;
border-radius: var(--border-radius);
background: linear-gradient(
to bottom right,
rgba(var(--tile-start-rgb), 1),
rgba(var(--tile-end-rgb), 1)
);
background-clip: content-box;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.thirteen::before {
animation: none;
}
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo,
.thirteen img {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
================================================
FILE: chapter4/next-example/src/styles/globals.css
================================================
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
================================================
FILE: chapter4/next-example/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter4/ssr-example/.eslintignore
================================================
dist/
================================================
FILE: chapter4/ssr-example/.eslintrc.js
================================================
module.exports = {
extends: [
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter4/ssr-example/.prettierignore
================================================
public/
dist/
.eslintrc.js
.prettierrc
.eslintignore
.prettierignore
================================================
FILE: chapter4/ssr-example/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter4/ssr-example/README.md
================================================
# SSR-Example
## Introduction
`react-dom`의 `renderToString`과 `renderToNodeStream`을 활용하여 리액트 서버사이드 애플리케이션을 간단하게 만들어보았습니다. 학습용으로 제작되어 많은 부분이 생략되었으며, 절대로 프로덕션 서비스에 사용해서는 안됩니다.
## How to Start
1. `npm install`
2. `npm run build`
3. `npm run start`
4. 다음 페이지에서 서버사이드 렌더링을 확인해보세요.
- http://localhost:3000/
- http://localhost:3000/stream
================================================
FILE: chapter4/ssr-example/checkStream.js
================================================
// 이 코드는 브라우저에서만 실행된다.
/* eslint-disable no-console */
const main = async () => {
// chrome에서 발생한 네트워크 요청을 복사해서 가져왔다.
const response = await fetch('http://localhost:3000/stream')
const reader = response.body.getReader()
while (true) {
const { value, done } = await reader.read()
const str = new TextDecoder().decode(value)
if (done) {
break
}
console.log(`===================================`)
console.log(str)
}
console.log('Response fully received')
}
main()
================================================
FILE: chapter4/ssr-example/package.json
================================================
{
"name": "ssr-example",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"start": "node dist/server.js",
"build": "webpack --mode production",
"build:dev": "webpack --mode development --watch",
"lint": "eslint '**/*.{js,ts,tsx}'",
"lint:fix": "npm run lint -- --fix",
"prettier": "prettier '**/*' --check",
"prettier:fix": "prettier '**/*' --write"
},
"author": "yceffort ",
"dependencies": {
"@types/isomorphic-fetch": "^0.0.36",
"@types/node": "^16",
"@types/react": "^17.0.8",
"@types/react-dom": "^17.0.5",
"@types/webpack": "^5.28.0",
"concurrently": "^7.3.0",
"isomorphic-fetch": "^3.0.0",
"raw-loader": "^4.0.2",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"source-map-loader": "^4.0.0",
"ts-loader": "^9.3.1",
"typescript": "^4.5.5",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-node-externals": "^3.0.0"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"eslint": "^8.38.0",
"prettier": "^2.8.7"
},
"license": "MIT"
}
================================================
FILE: chapter4/ssr-example/public/index-end.html
================================================
================================================
FILE: chapter4/ssr-example/public/index-front.html
================================================
SSR Example
================================================
FILE: chapter4/ssr-example/public/index.html
================================================
SSR Example
__placeholder__
================================================
FILE: chapter4/ssr-example/src/components/App.tsx
================================================
import React, { useEffect } from 'react'
import { TodoResponse } from '../fetch'
import { Todo } from './Todo'
export default function App({ todos }: { todos: Array }) {
useEffect(() => {
console.log('하이!') // eslint-disable-line no-console
}, [])
return (
<>
나의 할일!
{todos.map((todo, index) => (
))}
>
)
}
================================================
FILE: chapter4/ssr-example/src/components/Todo.tsx
================================================
import React, { useState } from 'react'
import { TodoResponse } from '../fetch'
export function Todo({ todo }: { todo: TodoResponse }) {
const { title, completed, userId, id } = todo
const [finished, setFinished] = useState(completed)
function handleClick() {
setFinished((prev) => !prev)
}
return (
{userId}-{id}) {title} {finished ? '완료' : '미완료'}
토글
)
}
================================================
FILE: chapter4/ssr-example/src/fetch/index.ts
================================================
import fetch from 'isomorphic-fetch'
export interface TodoResponse {
userId: number
id: number
title: string
completed: boolean
}
export async function fetchTodo() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const result: TodoResponse[] = await response.json()
return result
// 스트림의 극단적인 예제를 보고 싶다면 주석 해제
// return Array(10).fill(result).flat()
}
================================================
FILE: chapter4/ssr-example/src/index.tsx
================================================
import React from 'react'
import { hydrate } from 'react-dom'
import App from './components/App'
import { fetchTodo } from './fetch'
async function main() {
const result = await fetchTodo()
const app =
const el = document.getElementById('root')
hydrate(app, el)
}
main()
================================================
FILE: chapter4/ssr-example/src/server.ts
================================================
import { createServer, IncomingMessage, ServerResponse } from 'http'
import { createReadStream } from 'fs'
import { renderToNodeStream, renderToString } from 'react-dom/server'
import { createElement } from 'react'
import html from '../public/index.html'
import indexFront from '../public/index-front.html'
import indexEnd from '../public/index-end.html'
import App from './components/App'
import { fetchTodo } from './fetch'
const PORT = process.env.PORT || 3000
async function serverHandler(req: IncomingMessage, res: ServerResponse) {
const { url } = req
switch (url) {
case '/': {
const result = await fetchTodo()
const rootElement = createElement(
'div',
{ id: 'root' },
createElement(App, { todos: result }),
)
const renderResult = renderToString(rootElement)
const htmlResult = html.replace('__placeholder__', renderResult)
res.setHeader('Content-Type', 'text/html')
res.write(htmlResult)
res.end()
return
}
case '/stream': {
res.setHeader('Content-Type', 'text/html')
res.write(indexFront)
const result = await fetchTodo()
const rootElement = createElement(
'div',
{ id: 'root' },
createElement(App, { todos: result }),
)
const stream = renderToNodeStream(rootElement)
stream.pipe(res, { end: false })
stream.on('end', () => {
res.write(indexEnd)
res.end()
})
return
}
case '/browser.js': {
res.setHeader('Content-Type', 'application/javascript')
createReadStream(`./dist/browser.js`).pipe(res)
return
}
case '/browser.js.map': {
res.setHeader('Content-Type', 'application/javascript')
createReadStream(`./dist/browser.js.map`).pipe(res)
return
}
default: {
res.statusCode = 404
res.end('404 Not Found')
}
}
}
function main() {
createServer(serverHandler).listen(PORT, () => {
console.log(`Server has been started ${PORT}...`) // eslint-disable-line no-console
})
}
main()
================================================
FILE: chapter4/ssr-example/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "ES2022",
"moduleResolution": "node",
"jsx": "react",
"sourceMap": true,
"strict": true,
"noEmitOnError": true,
"esModuleInterop": true
},
"exclude": ["node_modules"]
}
================================================
FILE: chapter4/ssr-example/typings.d.ts
================================================
declare module '*.html' {
const content: string
export default content
}
================================================
FILE: chapter4/ssr-example/watch-stream.js
================================================
// fetch가 기본 제공 되는 node 19버전 부터 사용가능하다.
// 만약 이하 버전에서 사용하고 싶다면 `node-fetch`를 사용하자.
// const fetch = require('node-fetch')
;(async () => {
const response = await fetch('http://localhost:3000')
try {
for await (const chunk of response.body) {
// eslint-disable-next-line no-console
console.log('------chunk-----')
// eslint-disable-next-line no-console
console.log(Buffer.from(chunk).toString())
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(err.stack)
}
})()
================================================
FILE: chapter4/ssr-example/webpack.config.js
================================================
// @ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
const path = require('path')
const nodeExternals = require('webpack-node-externals')
/** @type WebpackConfig[] */
const configs = [
{
entry: {
browser: './src/index.tsx',
},
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx'],
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
},
],
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
{
entry: {
server: './src/server.ts',
},
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.tsx'],
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
},
{
test: /\.html$/,
use: 'raw-loader',
},
],
},
target: 'node',
externals: [nodeExternals()],
},
]
module.exports = configs
================================================
FILE: chapter8/eslint-plugin-yceffort/.eslintrc.js
================================================
"use strict";
module.exports = {
root: true,
extends: [
"@titicaca/eslint-config-triple",
"@titicaca/eslint-config-triple/frontend",
"@titicaca/eslint-config-triple/prettier"
],
env: {
node: true,
},
overrides: [
{
files: ["tests/**/*.js"],
env: { mocha: true },
},
],
};
================================================
FILE: chapter8/eslint-plugin-yceffort/.npmrc
================================================
registry=https://registry.npmjs.org/
================================================
FILE: chapter8/eslint-plugin-yceffort/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter8/eslint-plugin-yceffort/README.md
================================================
# eslint-plugin-yceffort
yceffort
## Installation
You'll first need to install [ESLint](https://eslint.org/):
```sh
npm i eslint --save-dev
```
Next, install `eslint-plugin-yceffort`:
```sh
npm install eslint-plugin-yceffort --save-dev
```
## Usage
Add `yceffort` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
```json
{
"plugins": [
"yceffort"
]
}
```
Then configure the rules you want to use under the rules section.
```json
{
"rules": {
"yceffort/rule-name": 2
}
}
```
## Supported Rules
* Fill in provided rules here
================================================
FILE: chapter8/eslint-plugin-yceffort/docs/rules/no-new-date.md
================================================
# yceffort (no-new-date)
기기 시간에 의존적인 `new Date()`를 사용하지 마세요. 대신 `ServerDate()` 함수를 만들어 사용하세요.
## Rule Details
This rule aims to...
Examples of **incorrect** code for this rule:
```js
// ❌
new Date()
```
Examples of **correct** code for this rule:
```js
// 👌
new Date('2022-01-01')
// 👌
ServerDate()
```
## When Not To Use It
- `ServerDate()` 함수가 없는 경우
- 기기 시간에 의존해도 상관없는 경우
## Further Reading
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date
================================================
FILE: chapter8/eslint-plugin-yceffort/lib/index.js
================================================
/**
* @fileoverview yceffort
* @author yceffort
*/
"use strict";
const path = require('path')
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const requireIndex = require("requireindex");
// ------------------------------------------------------------------------------
// Plugin Definition
// ------------------------------------------------------------------------------
// import all rules in lib/rules
module.exports.rules = requireIndex(path.resolve(__dirname, "/rules"));
================================================
FILE: chapter8/eslint-plugin-yceffort/lib/rules/no-new-date.js
================================================
/**
* @fileoverview yceffort
* @author yceffort
*/
"use strict";
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
/**
*
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow use of the new Date()',
recommended: false,
},
fixable: 'code',
schema: [],
messages: {
message: 'new Date()는 클라이언트에서 실행시 해당 기기의 시간에 의존적이라 정확하지 않습니다. 현재 시간이 필요하다면 ServerDate()를 사용해주세요.'
},
},
create: function (context) {
return {
NewExpression: function (node) {
if (node.callee.name === 'Date' && node.arguments.length === 0) {
context.report({
node,
messageId: 'message',
fix: function (fixer) {
return fixer.replaceText(node, 'ServerDate()')
},
})
}
},
}
},
}
================================================
FILE: chapter8/eslint-plugin-yceffort/package.json
================================================
{
"name": "eslint-plugin-yceffort",
"version": "0.0.0",
"description": "yceffort",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin"
],
"author": "yceffort",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"scripts": {
"lint": "eslint . --fix",
"prettier": "prettier . --write",
"test": "mocha tests --recursive"
},
"dependencies": {
"requireindex": "^1.2.0"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"eslint": "^8.38.0",
"mocha": "^10.0.0"
},
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
},
"peerDependencies": {
"eslint": ">=7"
},
"license": "ISC"
}
================================================
FILE: chapter8/eslint-plugin-yceffort/tests/lib/rules/no-new-date.js
================================================
/**
* @fileoverview yceffort
* @author yceffort
*/
"use strict";
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const RuleTester = require("eslint").RuleTester;
const rule = require("../../../lib/rules/no-new-date");
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
const ruleTester = new RuleTester();
ruleTester.run("no-new-date", rule, {
valid: [
{
code: 'new Date(2021, 1, 1)',
},
{
code: 'new Date("2022-01-01")',
},
],
invalid: [
{
code: "new Date()",
errors: [{ message: rule.meta.messages.message }],
output: "ServerDate()"
},
],
});
================================================
FILE: chapter8/react-test/.eslintignore
================================================
build
reportWebVitals.ts
================================================
FILE: chapter8/react-test/.eslintrc.js
================================================
module.exports = {
extends: [
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter8/react-test/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter8/react-test/.prettierignore
================================================
build
================================================
FILE: chapter8/react-test/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter8/react-test/README.md
================================================
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If 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.
Instead, 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.
You 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.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
================================================
FILE: chapter8/react-test/package.json
================================================
{
"name": "react-test",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.64",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"typescript": "^4.8.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"eslint": "^8.38.0",
"msw": "^0.47.4",
"prettier": "^2.8.7"
}
}
================================================
FILE: chapter8/react-test/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter8/react-test/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter8/react-test/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter8/react-test/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter8/react-test/src/App.test.tsx
================================================
import { render, screen } from '@testing-library/react'
import App from './App'
test('renders learn react link', () => {
render( )
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})
================================================
FILE: chapter8/react-test/src/App.tsx
================================================
import logo from './logo.svg'
import './App.css'
import StaticComponent from './components/StaticComponent'
function App() {
return (
)
}
export default App
================================================
FILE: chapter8/react-test/src/components/FetchComponent/index.test.tsx
================================================
import { fireEvent, render, screen } from '@testing-library/react'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { FetchComponent } from '.'
const MOCK_TODO_RESPONSE = {
userId: 1,
id: 1,
title: 'delectus aut autem',
completed: false,
}
const server = setupServer(
rest.get('/todos/:id', (req, res, ctx) => {
const todoId = req.params.id
if (Number(todoId)) {
return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))
} else {
return res(ctx.status(404))
}
}),
)
beforeAll(() => server.listen())
// afterEach(() => server.resetHandlers());
afterAll(() => server.close())
beforeEach(() => {
render( )
})
describe('FetchComponent 테스트', () => {
it('데이터를 불러오기 전에는 기본 문구가 뜬다.', async () => {
const nowLoading = screen.getByText(/불러온 데이터가 없습니다./)
expect(nowLoading).toBeInTheDocument()
})
it('버튼을 클릭하면 데이터를 불러온다.', async () => {
const button = screen.getByRole('button', { name: /1번/ })
fireEvent.click(button)
const data = await screen.findByText(MOCK_TODO_RESPONSE.title)
expect(data).toBeInTheDocument()
})
it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => {
server.use(
rest.get('/todos/:id', (req, res, ctx) => {
return res(ctx.status(503))
}),
)
const button = screen.getByRole('button', { name: /1번/ })
fireEvent.click(button)
const error = await screen.findByText(/에러가 발생했습니다/)
expect(error).toBeInTheDocument()
})
})
================================================
FILE: chapter8/react-test/src/components/FetchComponent/index.tsx
================================================
import { MouseEvent, useState } from 'react'
interface TodoResponse {
userId: number
id: number
title: string
completed: false
}
export function FetchComponent() {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
async function handleButtonClick(e: MouseEvent) {
const id = e.currentTarget.dataset.id
const response = await fetch(`/todos/${id}`)
if (response.ok) {
const result: TodoResponse = await response.json()
setData(result)
} else {
setError(response.status)
}
}
return (
{data === null ? '불러온 데이터가 없습니다.' : data.title}
{error &&
에러가 발생했습니다
}
{Array.from({ length: 10 }).map((_, index) => {
const id = index + 1
return (
{`${id}번`}
)
})}
)
}
================================================
FILE: chapter8/react-test/src/components/StateComponent/index.test.tsx
================================================
import { fireEvent, render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { InputComponent } from '.'
describe('InputComponent 테스트', () => {
const setup = () => {
const screen = render( )
const input = screen.getByLabelText('input') as HTMLInputElement
const button = screen.getByText(/제출하기/i) as HTMLButtonElement
return {
input,
button,
...screen,
}
}
it('input의 초기값은 빈 문자열이다.', () => {
const { input } = setup()
expect(input.value).toEqual('')
})
it('input의 최대길이가 20자로 설정되어 있다.', () => {
const { input } = setup()
expect(input).toHaveAttribute('maxlength', '20')
})
it('영문과 숫자만 입력된다.', () => {
const { input } = setup()
const inputValue = '안녕하세요123'
userEvent.type(input, inputValue)
expect(input.value).toEqual('123')
})
it('아이디를 입력하지 않으면 버튼이 활성화 되지 않는다.', () => {
const { button } = setup()
expect(button).toBeDisabled()
})
it('아이디를 입력하면 버튼이 활성화 된다.', () => {
const { button, input } = setup()
const inputValue = 'helloworld'
userEvent.type(input, inputValue)
expect(input.value).toEqual(inputValue)
expect(button).toBeEnabled()
})
it('버튼을 클릭하면 alert가 해당 아이디로 뜬다.', () => {
const alertMock = jest
.spyOn(window, 'alert')
.mockImplementation((_: string) => undefined)
const { button, input } = setup()
const inputValue = 'helloworld'
userEvent.type(input, inputValue)
fireEvent.click(button)
expect(alertMock).toHaveBeenCalledTimes(1)
expect(alertMock).toHaveBeenCalledWith(inputValue)
})
})
================================================
FILE: chapter8/react-test/src/components/StateComponent/index.tsx
================================================
import { useState } from 'react'
export function InputComponent() {
const [text, setText] = useState('')
function handleInputChange(event: React.ChangeEvent) {
const rawValue = event.target.value
const value = rawValue.replace(/[^A-Za-z0-9]/gi, '')
setText(value)
}
function handleButtonClick() {
alert(text)
}
return (
<>
아이디를 입력하세요.
제출하기
>
)
}
================================================
FILE: chapter8/react-test/src/components/StaticComponent/index.test.tsx
================================================
import { render, screen } from '@testing-library/react'
import StaticComponent from './index'
beforeEach(() => {
render( )
})
describe('링크 확인', () => {
it('링크가 3개 존재한다.', () => {
const ul = screen.getByTestId('ul')
expect(ul.children.length).toBe(3)
})
it('링크 목록의 스타일이 square다.', () => {
const ul = screen.getByTestId('ul')
expect(ul).toHaveStyle('list-style-type: square;')
})
})
describe('리액트 링크 테스트', () => {
it('리액트 링크가 존재한다.', () => {
const reactLink = screen.getByText('리액트')
expect(reactLink).toBeVisible()
})
it('리액트 링크가 올바른 주소로 존재한다.', () => {
const reactLink = screen.getByText('리액트')
expect(reactLink.tagName).toEqual('A')
expect(reactLink).toHaveAttribute('href', 'https://reactjs.org')
})
})
describe('네이버 링크 테스트', () => {
it('네이버 링크가 존재한다.', () => {
const naverLink = screen.getByText('네이버')
expect(naverLink).toBeVisible()
})
it('네이버 링크가 올바른 주소로 존재한다.', () => {
const naverLink = screen.getByText('네이버')
expect(naverLink.tagName).toEqual('A')
expect(naverLink).toHaveAttribute('href', 'https://www.naver.com')
})
})
describe('블로그 링크 테스트', () => {
it('블로그 링크가 존재한다.', () => {
const blogLink = screen.getByText('블로그')
expect(blogLink).toBeVisible()
})
it('블로그 링크가 올바른 주소로 존재한다.', () => {
const blogLink = screen.getByText('블로그')
expect(blogLink.tagName).toEqual('A')
expect(blogLink).toHaveAttribute('href', 'https://yceffort.kr')
})
it('블로그는 같은 창에서 열려야 한다.', () => {
const blogLink = screen.getByText('블로그')
expect(blogLink).not.toHaveAttribute('target')
})
})
================================================
FILE: chapter8/react-test/src/components/StaticComponent/index.tsx
================================================
import { memo } from 'react'
const AnchorTagComponent = memo(function AnchorTagComponent({
name,
href,
targetBlank,
}: {
name: string
href: string
targetBlank?: boolean
}) {
return (
{name}
)
})
export default function StaticComponent() {
return (
<>
Static Component
유용한 링크
>
)
}
================================================
FILE: chapter8/react-test/src/hooks/useEffectDebugger.test.ts
================================================
import { renderHook } from '@testing-library/react'
import useEffectDebugger, { CONSOLE_PREFIX } from './useEffectDebugger'
const consoleSpy = jest.spyOn(console, 'log')
const componentName = 'TestComponent'
describe('useEffectDebugger', () => {
afterAll(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
process.env.NODE_ENV = 'development'
})
it('props가 없으면 호출되지 않는다.', () => {
renderHook(() => useEffectDebugger(componentName))
expect(consoleSpy).not.toHaveBeenCalled()
})
it('최초에는 호출되지 않는다.', () => {
const props = { hello: 'world' }
renderHook(() => useEffectDebugger(componentName, props))
expect(consoleSpy).not.toHaveBeenCalled()
})
it('props가 변경되지 않으면 호출되지 않는다.', () => {
const props = { hello: 'world' }
const { rerender } = renderHook(() =>
useEffectDebugger(componentName, props),
)
expect(consoleSpy).not.toHaveBeenCalled()
rerender()
expect(consoleSpy).not.toHaveBeenCalled()
})
it('props가 변경되면 다시 호출한다.', () => {
const props = { hello: 'world' }
const { rerender } = renderHook(
({ componentName, props }) => useEffectDebugger(componentName, props),
{
initialProps: {
componentName,
props,
},
},
)
const newProps = { hello: 'world2' }
rerender({ componentName, props: newProps })
expect(consoleSpy).toHaveBeenCalled()
})
it('props가 변경되면 변경된 props를 정확히 출력한다', () => {
const props = { hello: 'world' }
const { rerender } = renderHook(
({ componentName, props }) => useEffectDebugger(componentName, props),
{
initialProps: {
componentName,
props,
},
},
)
const newProps = { hello: 'world2' }
rerender({ componentName, props: newProps })
expect(consoleSpy).toHaveBeenCalledWith(CONSOLE_PREFIX, 'TestComponent', {
hello: { after: 'world2', before: 'world' },
})
})
it('객체는 참조가 다르다면 변경된 것으로 간주한다', () => {
const props = { hello: { hello: 'world' } }
const newProps = { hello: { hello: 'world' } }
const { rerender } = renderHook(
({ componentName, props }) => useEffectDebugger(componentName, props),
{
initialProps: {
componentName,
props,
},
},
)
rerender({ componentName, props: newProps })
// 이후 호출
expect(consoleSpy).toHaveBeenCalled()
})
it('process.env.NODE_ENV가 production이면 호출되지 않는다', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
process.env.NODE_ENV = 'production'
const props = { hello: 'world' }
const { rerender } = renderHook(
({ componentName, props }) => useEffectDebugger(componentName, props),
{
initialProps: {
componentName,
props,
},
},
)
const newProps = { hello: 'world2' }
rerender({ componentName, props: newProps })
expect(consoleSpy).not.toHaveBeenCalled()
})
})
================================================
FILE: chapter8/react-test/src/hooks/useEffectDebugger.ts
================================================
import { useEffect, useRef } from 'react'
export type Props = Record
export const CONSOLE_PREFIX = '[useEffectDebugger]'
export default function useEffectDebugger(
componentName: string,
props?: Props,
) {
const prevProps = useRef()
useEffect(() => {
if (process.env.NODE_ENV === 'production') {
return
}
const prevPropsCurrent = prevProps.current
if (prevPropsCurrent !== undefined) {
const allKeys = Object.keys({ ...prevProps.current, ...props })
const changedProps: Props = allKeys.reduce((result, key) => {
const prevValue = prevPropsCurrent[key]
const currentValue = props ? props[key] : undefined
if (!Object.is(prevValue, currentValue)) {
result[key] = {
before: prevValue,
after: currentValue,
}
}
return result
}, {})
if (Object.keys(changedProps).length) {
// eslint-disable-next-line no-console
console.log(CONSOLE_PREFIX, componentName, changedProps)
}
}
prevProps.current = props
})
}
================================================
FILE: chapter8/react-test/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter8/react-test/src/index.tsx
================================================
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
,
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// eslint-disable-next-line
reportWebVitals(console.log)
================================================
FILE: chapter8/react-test/src/react-app-env.d.ts
================================================
///
================================================
FILE: chapter8/react-test/src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals
================================================
FILE: chapter8/react-test/src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'
================================================
FILE: chapter8/react-test/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
================================================
FILE: chapter9/danger-react-app/.eslintignore
================================================
build/
node_modules/
================================================
FILE: chapter9/danger-react-app/.eslintrc.js
================================================
module.exports = {
extends: [
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter9/danger-react-app/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter9/danger-react-app/.npmrc
================================================
registry=https://registry.npmjs.org/
================================================
FILE: chapter9/danger-react-app/.prettierignore
================================================
build/
node_modules/
================================================
FILE: chapter9/danger-react-app/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter9/danger-react-app/README.md
================================================
# danger-react-app
================================================
FILE: chapter9/danger-react-app/package.json
================================================
{
"name": "danger-react-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.1.3",
"mobx": "^5.11.0",
"mobx-react-lite": "^1.4.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"react-scripts": "^3.4.1",
"react-swipeable-views": "^0.13.3"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"overrides": {
"ansi-html": "^0.0.8",
"browserslist": "^4.21.4",
"immer": "^9.0.6",
"jsdom": "^16.5.0",
"glob-parent": "^5.1.2",
"minimatch": "^3.0.5",
"node-notifier": "^8.0.1",
"nth-check": "^2.1.1",
"node-forge": "^1.3.0",
"postcss": "^7.0.36",
"react-dev-utils": "^11.0.4",
"shell-quote": "^1.7.3"
}
}
================================================
FILE: chapter9/danger-react-app/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter9/danger-react-app/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter9/danger-react-app/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter9/danger-react-app/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter9/danger-react-app/src/App.js
================================================
import React from 'react'
import logo from './logo.svg'
import './App.css'
function App() {
return (
)
}
export default App
================================================
FILE: chapter9/danger-react-app/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter9/danger-react-app/src/index.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'
import App from './App'
ReactDOM.render(
,
document.getElementById('root'),
)
================================================
FILE: chapter9/deploy/aws/cra/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter9/deploy/aws/cra/README.md
================================================
# react-deep-dive-example-cra
visit: https://main.drv3mroaqiuz2.amplifyapp.com/
================================================
FILE: chapter9/deploy/aws/cra/package.json
================================================
{
"name": "cra",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.3",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.8.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: chapter9/deploy/aws/cra/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter9/deploy/aws/cra/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter9/deploy/aws/cra/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter9/deploy/aws/cra/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter9/deploy/aws/cra/src/App.test.tsx
================================================
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render( );
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
================================================
FILE: chapter9/deploy/aws/cra/src/App.tsx
================================================
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
);
}
export default App;
================================================
FILE: chapter9/deploy/aws/cra/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter9/deploy/aws/cra/src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: chapter9/deploy/aws/cra/src/react-app-env.d.ts
================================================
///
================================================
FILE: chapter9/deploy/aws/cra/src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: chapter9/deploy/aws/cra/src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: chapter9/deploy/aws/cra/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
================================================
FILE: chapter9/deploy/aws/next/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: chapter9/deploy/aws/next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter9/deploy/aws/next/README.md
================================================
# react-deep-dive-example-next
visit: https://main.dtabop25r3q54.amplifyapp.com/
================================================
FILE: chapter9/deploy/aws/next/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
================================================
FILE: chapter9/deploy/aws/next/package.json
================================================
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^11.1.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/node": "18.11.8",
"@types/react": "18.0.1",
"@types/react-dom": "^17.0.18",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
}
================================================
FILE: chapter9/deploy/aws/next/pages/_app.tsx
================================================
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp
================================================
FILE: chapter9/deploy/aws/next/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: chapter9/deploy/aws/next/pages/index.tsx
================================================
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
Create Next App
Get started by editing{' '}
pages/index.tsx
)
}
export default Home
================================================
FILE: chapter9/deploy/aws/next/styles/Home.module.css
================================================
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}
================================================
FILE: chapter9/deploy/aws/next/styles/globals.css
================================================
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
================================================
FILE: chapter9/deploy/aws/next/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter9/deploy/digitalocean/cra/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter9/deploy/digitalocean/cra/README.md
================================================
# react-deep-dive-example-cra
visit: https://cra-gbqbi.ondigitalocean.app/
================================================
FILE: chapter9/deploy/digitalocean/cra/package.json
================================================
{
"name": "cra",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: chapter9/deploy/digitalocean/cra/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter9/deploy/digitalocean/cra/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter9/deploy/digitalocean/cra/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter9/deploy/digitalocean/cra/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter9/deploy/digitalocean/cra/src/App.js
================================================
import logo from './logo.svg';
import './App.css';
function App() {
return (
);
}
export default App;
================================================
FILE: chapter9/deploy/digitalocean/cra/src/App.test.js
================================================
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render( );
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
================================================
FILE: chapter9/deploy/digitalocean/cra/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter9/deploy/digitalocean/cra/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: chapter9/deploy/digitalocean/cra/src/reportWebVitals.js
================================================
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: chapter9/deploy/digitalocean/cra/src/setupTests.js
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: chapter9/deploy/digitalocean/next/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: chapter9/deploy/digitalocean/next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter9/deploy/digitalocean/next/README.md
================================================
# react-deep-dive-example-next
visit: https://next-g6zet.ondigitalocean.app/
================================================
FILE: chapter9/deploy/digitalocean/next/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
================================================
FILE: chapter9/deploy/digitalocean/next/package.json
================================================
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "13.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "18.11.7",
"@types/react": "18.0.24",
"@types/react-dom": "18.0.8",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
}
================================================
FILE: chapter9/deploy/digitalocean/next/pages/_app.tsx
================================================
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp
================================================
FILE: chapter9/deploy/digitalocean/next/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: chapter9/deploy/digitalocean/next/pages/index.tsx
================================================
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
Create Next App
Get started by editing{' '}
pages/index.tsx
)
}
export default Home
================================================
FILE: chapter9/deploy/digitalocean/next/styles/Home.module.css
================================================
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}
================================================
FILE: chapter9/deploy/digitalocean/next/styles/globals.css
================================================
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
================================================
FILE: chapter9/deploy/digitalocean/next/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter9/deploy/netlify/cra/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter9/deploy/netlify/cra/README.md
================================================
# react-deep-dive-example-cra
visit: https://create-react-app-yceffort.netlify.app/
================================================
FILE: chapter9/deploy/netlify/cra/package.json
================================================
{
"name": "cra",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.2",
"@types/react": "^18.0.23",
"@types/react-dom": "^18.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.8.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: chapter9/deploy/netlify/cra/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter9/deploy/netlify/cra/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter9/deploy/netlify/cra/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter9/deploy/netlify/cra/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter9/deploy/netlify/cra/src/App.test.tsx
================================================
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render( );
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
================================================
FILE: chapter9/deploy/netlify/cra/src/App.tsx
================================================
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
);
}
export default App;
================================================
FILE: chapter9/deploy/netlify/cra/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter9/deploy/netlify/cra/src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: chapter9/deploy/netlify/cra/src/react-app-env.d.ts
================================================
///
================================================
FILE: chapter9/deploy/netlify/cra/src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: chapter9/deploy/netlify/cra/src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: chapter9/deploy/netlify/cra/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
================================================
FILE: chapter9/deploy/netlify/next/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: chapter9/deploy/netlify/next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter9/deploy/netlify/next/README.md
================================================
# react-deep-dive-example-next
visit: https://create-next-app-yceffort.netlify.app/
================================================
FILE: chapter9/deploy/netlify/next/netlify.toml
================================================
[[plugins]]
package = "@netlify/plugin-nextjs"
================================================
FILE: chapter9/deploy/netlify/next/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
================================================
FILE: chapter9/deploy/netlify/next/package.json
================================================
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "13.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "18.11.7",
"@types/react": "18.0.23",
"@types/react-dom": "18.0.7",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
}
================================================
FILE: chapter9/deploy/netlify/next/pages/_app.tsx
================================================
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp
================================================
FILE: chapter9/deploy/netlify/next/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: chapter9/deploy/netlify/next/pages/hello.tsx
================================================
import { GetServerSideProps } from "next"
export default function Hello({hello}: {hello: string}) {
return <>Hello {hello}!>
}
export const getServerSideProps: GetServerSideProps = async () => {
return {
props: {
hello:'world'
}
}
}
================================================
FILE: chapter9/deploy/netlify/next/pages/index.tsx
================================================
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
Create Next App
Get started by editing{' '}
pages/index.tsx
)
}
export default Home
================================================
FILE: chapter9/deploy/netlify/next/styles/Home.module.css
================================================
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}
================================================
FILE: chapter9/deploy/netlify/next/styles/globals.css
================================================
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
================================================
FILE: chapter9/deploy/netlify/next/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter9/deploy/vercel/cra/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: chapter9/deploy/vercel/cra/README.md
================================================
# react-deep-dive-example-cra
visit: https://react-deep-dive-example-cra.vercel.app/
================================================
FILE: chapter9/deploy/vercel/cra/package.json
================================================
{
"name": "cra",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.1",
"@types/react": "^18.0.23",
"@types/react-dom": "^18.0.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.8.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: chapter9/deploy/vercel/cra/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: chapter9/deploy/vercel/cra/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: chapter9/deploy/vercel/cra/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: chapter9/deploy/vercel/cra/src/App.css
================================================
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: chapter9/deploy/vercel/cra/src/App.test.tsx
================================================
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render( );
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
================================================
FILE: chapter9/deploy/vercel/cra/src/App.tsx
================================================
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
);
}
export default App;
================================================
FILE: chapter9/deploy/vercel/cra/src/index.css
================================================
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
================================================
FILE: chapter9/deploy/vercel/cra/src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
================================================
FILE: chapter9/deploy/vercel/cra/src/react-app-env.d.ts
================================================
///
================================================
FILE: chapter9/deploy/vercel/cra/src/reportWebVitals.ts
================================================
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
================================================
FILE: chapter9/deploy/vercel/cra/src/setupTests.ts
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
================================================
FILE: chapter9/deploy/vercel/cra/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
================================================
FILE: chapter9/deploy/vercel/next/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: chapter9/deploy/vercel/next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter9/deploy/vercel/next/README.md
================================================
# react-deep-dive-example-next
visit: https://react-deep-dive-example-next.vercel.app/
================================================
FILE: chapter9/deploy/vercel/next/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
================================================
FILE: chapter9/deploy/vercel/next/package.json
================================================
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "13.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "18.11.6",
"@types/react": "18.0.23",
"@types/react-dom": "18.0.7",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
}
================================================
FILE: chapter9/deploy/vercel/next/pages/_app.tsx
================================================
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp
================================================
FILE: chapter9/deploy/vercel/next/pages/api/hello.ts
================================================
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
res.status(200).json({ name: 'John Doe' })
}
================================================
FILE: chapter9/deploy/vercel/next/pages/index.tsx
================================================
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
Create Next App
Get started by editing{' '}
pages/index.tsx
)
}
export default Home
================================================
FILE: chapter9/deploy/vercel/next/styles/Home.module.css
================================================
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}
@media (prefers-color-scheme: dark) {
.card,
.footer {
border-color: #222;
}
.code {
background: #111;
}
.logo img {
filter: invert(1);
}
}
================================================
FILE: chapter9/deploy/vercel/next/styles/globals.css
================================================
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
body {
color: white;
background: black;
}
}
================================================
FILE: chapter9/deploy/vercel/next/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: chapter9/zero-to-next/.eslintignore
================================================
.next/
node_modules/
================================================
FILE: chapter9/zero-to-next/.eslintrc.js
================================================
module.exports = {
extends: [
'next/core-web-vitals',
'@titicaca/eslint-config-triple',
'@titicaca/eslint-config-triple/frontend',
'@titicaca/eslint-config-triple/prettier',
],
}
================================================
FILE: chapter9/zero-to-next/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: chapter9/zero-to-next/.prettierignore
================================================
.next/
node_modules/
================================================
FILE: chapter9/zero-to-next/.prettierrc
================================================
"@titicaca/prettier-config-triple"
================================================
FILE: chapter9/zero-to-next/README.md
================================================
# 7장 예제 애플리케이션
7장의 예제 애플리케이션입니다.
================================================
FILE: chapter9/zero-to-next/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
poweredByHeader: false,
eslint: {
ignoreDuringBuilds: true,
},
}
module.exports = nextConfig
================================================
FILE: chapter9/zero-to-next/package.json
================================================
{
"name": "my-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "next dev",
"start": "next start",
"build": "next build",
"lint": "eslint . --fix",
"prettier": "prettier . --write"
},
"author": "yceffort",
"license": "ISC",
"dependencies": {
"next": "^12.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"styled-components": "^5.3.6"
},
"devDependencies": {
"@titicaca/eslint-config-triple": "^5.0.0",
"@titicaca/prettier-config-triple": "^1.0.2",
"@types/node": "^18.8.5",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.26",
"eslint": "^8.38.0",
"prettier": "^2.8.7",
"eslint-config-next": "^12.3.1",
"typescript": "^4.8.4"
}
}
================================================
FILE: chapter9/zero-to-next/src/_app.tsx
================================================
import { AppProps, NextWebVitalsMetric } from 'next/app'
export function reportWebVitals(metric: NextWebVitalsMetric) {
// eslint-disable-next-line no-console
console.log(metric)
}
function MyApp({ Component, pageProps }: AppProps) {
return
}
export default MyApp
================================================
FILE: chapter9/zero-to-next/src/components/common/title.tsx
================================================
import styled from 'styled-components'
export const Title = styled.h1`
font-size: 20px;
color: red;
`
================================================
FILE: chapter9/zero-to-next/src/components/todo/todo.tsx
================================================
import { useMemo } from 'react'
import Link from 'next/link'
import styled from 'styled-components'
import useToggle from '#hooks/useToggle'
import { Todo } from '#types/todo'
const Anchor = styled.a`
text-decoration: none;
`
export function TodoComponent({ todos }: { todos: Array }) {
return (
{todos.map((todo) => (
))}
)
}
export function TodoItem({ todo }: { todo: Todo }) {
const { id, userId, complete, title } = todo
const [completed, toggle] = useToggle(complete)
const toggleId = useMemo(() => {
return `toggle_${id}`
}, [id])
return (
{title}
{' '}
by {userId}
)
}
================================================
FILE: chapter9/zero-to-next/src/hooks/useToggle.ts
================================================
import { Reducer, useReducer } from 'react'
const toggleReducer = (value: boolean, nextValue?: unknown) =>
typeof nextValue === 'boolean' ? nextValue : !value
export default function useToggle(
initialValue = false,
): [boolean, (nextValue?: unknown) => void] {
return useReducer>(toggleReducer, initialValue)
}
================================================
FILE: chapter9/zero-to-next/src/pages/_document.tsx
================================================
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps,
} from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default function MyDocument() {
return (
)
}
MyDocument.getInitialProps = async (
ctx: DocumentContext,
): Promise => {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles( ),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
>
),
}
} finally {
sheet.seal()
}
}
================================================
FILE: chapter9/zero-to-next/src/pages/index.tsx
================================================
import { GetServerSideProps } from 'next'
import { Todo } from '#types/todo'
import { Title } from '#components/common/title'
import { TodoComponent } from '#components/todo/todo'
export default function Index({ todos }: { todos: Array }) {
return (
Hello NextJs!
)
}
export const getServerSideProps: GetServerSideProps = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const result = await response.json()
return {
props: {
todos: result,
},
}
}
================================================
FILE: chapter9/zero-to-next/src/pages/todos/[id].tsx
================================================
import { GetServerSideProps } from 'next'
import { Todo } from '#types/todo'
import withGetServerSideProps, { NotFoundError } from '#utils/errors'
export default function TodoItem({ todo }: { todo: Todo }) {
return Todo: {todo.title}
}
export const getServerSideProps: GetServerSideProps = withGetServerSideProps(
async (ctx) => {
const id = ctx.params?.id
if (!id) {
throw new NotFoundError(id)
}
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
)
if (response.status === 404) {
throw new NotFoundError(id)
}
return {
props: {
todo: await response.json(),
},
}
},
)
================================================
FILE: chapter9/zero-to-next/src/types/todo.ts
================================================
export interface Todo {
userId: number
id: number
title: string
complete: boolean
}
================================================
FILE: chapter9/zero-to-next/src/utils/errors/index.ts
================================================
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
export class NotFoundError extends Error {
public constructor(private resourceId: unknown) {
super()
}
public get message() {
return `${this.resourceId} not found`
}
}
export default function withGetServerSideProps(
getServerSideProps: GetServerSideProps,
): GetServerSideProps {
return async (context: GetServerSidePropsContext) => {
try {
return await getServerSideProps(context)
} catch (error) {
if (error instanceof NotFoundError) {
return {
notFound: true,
}
}
throw error
}
}
}
================================================
FILE: chapter9/zero-to-next/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": "src",
"paths": {
"#pages/*": ["pages/*"],
"#hooks/*": ["hooks/*"],
"#types/*": ["types/*"],
"#components/*": ["components/*"],
"#utils/*": ["utils/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}