Showing preview only (351K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
================================================
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 (
<ThemeContext.Consumer>
{theme => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div style={{border: '1px dashed black', padding: 20}} onClick={this.handleClick}>
<h3>src/legacy/Greeting.js</h3>
<h4 style={{color: theme}}>
This component is rendered by the nested React ({React.version}).
</h4>
<Clock />
</div>
)}
</ThemeContext.Consumer>
);
}
}
================================================
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(
<ThemeContext.Provider value={context.theme}>
<Component {...props} />
</ThemeContext.Provider>,
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 (
<>
<h2>src/modern/AboutPage.js</h2>
<h3 style={{color: theme}}>
This component is rendered by the outer React ({React.version}).
</h3>
<Clock />
<Greeting />
<br />
</>
);
}
================================================
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 (
<ThemeContext.Provider value={theme}>
<div style={{fontFamily: 'sans-serif'}}>
<div
style={{
margin: 20,
padding: 20,
border: '1px solid black',
minHeight: 300,
}}>
<button onClick={handleToggleClick}>Toggle Theme Context</button>
<br />
<Suspense fallback={<Spinner />}>
<AboutPage />
</Suspense>
</div>
</div>
</ThemeContext.Provider>
);
}
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 (
<>
<h2>src/modern/HomePage.js</h2>
<h3 style={{color: theme}}>
This component is rendered by the outer React ({React.version}).
</h3>
<Clock />
</>
);
}
================================================
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(
<StrictMode>
<App />
</StrictMode>,
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 <div style={{display: 'contents'}} ref={containerRef} />;
};
}
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 <p>Time: {time}</p>;
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
이름: {user.name}
</h1>
<Counter />
</div>
)
}
================================================
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 (
<CounterProvider>
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/context" items={items} />
</div>
<div>{children}</div>
</div>
</CounterProvider>
)
}
================================================
FILE: chapter11/next13/app/context/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Client Context</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
Context는 클라이언트 컴포넌트로, 기존에 리액트에서 사용하던 Context
문법을 그대로 사용하면 서버와 클라이언트 컴포넌트 모두에서 사용할 수
있다.
</li>
<li>
Context는 상태를 가지고 있어야 하므로 클라이언트 컴포넌트가 될 수
밖에 없으며, 반드시 파일 상단에 "use client"를 선언해주어야 한다.
</li>
<li>
Context.Provider로 하위 라우팅을 감싸주면, 라우팅 내부에서 이동이
발생하더라도 Context 내부의 값잉 유지되는 것을 볼 수 있다.
</li>
</ul>
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/error/[id]/page.tsx
================================================
import { notFound } from 'next/navigation'
import { ReactNode } from 'react'
export default function Page(): ReactNode | Promise<ReactNode> {
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 (
<div className="space-y-4">
<div className="text-sm text-vercel-pink">
<strong className="font-bold">Error:</strong> {error?.message}
</div>
<div>
<button
className="rounded-lg px-3 py-1 text-sm font-medium bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white"
onClick={() => reset()}
>
에러 리셋
</button>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/error" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<div className="flex justify-between gap-x-3">
<h1 className="text-xl font-medium text-gray-400/80">Error Handling</h1>
<ErrorButton />
</div>
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
`error`는 nextjs의 또 다른 예약어로, 해당 라우트 내부에서 사용가능한
에러 바운더리를 정의할 수 있는 파일이다.
</li>
<li>
에러 버튼을 눌러보자. 해당 라우트 내부의 레이아웃과 페이지에만 영향을
미치며, 여전히 다른 페이지는 상호작용이 가능하다.
</li>
<li>
`not-found`파일을 활용하면 해당 라우트 내부의 404 페이지를 정의할 수
있다.
</li>
<li>
유저 버튼을 누르면 /error/{'{id}'}로 이동하는데 이 페이지는
`notFound()`를 실행하여 404페이지로 보낸다.
</li>
<li>주의: `error`는 반드시 클라이언트 컴포넌트여야 한다.</li>
</ul>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup
path="/grouped-layouts"
items={[
{
text: 'Home',
},
{ text: 'users', slug: 'users' },
{ text: 'todos', slug: 'todos' },
{ text: 'hello', slug: 'hello' },
]}
/>
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(main)/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Route Groups</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
라우팅 그룹은 URL 구조에 영향을 주지 않으면서, 주소에 따라 서로다른
레이아웃을 적용할 수 있는 방법이다.
</li>
<li>
만약 대표 루트 URL 페이지가 선언되어 있지 않다면 `(main)`을 해당
페이지의 루트로 간주하여 라우팅한다.
</li>
<li>
서로 다른 탭을 네비게이션 해보면, 주소에는 영향이 없지만 주소에
따라서 서로 다른 레이아웃을 적용할 수 있다는 것을 알 수 있다.
</li>
</ul>
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(todos)/hello/page.tsx
================================================
export default async function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
url: /groped-layouts/hello
</h1>
<div className="space-y-4">안녕하세요.</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(todos)/layout.tsx
================================================
import { ReactNode } from 'react'
export default async function Layout({ children }: { children: ReactNode }) {
return (
<div className="space-y-9">
<h1>여기는 /groped-layout/(todos) 입니다.</h1>
<div>{children}</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
url: /groped-layouts/todos
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/grouped-layouts/(users)/layout.tsx
================================================
import { ReactNode } from 'react'
export default async function Layout({ children }: { children: ReactNode }) {
return (
<div className="space-y-9">
<h1>여기는 /groped-layout/(user) 입니다.</h1>
<div>{children}</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
url: /groped-layouts/users
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
</div>
)
}
================================================
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 (
<>
<DefaultHeader />
<title>{user.name}</title>
<meta name="description" content="head를 재정의해보았습니다." />
</>
)
}
================================================
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 (
<>
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
이름: {user.name}
</h1>
</div>
{/* @ts-expect-error Async Server Component */}
<SubPage params={{ userId: params.userId }} />
</>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
서브페이지 이름: {user.name}
</h1>
</div>
)
}
================================================
FILE: chapter11/next13/app/head/head.tsx
================================================
import DefaultHeader from '#components/DefaultHeader'
export default function Head() {
return (
<>
<DefaultHeader />
<title>라우트 내부에서 head를 재정의할 수 있습니다.</title>
<meta name="description" content="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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/head" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/head/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-6">
<h1 className="text-xl font-medium text-gray-400/80">
head 태그 설정하기
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
`head`를 활용하면 라우트 내부의 {'<head>'}를 원하는 대로 커스텀할 수
있다.
</li>
<li>
또한 `head`내부에서 데이터를 불러와서 동적으로 결정하는 것 또한
가능하다.
</li>
<li>
Next will dedupe requests for the same data across `layout.js`,
`page.js` and `head.js` when rendering a route.
</li>
<li>
nextjs는 `head` 내부에 데이터 요청이 있다면 이 요청이 완료되고{' '}
{'<head>'}가 렌더링이 완료될 때 까지 기다린다. 이는 첫번째 스트리밍
응답에 무조건 {'<head>'}가 포함될 수 있도록 보장해준다.
</li>
<li>
추가로 같은 라우트 내부에서 발생하는 같은 중복 api 요청에 대한
처리도 잘되어 있는 것을 확인할 수 있다. 이는 프로덕션 모드에서만
확인가능하며, {'/head/{id}'}페이지에서 같은 api를 3차례 부르지만
한번만 요청이 가는 것을 확인할 수 있다.
</li>
<li>
클라이언트에서의 중복처리도 원래 가능한 것으로 알려져 있지만, 현재
클라이언트 요청에 대한 중복처리는 아직 개발중인 것으로 보인다.
(2023년 1월 기준)
<a
className="inline-flex gap-x-2 rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium text-gray-100 hover:bg-gray-500 hover:text-white"
href="https://github.com/vercel/next.js/discussions/41745#discussioncomment-3986980"
>
관련 링크 보기
</a>
</li>
</ul>
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/head.tsx
================================================
import DefaultHeader from '../src/components/DefaultHeader'
export default function Head() {
return (
<>
<DefaultHeader />
<title>Next@13 예제</title>
<meta
name="description"
content="Nextjs@13을 기반으로 한 리액트 deep dive 예제입니다."
/>
</>
)
}
================================================
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 (
<div className="space-y-4">
<div className="self-start whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100">
마지막 렌더링 시간 (프로덕션 모드만 확인 가능): UTC{' '}
{new Date().toLocaleTimeString()}
</div>
<h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
<p className="font-medium text-gray-400">{data.body}</p>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup
path="/isr"
items={[
{
text: 'Home',
},
...ids.map((x) => ({
text: `Post ${x.id}`,
slug: x.id,
})),
]}
/>
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/isr/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Incremental Static Regeneration
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
이 예제에서는 과거 `getStaticProps`와 `revalidate`의 조합으로
제공하는 `incremental static regeneration`을 구현한 예제다.
</li>
<li>
이 하위 페이지들은 15초 간격으로 페이지를 재생성하며, 15초 이내에
방문한 사용자에 대해서는 기존에 캐싱된 페이지를 보여준다.
</li>
<li>
먼저 최초에 페이지를 방문하면 캐싱된 페이지를 보여준다. 그리고 만약
그 방문 시점이 revalidate시간, 즉 생성후 15초를 지났다면 새로
페이지를 다시 만들고, 그 이후에 방문한 사용자에게는 재생성한
페이지를 보여준다.
</li>
<li>우측 상단의 마지막으로 렌더링된 시간을 주목해서 살펴보자.</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<html lang="en">
<body className="overflow-y-scroll">
<SideBar />
<div className="lg:pl-72">
<div className="mx-auto max-w-4xl space-y-8 px-2 pt-20 lg:py-8 lg:px-8">
<div className="rounded-lg p-px shadow-lg">
<div className="rounded-lg p-3.5 lg:p-6">{children}</div>
</div>
</div>
</div>
</body>
</html>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
이름: {user.name}
</h1>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/layouts" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/layouts/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Layouts</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>레이아웃은 특정 주소 내부에 공유할 수 있는 UI 를 말한다.</li>
<li>
네비게이션이 발생하더라도 레이아웃은 그 상태를 유지하고, 다시
렌더링하지 않는다.
</li>
<li>레이아웃은 여러 페이지에 걸쳐 중첩하는 것 또한 가능하다.</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
이름: {user.name}
</h1>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/loading" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/loading/loading.tsx
================================================
export default function Loading() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Loading...</h1>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">로딩 중...</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/loading/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Loading</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
파일명 loading은 nextjs에서 사용하는 예약어로, 페이지가 아직 렌더링
준비가 되지 않았을 때 노출되는 컴포넌트다.
</li>
<li>
Streaming 예제의 Suspense와 다르게, 별도로 Suspense로 감싸지 않아도
하위 라우팅 내부에서 공통으로 사용할 수 있다는 장점이 있다.
</li>
<li>
유저를 클릭하면, 유저에 해당하는 컴포넌트가 렌더링 되기 전까지
loading.tsx가 잠깐 노출되는 것을 확인할 수 있다.
</li>
</ul>
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/page.tsx
================================================
import Link from 'next/link'
import { demos } from '../src/constant/menu'
export default function Page() {
return (
<div className="space-y-8">
<h1 className="text-xl font-medium text-gray-300">Examples</h1>
<div className="space-y-10 text-white">
{demos.map((section) => {
return (
<div key={section.name} className="space-y-5">
<div className="text-xs font-semibold uppercase tracking-wider text-gray-400">
{section.name}
</div>
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2">
{section.items.map((item) => {
return (
<Link
href={`/${item.slug}`}
key={item.name}
className="group block space-y-1.5 rounded-lg bg-gray-900 px-5 py-3 hover:bg-gray-800"
>
<div className="font-medium text-gray-200 group-hover:text-gray-50">
{item.name}
</div>
{item.description ? (
<div className="line-clamp-3 text-sm text-gray-400 group-hover:text-gray-300">
{item.description}
</div>
) : null}
</Link>
)
})}
</div>
</div>
)
})}
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/server-action/form/[id]/loading.tsx
================================================
export default function Loading() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Loading...</h1>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">로딩 중...</div>
</div>
)
}
================================================
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<Data>(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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">form with data</h1>
<h2 className="text-l font-medium text-gray-400/80">
서버에 저장된 정보: {data?.name} {data?.age}
</h2>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>
<form action={handleSubmit}>
<li>
<label htmlFor="name">이름: </label>
<input
type="text"
id="name"
name="name"
defaultValue={data?.name}
placeholder="이름을 입력해주세요."
/>
</li>
<li>
<label htmlFor="age">나이: </label>
<input
type="number"
id="age"
name="age"
defaultValue={data?.age}
placeholder="나이를 입력해주세요."
/>
</li>
<li>
<button type="submit">submit</button>
</li>
</form>
</ul>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">form</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>
<li>
<form action={handleSubmit}>
<button type="submit">form 요청 보내보기</button>
</form>
</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/server-action" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/server-action/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Server action (alpha)
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
서버 액션은 컴포넌트에서 직접 서버사이드 데이터 조작을 할 수 있게
해주는 nextjs의 새로운 기능이다.
</li>
<li>
13.4.0 기준으로 실험 기능이므로, `next.config.js`에서
`experimental.serverActions = true`로 설정해두어야 한다.
</li>
<li>서버 액션에서 할 수 있는 것들을 서브 메뉴로 확인해보자.</li>
</ul>
</div>
</div>
)
}
================================================
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<Data>(key)
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">form with data</h1>
<h2 className="text-l font-medium text-gray-400/80">
서버에 저장된 정보: {data?.name} {data?.age}
</h2>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>아래 버튼을 누르면 서버에서 직접 form 요청을 보냅니다.</li>
<li>이 작업은 useTransition을 기반으로 실행됩니다.</li>
<li>
<ClientButtonComponent id={params.id} />
</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
<p className="font-medium text-gray-400">{data.body}</p>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup
path="/ssg"
items={[
{
text: 'Home',
},
...ids.map((x) => ({
text: `Post ${x.id}`,
slug: x.id,
})),
]}
/>
<div className="self-start whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100">
마지막 렌더링 시간 (프로덕션 모드만 확인 가능)
{new Date().toLocaleTimeString()}
</div>
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/ssg/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Static-Site Generation
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
이 예제는 과거 `getStaticProps`와 `getStaticPaths`를 구현한 예제다.
`getStaticPaths`는 `generateStaticParams`으로 대체되었으며, 데이터를
불러오는 것은 `fetch`를 사용하는 것으로 동일하다. 최초 빌드시에 미리
데이터를 불러오고, 이후 재요청이 있으면 계속 해당 데이터를 사용한다.
</li>
<li>
미리 빌드된 페이지를 확인하고 싶다면, `./next/server/app/ssg`로
이동해서 확인해보면 된다. 미리 빌드된 html 파일이 준비되어 있을
것이다.
</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<div className="space-y-4">
<h1 className="text-2xl font-medium text-gray-100">{data.title}</h1>
<p className="font-medium text-gray-400">{data.body}</p>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/ssr" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/ssr/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Static-Site Generation
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
서버사이드 렌더링을 수행하면, HTML 페이지를 매 요청이 있을 때 마다
새로 만들게 된다. 서버에서는 HTML과 요청의 결과에 따른 JSON 데이터와
함께 클라이언트에 필요한 자바스크립트 파일이 전송된다.
</li>
<li>
`./next/server/app/ssr`를 확인해보면, `/ssg` 페이지와는 다르게 미리
빌드된 결과물 없이 항상 데이터를 `fetch`할 준비만 되어 있는 것을 볼
수 있다.
</li>
<li>
클라이언트에서는 이벤트 핸들러 등이 추가되지 않은 정적인 HTML을
받아서 페이지를 미리 보여주고, 리액트는 이 정적인 페이지에 JSON
데이터와 자바스크립트를 받아 컴포넌트를 상호작용 가능한 페이지로
만들어 준다. 이러한 일련의 과정을 hydration 이라고 한다.
</li>
</ul>
</div>
</div>
)
}
================================================
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 (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
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 (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
================================================
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 (
<div className="space-y-8 lg:space-y-14">
<Suspense fallback={<div>유저 목록을 로딩중입니다.</div>}>
{/* 타입스크립트에서 Promise 컴포넌트에 대해 에러를 내기 때문에 임시 처리 */}
{/* @ts-expect-error Async Server Component */}
<Users />
</Suspense>
<Suspense
fallback={<div>유저 {params.id}의 작성 글을 로딩중입니다.</div>}
>
{/* @ts-expect-error Async Server Component */}
<PostByUserId userId={params.id} />
</Suspense>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<div className="flex justify-between">
<TabGroup path="/streaming" items={items} />
</div>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/streaming/page.tsx
================================================
export default async function Page() {
return (
<div className="space-y-8">
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Streaming with Suspense
</h1>
<div className="space-y-4">
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>
스트리밍을 활용하면 서버에서 클라이언트로 UI 컴포넌트를 점진적으로
조금씩 보내는 것(스트리밍)이 가능해진다.
</li>
<li>
스트리밍을 활용하면 서버사이드렌더링과 다르게, 전체 페이지를 모두
보여줄 때 까지 기다리게 하는 것이 아니라 필요한 부분 부터 먼저
렌더링을 마치고 인터랙션할 수 있는 상태로 제공하는 것이
가능해진다.
</li>
<li>
위 유저 목록 중 하나를 누르면 유저 컴포넌트로 가는데, 이
컴포넌트는 각각 유저목록과 유저의 작성 글을 서로다른 Suspense
내부에서 불러온다. 이를 활용하면 `loading` 컴포넌트를 사용했을 때
보다 더 세밀하게 로딩을 보여줄 수 있다.
</li>
</ul>
</div>
</div>
<div className="grid grid-cols-4 gap-6">
{[1, 2, 3, 4, 5].map((id) => (
<div key={id} className="col-span-4 lg:col-span-1" />
))}
</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/styles/css-modules/page.tsx
================================================
import styles from './styles.module.css'
const SkeletonCard = () => (
<div className={styles.skeleton}>
<div className={styles['skeleton-img']} />
<div className={styles['skeleton-btn']} />
<div className={styles['skeleton-line-one']} />
<div className={styles['skeleton-line-two']} />
</div>
)
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Styled with CSS Modules
</h1>
<div className={styles.container}>
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</div>
)
}
================================================
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 = () => (
<div className="skeleton">
<div className="skeleton-img" />
<div className="skeleton-btn" />
<div className="skeleton-line-one" />
<div className="skeleton-line-two" />
</div>
)
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Styled with a Global CSS Stylesheet
</h1>
<div className="container">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</div>
)
}
================================================
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 (
<div className="space-y-9">
<TabGroup
path="/styles"
items={[
{
text: 'Home',
},
...items,
]}
/>
<div>{children}</div>
</div>
)
}
================================================
FILE: chapter11/next13/app/styles/page.tsx
================================================
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">Styling</h1>
<ul className="list-disc space-y-2 pl-4 text-sm text-gray-300">
<li>스타일을 적용하는 다양한 방법</li>
</ul>
</div>
)
}
================================================
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 <StyledComponentsRegistry>{children}</StyledComponentsRegistry>
}
================================================
FILE: chapter11/next13/app/styles/styled-components/page.tsx
================================================
import {
SkeletonInner,
SkeletonImg,
SkeletonBtn,
SkeletonLineOne,
SkeletonLineTwo,
Container,
} from '#components/components'
const Skeleton = () => (
<SkeletonInner>
<SkeletonImg />
<SkeletonBtn />
<SkeletonLineOne />
<SkeletonLineTwo />
</SkeletonInner>
)
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Styled Components (styled로 만들어진 컴포넌트는 반드시 클라이언트
컴포넌트 여야 합니다.)
</h1>
<Container>
<Skeleton />
<Skeleton />
<Skeleton />
</Container>
</div>
)
}
================================================
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 <StyleRegistry registry={jsxStyleRegistry}>{children}</StyleRegistry>
}
================================================
FILE: chapter11/next13/app/styles/styled-jsx/components.tsx
================================================
'use client'
export const SkeletonCard = () => (
<>
<div className="skeleton">
<div className="skeleton-img" />
<div className="skeleton-btn" />
<div className="skeleton-line-one" />
<div className="skeleton-line-two" />
</div>
{/* eslint-disable-next-line react/no-unknown-property */}
<style jsx>
{`
.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(0 112 243 / 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%;
}
`}
</style>
</>
)
================================================
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 <StyledJsxRegistry>{children}</StyledJsxRegistry>
}
================================================
FILE: chapter11/next13/app/styles/styled-jsx/page.tsx
================================================
'use client'
import { SkeletonCard } from './components'
export default function Page() {
return (
<div className="space-y-4">
<h1 className="text-xl font-medium text-gray-400/80">
Styled JSX ({'<style jsx>'}문법을 사용하기 위해서는 반드시 클라이언트
컴포넌트여야 합니다.)
</h1>
<div className="container">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
{/* eslint-disable-next-line react/no-unknown-property */}
<style jsx>
{`
.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));
}
}
`}
</style>
</div>
)
}
================================================
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 (
<button
onClick={handleClick}
className="rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100 hover:bg-gray-500 hover:text-white"
>
{count} Clicks
</button>
)
}
export default Counter
================================================
FILE: chapter11/next13/src/components/DefaultHeader.tsx
================================================
import { memo } from 'react'
function DefaultHeader() {
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="/favicon/apple-touch-icon.png"
rel="apple-touch-icon"
sizes="180x180"
/>
<link
href="/favicon/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
href="/favicon/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link href="/favicon/favicon.ico" rel="shortcut icon" />
</>
)
}
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 (
<button
className="rounded-lg px-3 py-1 text-sm font-medium bg-red-600 text-red-50 hover:bg-red-500 hover:text-white"
onClick={handleButtonClick}
>
에러 던지기!
</button>
)
}
================================================
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 (
<div className="fixed top-0 z-10 flex w-full flex-col border-b border-gray-800 bg-black lg:bottom-0 lg:z-auto lg:w-72 lg:border-r lg:border-gray-800">
<div className="flex h-14 items-center py-4 px-4 lg:h-auto">
<Link
href="/"
className="group flex w-full items-center gap-x-2.5"
onClick={handleClose}
>
<h3 className="font-semibold tracking-wide text-gray-400 group-hover:text-gray-50">
Next@13 App Directory 예제
</h3>
</Link>
</div>
<button
type="button"
className="group absolute right-0 top-0 flex h-14 items-center gap-x-2 px-4 lg:hidden"
onClick={handleButtonClick}
>
<div className="font-medium text-gray-100 group-hover:text-gray-400">
Menu
</div>
</button>
<div
className={clsx(
'overflow-y-auto lg:static lg:block',
open ? 'fixed inset-x-0 bottom-0 top-14 mt-px bg-black' : 'hidden',
)}
>
<nav className="space-y-6 px-2 py-5">
{demos.map((section) => {
return (
<div key={section.name}>
<div className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-gray-400/80">
<div>{section.name}</div>
</div>
<div className="space-y-1">
{section.items.map((item) => (
<GlobalNavItem
key={item.slug}
item={item}
close={handleClose}
/>
))}
</div>
</div>
)
})}
</nav>
</div>
</div>
)
}
function GlobalNavItem({
item,
close,
}: {
item: Item
close: () => false | void
}) {
const segment = useSelectedLayoutSegment()
const isActive = item.slug === segment
return (
<Link
onClick={close}
href={`/${item.slug}`}
className={clsx(
'block rounded-md px-3 py-2 text-sm font-medium hover:text-gray-300',
isActive ? 'text-white' : 'text-gray-400 hover:bg-gray-800',
)}
>
{item.name}
</Link>
)
}
================================================
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 (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
<>{children}</>
</StyleSheetManager>
)
}
================================================
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 (
<Link
href={href}
prefetch={false}
className={clsx(
'rounded-lg px-3 py-1 text-sm font-medium',
isActive
? 'bg-vercel-blue text-white'
: 'bg-gray-700 text-gray-100 hover:bg-gray-500 hover:text-white',
)}
>
{text}
</Link>
)
}
================================================
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 (
<div className="flex flex-wrap gap-2 items-center">
{items.map((item) => (
<Tab key={path + item.slug} item={item} path={path} />
))}
</div>
)
}
================================================
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 ? (
<SkeletonBtn />
) : (
<button onClick={handleClick}>기본값으로 돌리기</button>
)
}
================================================
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<SetStateAction<number>>] | undefined
>(undefined)
export function CounterProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0)
return (
<CounterContext.Provider value={[count, setCount]}>
{children}
</CounterContext.Provider>
)
}
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<Array<User>> {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const result: Array<User> = await response.json()
return result
}
export async function fetchUserById(id: string | number): Promise<User> {
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<Array<Todo>> {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const result: Array<Todo> = await response.json()
return result
}
interface Post {
userId: number
id: number
title: string
body: string
}
export async function fetchPosts(): Promise<Array<Post>> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
const result: Array<Post> = await response.json()
return result
}
export async function fetchPostById(
id: number | string,
options?: RequestInit,
): Promise<Post> {
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 <opensource-conduct@fb.com>. 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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="React with Server Components demo">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="style.css" />
<title>React Notes</title>
</head>
<body>
<div id="root"></div>
<script>
// In development, we restart the server on every edit.
// For the purposes of this demo, retry fetch automatically.
let nativeFetch = window.fetch;
window.fetch = async function fetchWithRetry(...args) {
for (let i = 0; i < 4; i++) {
try {
return await nativeFetch(...args);
} catch (e) {
if (args[1] && args[1].method !== 'GET') {
// Don't retry mutations to avoid confusion
throw e;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return nativeFetch(...args);
}
</script>
</body>
</html>
================================================
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 (
<div className="main">
<section className="col sidebar">
<section className="sidebar-header">
<img
className="logo"
src="logo.svg"
width="22px"
height="20px"
alt=""
role="presentation"
/>
<strong>React Notes</strong>
</section>
<section className="sidebar-menu" role="menubar">
<SearchField />
<EditButton noteId={null}>New</EditButton>
</section>
<nav>
<Suspense fallback={<NoteListSkeleton />}>
<NoteList searchText={searchText} />
</Suspense>
</nav>
</section>
<section key={selectedId} className="col note-viewer">
<Suspense fallback={<NoteSkeleton isEditing={isEditing} />}>
<Note selectedId={selectedId} isEditing={isEditing} />
</Suspense>
</section>
</div>
);
}
================================================
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 (
<button
className={[
'edit-button',
isDraft ? 'edit-button--solid' : 'edit-button--outline',
].join(' ')}
disabled={isPending}
onClick={() => {
startTransition(() => {
navigate({
selectedId: noteId,
isEditing: true,
});
});
}}
role="menuitem">
{children}
</button>
);
}
================================================
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 (
<NoteEditor noteId={null} initialTitle="Untitled" initialBody="" />
);
} else {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something! 🥺
</span>
</div>
);
}
}
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 <NoteEditor noteId={id} initialTitle={title} initialBody={body} />;
} else {
return (
<div className="note">
<div className="note-header">
<h1 className="note-title">{title}</h1>
<div className="note-menu" role="menubar">
<small className="note-updated-at" role="status">
Last updated on {format(updatedAt, "d MMM yyyy 'at' h:mm bb")}
</small>
<EditButton noteId={id}>Edit</EditButton>
</div>
</div>
<NotePreview body={body} />
</div>
);
}
}
================================================
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 (
<div className="note-editor">
<form
className="note-editor-form"
autoComplete="off"
onSubmit={(e) => e.preventDefault()}>
<label className="offscreen" htmlFor="note-title-input">
Enter a title for your note
</label>
<input
id="note-title-input"
type="text"
value={title}
onChange={(e) => {
setTitle(e.target.value);
}}
/>
<label className="offscreen" htmlFor="note-body-input">
Enter the body for your note
</label>
<textarea
id="note-body-input"
value={body}
onChange={(e) => {
setBody(e.target.value);
}}
/>
</form>
<div className="note-editor-preview">
<div className="note-editor-menu" role="menubar">
<button
className="note-editor-done"
disabled={isSaving || isNavigating}
onClick={() => handleSave()}
role="menuitem">
<img
src="checkmark.svg"
width="14px"
height="10px"
alt=""
role="presentation"
/>
Done
</button>
{!isDraft && (
<button
className="note-editor-delete"
disabled={isDeleting || isNavigating}
onClick={() => handleDelete()}
role="menuitem">
<img
src="cross.svg"
width="10px"
height="10px"
alt=""
role="presentation"
/>
Delete
</button>
)}
</div>
<div className="label label--preview" role="status">
Preview
</div>
<h1 className="note-title">{title}</h1>
<NotePreview title={title} body={body} />
</div>
</div>
);
}
================================================
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 ? (
<ul className="notes-list">
{notes.map((note) => (
<li key={note.id}>
<SidebarNote note={note} />
</li>
))}
</ul>
) : (
<div className="notes-empty">
{searchText
? `Couldn't find any notes titled "${searchText}".`
: 'No notes created yet!'}{' '}
</div>
);
}
================================================
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 (
<div>
<ul className="notes-list skeleton-container">
<li className="v-stack">
<div
className="sidebar-note-list-item skeleton"
style={{height: '5em'}}
/>
</li>
<li className="v-stack">
<div
className="sidebar-note-list-item skeleton"
style={{height: '5em'}}
/>
</li>
<li className="v-stack">
<div
className="sidebar-note-list-item skeleton"
style={{height: '5em'}}
/>
</li>
</ul>
</div>
);
}
================================================
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 (
<div className="note-preview">
<TextWithMarkdown text={body} />
</div>
);
}
================================================
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 ? <NoteEditorSkeleton /> : <NotePreviewSkeleton />;
}
function NoteEditorSkeleton() {
return (
<div
className="note-editor skeleton-container"
role="progressbar"
aria-busy="true">
<div className="note-editor-form">
<div className="skeleton v-stack" style={{height: '3rem'}} />
<div className="skeleton v-stack" style={{height: '100%'}} />
</div>
<div className="note-editor-preview">
<div className="note-editor-menu">
<div
className="skeleton skeleton--button"
style={{width: '8em', height: '2.5em'}}
/>
<div
className="skeleton skeleton--button"
style={{width: '8em', height: '2.5em', marginInline: '12px 0'}}
/>
</div>
<div
className="note-title skeleton"
style={{height: '3rem', width: '65%', marginInline: '12px 1em'}}
/>
<div className="note-preview">
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
</div>
</div>
</div>
);
}
function NotePreviewSkeleton() {
return (
<div
className="note skeleton-container"
role="progressbar"
aria-busy="true">
<div className="note-header">
<div
className="note-title skeleton"
style={{height: '3rem', width: '65%', marginInline: '12px 1em'}}
/>
<div
className="skeleton skeleton--button"
style={{width: '8em', height: '2.5em'}}
/>
</div>
<div className="note-preview">
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
<div className="skeleton v-stack" style={{height: '1.5em'}} />
</div>
</div>
);
}
================================================
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 (
<form className="search" role="search" onSubmit={(e) => e.preventDefault()}>
<label className="offscreen" htmlFor="sidebar-search-input">
Search for a note by title
</label>
<input
id="sidebar-search-input"
placeholder="Search"
value={text}
onChange={(e) => {
const newText = e.target.value;
setText(newText);
startSearching(() => {
navigate({
searchText: newText,
});
});
}}
/>
<Spinner active={isSearching} />
</form>
);
}
================================================
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 (
<SidebarNoteContent
id={note.id}
title={note.title}
expandedChildren={
<p className="sidebar-note-excerpt">{summary || <i>(No content)</i>}</p>
}>
<header className="sidebar-note-header">
<strong>{note.title}</strong>
<small>{lastUpdatedAt}</small>
</header>
</SidebarNoteContent>
);
}
================================================
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 (
<div
ref={itemRef}
onAnimationEnd={() => {
itemRef.current.classList.remove('flash');
}}
className={[
'sidebar-note-list-item',
isExpanded ? 'note-expanded' : '',
].join(' ')}>
{children}
<button
className="sidebar-note-open"
style={{
backgroundColor: isPending
? 'var(--gray-80)'
: isActive
? 'var(--tertiary-blue)'
: '',
border: isActive
? '1px solid var(--primary-border)'
: '1px solid transparent',
}}
onClick={() => {
startTransition(() => {
navigate({
selectedId: id,
isEditing: false,
});
});
}}>
Open note for preview
</button>
<button
className="sidebar-note-toggle-expand"
onClick={(e) => {
e.stopPropagation();
setIsExpanded(!isExpanded);
}}>
{isExpanded ? (
<img
src="chevron-down.svg"
width="10px"
height="10px"
alt="Collapse"
/>
) : (
<img src="chevron-up.svg" width="10px" height="10px" alt="Expand" />
)}
</button>
{isExpanded && expandedChildren}
</div>
);
}
================================================
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 (
<div
className={['spinner', active && 'spinner--active'].join(' ')}
role="progressbar"
aria-busy={active ? 'true' : 'false'}
/>
);
}
================================================
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 (
<div
className="text-with-markdown"
dangerouslySetInnerHTML={{
__html: sanitizeHtml(marked(text), {
allowedTags,
allowedAttributes,
}),
}}
/>
);
}
================================================
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(<Root />);
function Root() {
return (
<ErrorBoundary FallbackComponent={Error}>
<Router />
</ErrorBoundary>
);
}
function Error({error}) {
return (
<div>
<h1>Application Error</h1>
<pre style={{whiteSpace: 'pre-wrap'}}>{error.stack}</pre>
</div>
);
}
================================================
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 (
<RouterContext.Provider value={{location, navigate, refresh}}>
{use(content)}
</RouterContext.Provider>
);
}
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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
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 (
<div>
<h1>Chapter2 예제 코드</h1>
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
marginBottom: '1rem',
}}
>
<Link to="/1">예제 2-1</Link> | <Link to="/4">예제 2-4</Link> |
<Link to="/5">예제 2-5</Link> | <Link to="/7">예제 2-7</Link> |{' '}
<Link to="/8">예제 2-8</Link> |
</nav>
<Outlet />
</div>
)
}
================================================
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(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route path="/1" element={<Component1 />} />
<Route path="/4" element={<SampleComponent required text="hello" />} />
<Route path="/5" element={<SampleComponent2 />} />
<Route path="/7" element={<CompareComponent />} />
<Route path="/8" element={<ReactPureComponent />} />
</Route>
</Routes>
</BrowserRouter>,
)
================================================
FILE: chapter2/react/src/react-app-env.d.ts
================================================
/// <reference types="react-scripts" />
================================================
FILE: chapter2/react/src/routes/2-1.tsx
================================================
import { ReactNode } from 'react'
function A({
children,
required,
}: {
required?: boolean
children?: ReactNode
}) {
return (
<>
<input type="text" required={required} />
<div>{children}</div>
</>
)
}
function B({
text,
optionalChildren,
}: {
text?: string
optionalChildren?: ReactNode
}) {
return (
<>
<h1>{text}</h1>
{optionalChildren}
</>
)
}
// 하나의 요소로 구성된 가장 단순한 형태
const ComponentA = <A>안녕하세요.</A>
// 자식이 없이 SelfClosingTag로 닫혀있는 형태도 가능하다.
const ComponentB = <A />
// 옵션을 { } 와 전개 연산자로 넣을 수 있다.
const ComponentC = <A {...{ required: true }} />
// 옵션명만 넣어도 가능하다.
const ComponentD = <A required />
// 옵션명과 속성을 넣을 수 있다.
const ComponentE = <A required={false} />
const ComponentF = (
<A>
{/* 문자열은 쌍따옴표및 홀따옴표 모두 가능하다. */}
<B text="리액트" />
</A>
)
const ComponentG = (
<A>
{/* 옵선의 값으로 JSXElement를 넣는 것 또한 올바른 문법이다. */}
<B optionalChildren={<>안녕하세요.</>} />
</A>
)
const ComponentH = (
<A>
{/* 여러개의 자식도 포함할 수 있다. */}
안녕하세요
<B text="리액트" />
</A>
)
export function Component1() {
return (
<>
<div>{ComponentA}</div>
<div>{ComponentB}</div>
<div>{ComponentC}</div>
<div>{ComponentD}</div>
<div>{ComponentE}</div>
<div>{ComponentF}</div>
<div>{ComponentG}</div>
<div>{ComponentH}</div>
</>
)
}
================================================
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<SampleProps, SampleState> {
// 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 (
<h2>
Sample Component
<div>{required ? '필수' : '필수아님'}</div>
<div>문자: {text}</div>
<div>count: {count}</div>
<button onClick={this.handleClick} disabled={isLimited}>
증가
</button>
</h2>
)
}
}
export default SampleComponent
================================================
FILE: chapter2/react/src/routes/2-5.tsx
================================================
import { Component } from 'react'
type Props = Record<string, never>
interface State {
count: number
}
class SampleComponent extends Component<Props, State> {
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 (
<div>
<button onClick={this.handleClick}>증가</button>
{count}
</div>
)
}
}
export default SampleComponent
================================================
FILE: chapter2/react/src/routes/2-7.tsx
================================================
import React from 'react'
interface State {
count: number
}
type Props = Record<string, never>
export class ReactComponent extends React.Component<Props, State> {
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 (
<h1>
ReactComponent: {this.state.count}{' '}
<button onClick={this.handleClick}>+</button>
</h1>
)
}
}
export class ReactPureComponent extends React.PureComponent<Props, State> {
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 (
<h1>
ReactPureComponent: {this.state.count}{' '}
<button onClick={this.handleClick}>+</button>
</h1>
)
}
}
export default function CompareComponent() {
return (
<>
<h2>React.Component</h2>
<ReactComponent />
<h2>React.PureComponent</h2>
<ReactPureComponent />
</>
)
}
================================================
FILE: chapter2/react/src/routes/2-8.tsx
================================================
import React from 'react'
interface State {
count: number
}
type Props = Record<string, never>
export class ReactPureComponent extends React.PureComponent<Props, State> {
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 (
<>
<h1>
ReactPureComponent: {this.state.count}{' '}
<button onClick={this.handleClick}>+</button>
</h1>
<span>
위 빌드 결과가 알고 싶다면, 빌드를 수행한 뒤에 번들링 결과물에서
console.log 내용을 검색해보면 된다. 코드가 읽기 어렵다면 전체코드를
<a href="https://beautifier.io/">여기</a> 에서 보기 좋게 포맷팅해보자.
</span>
</>
)
}
}
================================================
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 (
<h1>
페이지를 찾을 수 없습니다. <button onClick={handleClick}>클릭</button>
</h1>
)
}
================================================
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 (
<h1>
(500페이지) 서버에서 에러가 발생했습니다.{' '}
<button onClick={handleClick}>클릭</button>
</h1>
)
}
================================================
FILE: chapter4/next-example/src/pages/_app.tsx
================================================
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
================================================
FILE: chapter4/next-example/src/pages/_document.tsx
================================================
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
================================================
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<Data>,
) {
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 <div>hello {greeting}</div>
}
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{' '}
<ul>
{serverProps.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
)
}
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 (
<ul>
<li>
{/* next의 eslint 룰을 잠시 끄기 위해 추가했다. */}
{/* eslint-disable-next-line */}
<a href="/hello">A 태그로 이동</a>
</li>
<li>
{/* 차이를 극적으로 보여주기 위해 해당 페이지의 리소스를 미리 가져오는 prefetch를 잠시 꺼두었다. */}
<Link prefetch={false} href="/hello">
next/link로 이동
</Link>
</li>
</ul>
)
}
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 (
<>
<h1>{todo.title}</h1>
<ul>
<li>
<Link href="/todo/1">1번</Link>
</li>
<li>
<Link href="/todo/2">2번</Link>
</li>
<li>
<Link href="/todo/3">3번</Link>
</li>
</ul>
</>
)
}
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 <yceffort@gmail.com>",
"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
================================================
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<script src="/browser.js"></script>
</body>
</html>
================================================
FILE: chapter4/ssr-example/public/index-front.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR Example</title>
</head>
<body>
================================================
FILE: chapter4/ssr-example/public/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR Example</title>
</head>
<body>
__placeholder__
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<script src="/browser.js"></script>
</body>
</html>
================================================
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<TodoResponse> }) {
useEffect(() => {
console.log('하이!') // eslint-disable-line no-console
}, [])
return (
<>
<h1>나의 할일!</h1>
<ul>
{todos.map((todo, index) => (
<Todo key={index} todo={todo} />
))}
</ul>
</>
)
}
================================================
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 (
<li>
<span>
{userId}-{id}) {title} {finished ? '완료' : '미완료'}
<button onClick={handleClick}>토글</button>
</span>
</li>
)
}
================================================
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 = <App todos={result} />
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
==
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
SYMBOL INDEX (222 symbols across 153 files)
FILE: chapter10/react-gradual-demo/src/legacy/Greeting.js
class AboutSection (line 7) | class AboutSection extends Component {
method componentDidMount (line 9) | componentDidMount() {
method render (line 21) | render() {
FILE: chapter10/react-gradual-demo/src/legacy/createLegacyRoot.js
function createLegacyRoot (line 6) | function createLegacyRoot(container) {
FILE: chapter10/react-gradual-demo/src/modern/AboutPage.js
function AboutPage (line 9) | function AboutPage() {
FILE: chapter10/react-gradual-demo/src/modern/App.js
function App (line 6) | function App() {
function Spinner (line 38) | function Spinner() {
FILE: chapter10/react-gradual-demo/src/modern/HomePage.js
function HomePage (line 6) | function HomePage() {
FILE: chapter10/react-gradual-demo/src/modern/lazyLegacyRoot.js
function lazyLegacyRoot (line 11) | function lazyLegacyRoot(getLegacyComponent) {
function readModule (line 62) | function readModule(record, importStatement) {
FILE: chapter10/react-gradual-demo/src/shared/Clock.js
function Clock (line 12) | function Clock() {
FILE: chapter10/react-gradual-demo/src/shared/useTime.js
function useTimer (line 10) | function useTimer() {
FILE: chapter11/next13/app/api/hello/route.ts
function GET (line 1) | async function GET() {
FILE: chapter11/next13/app/api/posts/[id]/route.ts
function GET (line 3) | async function GET(
FILE: chapter11/next13/app/api/posts/route.ts
function GET (line 3) | async function GET(request: NextRequest) {
FILE: chapter11/next13/app/api/users/[id]/route.ts
function GET (line 3) | async function GET(
FILE: chapter11/next13/app/api/users/route.ts
function GET (line 1) | async function GET() {
FILE: chapter11/next13/app/context/[id]/page.tsx
function Page (line 4) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/context/layout.tsx
function Layout (line 6) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/context/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/error/[id]/page.tsx
function Page (line 4) | function Page(): ReactNode | Promise<ReactNode> {
FILE: chapter11/next13/app/error/error.tsx
function Error (line 6) | function Error({ error, reset }: any) {
FILE: chapter11/next13/app/error/layout.tsx
function Layout (line 5) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/error/not-found.tsx
function NotFound (line 1) | function NotFound() {
FILE: chapter11/next13/app/error/page.tsx
function Page (line 3) | function Page() {
FILE: chapter11/next13/app/grouped-layouts/(main)/layout.tsx
function Layout (line 4) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/grouped-layouts/(main)/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/grouped-layouts/(todos)/hello/page.tsx
function Page (line 1) | async function Page() {
FILE: chapter11/next13/app/grouped-layouts/(todos)/layout.tsx
function Layout (line 3) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/grouped-layouts/(todos)/todos/page.tsx
function Page (line 3) | async function Page() {
FILE: chapter11/next13/app/grouped-layouts/(users)/layout.tsx
function Layout (line 3) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/grouped-layouts/(users)/users/page.tsx
function Page (line 3) | async function Page() {
FILE: chapter11/next13/app/head.tsx
function Head (line 3) | function Head() {
FILE: chapter11/next13/app/head/[userId]/head.tsx
function Head (line 4) | async function Head({ params }: { params: { userId: string } }) {
FILE: chapter11/next13/app/head/[userId]/page.tsx
function Page (line 4) | async function Page({ params }: { params: { userId: string } }) {
FILE: chapter11/next13/app/head/[userId]/sub.tsx
function SubPage (line 3) | async function SubPage({
FILE: chapter11/next13/app/head/head.tsx
function Head (line 3) | function Head() {
FILE: chapter11/next13/app/head/layout.tsx
function Layout (line 5) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/head/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/internal-api/hello/route.ts
function GET (line 3) | async function GET(request: NextRequest) {
FILE: chapter11/next13/app/isr/[id]/page.tsx
function generateStaticParams (line 7) | async function generateStaticParams() {
function Page (line 11) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/isr/layout.tsx
function Layout (line 6) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/isr/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/layout.tsx
function Layout (line 5) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/layouts/[userId]/page.tsx
function Page (line 3) | async function Page({ params }: { params: { userId: string } }) {
FILE: chapter11/next13/app/layouts/layout.tsx
function Layout (line 5) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/layouts/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/loading/[userId]/page.tsx
function Page (line 4) | async function Page({ params }: { params: { userId: string } }) {
FILE: chapter11/next13/app/loading/layout.tsx
function Layout (line 6) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/loading/loading.tsx
function Loading (line 1) | function Loading() {
FILE: chapter11/next13/app/loading/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/page.tsx
function Page (line 4) | function Page() {
FILE: chapter11/next13/app/server-action/form/[id]/loading.tsx
function Loading (line 1) | function Loading() {
FILE: chapter11/next13/app/server-action/form/[id]/page.tsx
type Data (line 4) | interface Data {
function Page (line 9) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/server-action/form/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/server-action/layout.tsx
function Layout (line 5) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/server-action/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/server-action/start-transition/[id]/page.tsx
type Data (line 4) | interface Data {
function Page (line 9) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/ssg/[id]/page.tsx
function generateStaticParams (line 3) | async function generateStaticParams() {
function Page (line 7) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/ssg/layout.tsx
function Layout (line 7) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/ssg/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/ssr/[id]/page.tsx
function Page (line 3) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/ssr/layout.tsx
function Layout (line 6) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/ssr/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/streaming/[id]/components.tsx
function Users (line 4) | async function Users() {
function PostByUserId (line 18) | async function PostByUserId({ userId }: { userId: string }) {
FILE: chapter11/next13/app/streaming/[id]/page.tsx
function Page (line 5) | async function Page({ params }: { params: { id: string } }) {
FILE: chapter11/next13/app/streaming/layout.tsx
function Layout (line 6) | async function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/streaming/page.tsx
function Page (line 1) | async function Page() {
FILE: chapter11/next13/app/styles/css-modules/page.tsx
function Page (line 12) | function Page() {
FILE: chapter11/next13/app/styles/global-css/page.tsx
function Page (line 12) | function Page() {
FILE: chapter11/next13/app/styles/layout.tsx
function Layout (line 24) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/styles/page.tsx
function Page (line 1) | function Page() {
FILE: chapter11/next13/app/styles/styled-components/layout.tsx
function Layout (line 5) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/styles/styled-components/page.tsx
function Page (line 19) | function Page() {
FILE: chapter11/next13/app/styles/styled-jsx/StyledRegistry.tsx
function StyledJsxRegistry (line 7) | function StyledJsxRegistry({
FILE: chapter11/next13/app/styles/styled-jsx/layout.tsx
function Layout (line 5) | function Layout({ children }: { children: ReactNode }) {
FILE: chapter11/next13/app/styles/styled-jsx/page.tsx
function Page (line 5) | function Page() {
FILE: chapter11/next13/middleware.ts
function middleware (line 3) | function middleware(request: NextRequest) {
FILE: chapter11/next13/src/components/DefaultHeader.tsx
function DefaultHeader (line 3) | function DefaultHeader() {
FILE: chapter11/next13/src/components/ErrorButton.tsx
function ErrorButton (line 5) | function ErrorButton() {
FILE: chapter11/next13/src/components/Sidebar.tsx
function SideBar (line 10) | function SideBar() {
function GlobalNavItem (line 70) | function GlobalNavItem({
FILE: chapter11/next13/src/components/StyledComponentsRegistry.tsx
function StyledComponentsRegistry (line 7) | function StyledComponentsRegistry({
FILE: chapter11/next13/src/components/TabGroup.tsx
type Item (line 3) | interface Item {
FILE: chapter11/next13/src/components/server-action/client-component.tsx
function ClientButtonComponent (line 6) | function ClientButtonComponent({ id }: { id: string }) {
FILE: chapter11/next13/src/constant/menu.ts
type Item (line 1) | interface Item {
FILE: chapter11/next13/src/context/counter.tsx
function CounterProvider (line 16) | function CounterProvider({ children }: { children: ReactNode }) {
function useCounter (line 26) | function useCounter() {
FILE: chapter11/next13/src/lib/utils.ts
function sleep (line 1) | async function sleep(ms: number) {
FILE: chapter11/next13/src/server-action/index.ts
function updateData (line 7) | async function updateData(
FILE: chapter11/next13/src/services/constant.ts
constant API_URL_BASE (line 1) | const API_URL_BASE = process.env.VERCEL_URL
FILE: chapter11/next13/src/services/server.ts
type User (line 1) | interface User {
function fetchUsers (line 24) | async function fetchUsers(): Promise<Array<User>> {
function fetchUserById (line 31) | async function fetchUserById(id: string | number): Promise<User> {
type Todo (line 40) | interface Todo {
function fetchTodos (line 47) | async function fetchTodos(): Promise<Array<Todo>> {
type Post (line 54) | interface Post {
function fetchPosts (line 61) | async function fetchPosts(): Promise<Array<Post>> {
function fetchPostById (line 68) | async function fetchPostById(
FILE: chapter11/server-components-demo/scripts/seed.js
constant NOTES_PATH (line 18) | const NOTES_PATH = './notes';
function randomDateBetween (line 24) | function randomDateBetween(start, end) {
function seed (line 64) | async function seed() {
FILE: chapter11/server-components-demo/server/api.server.js
constant PORT (line 34) | const PORT = process.env.PORT || 4000;
function handleErrors (line 64) | function handleErrors(fn) {
function renderReactTree (line 89) | async function renderReactTree(res, props) {
function sendResponse (line 103) | function sendResponse(req, res, redirectToId) {
constant NOTES_PATH (line 120) | const NOTES_PATH = path.resolve(__dirname, '../notes');
function waitForWebpack (line 194) | async function waitForWebpack() {
FILE: chapter11/server-components-demo/src/App.js
function App (line 18) | function App({selectedId, isEditing, searchText}) {
FILE: chapter11/server-components-demo/src/EditButton.js
function EditButton (line 14) | function EditButton({noteId, children}) {
FILE: chapter11/server-components-demo/src/Note.js
function Note (line 19) | async function Note({selectedId, isEditing}) {
FILE: chapter11/server-components-demo/src/NoteEditor.js
function NoteEditor (line 16) | function NoteEditor({noteId, initialTitle, initialBody}) {
FILE: chapter11/server-components-demo/src/NoteList.js
function NoteList (line 12) | async function NoteList({searchText}) {
FILE: chapter11/server-components-demo/src/NoteListSkeleton.js
function NoteListSkeleton (line 9) | function NoteListSkeleton() {
FILE: chapter11/server-components-demo/src/NotePreview.js
function NotePreview (line 11) | function NotePreview({body}) {
FILE: chapter11/server-components-demo/src/NoteSkeleton.js
function NoteSkeleton (line 9) | function NoteSkeleton({isEditing}) {
function NoteEditorSkeleton (line 13) | function NoteEditorSkeleton() {
function NotePreviewSkeleton (line 50) | function NotePreviewSkeleton() {
FILE: chapter11/server-components-demo/src/SearchField.js
function SearchField (line 16) | function SearchField() {
FILE: chapter11/server-components-demo/src/SidebarNote.js
function SidebarNote (line 15) | function SidebarNote({note}) {
FILE: chapter11/server-components-demo/src/SidebarNoteContent.js
function SidebarNoteContent (line 14) | function SidebarNoteContent({id, title, children, expandedChildren}) {
FILE: chapter11/server-components-demo/src/Spinner.js
function Spinner (line 9) | function Spinner({active = true}) {
FILE: chapter11/server-components-demo/src/TextWithMarkdown.js
function TextWithMarkdown (line 26) | function TextWithMarkdown({text}) {
FILE: chapter11/server-components-demo/src/framework/bootstrap.js
function Root (line 20) | function Root() {
function Error (line 28) | function Error({error}) {
FILE: chapter11/server-components-demo/src/framework/router.js
function Router (line 27) | function Router() {
function useRouter (line 74) | function useRouter() {
function useMutation (line 78) | function useMutation({endpoint, method}) {
FILE: chapter2/react/src/App.tsx
function App (line 3) | function App() {
FILE: chapter2/react/src/routes/2-1.tsx
function A (line 3) | function A({
function B (line 18) | function B({
function Component1 (line 70) | function Component1() {
FILE: chapter2/react/src/routes/2-4.tsx
type SampleProps (line 4) | interface SampleProps {
type SampleState (line 10) | interface SampleState {
class SampleComponent (line 16) | class SampleComponent extends React.Component<SampleProps, SampleState> {
method constructor (line 18) | private constructor(props: SampleProps) {
method render (line 33) | public render() {
FILE: chapter2/react/src/routes/2-5.tsx
type Props (line 3) | type Props = Record<string, never>
type State (line 5) | interface State {
class SampleComponent (line 9) | class SampleComponent extends Component<Props, State> {
method constructor (line 10) | private constructor(props: Props) {
method handleClick (line 19) | private handleClick() {
method render (line 23) | public render() {
FILE: chapter2/react/src/routes/2-7.tsx
type State (line 3) | interface State {
type Props (line 7) | type Props = Record<string, never>
class ReactComponent (line 9) | class ReactComponent extends React.Component<Props, State> {
method constructor (line 12) | private constructor(props: Props) {
method render (line 23) | public render() {
class ReactPureComponent (line 34) | class ReactPureComponent extends React.PureComponent<Props, State> {
method constructor (line 37) | private constructor(props: Props) {
method render (line 48) | public render() {
function CompareComponent (line 59) | function CompareComponent() {
FILE: chapter2/react/src/routes/2-8.tsx
type State (line 3) | interface State {
type Props (line 7) | type Props = Record<string, never>
class ReactPureComponent (line 9) | class ReactPureComponent extends React.PureComponent<Props, State> {
method constructor (line 10) | private constructor(props: Props) {
method render (line 27) | public render() {
FILE: chapter4/next-example/src/pages/404.tsx
function My404Page (line 3) | function My404Page() {
FILE: chapter4/next-example/src/pages/500.tsx
function My500Page (line 3) | function My500Page() {
FILE: chapter4/next-example/src/pages/_app.tsx
function App (line 3) | function App({ Component, pageProps }: AppProps) {
FILE: chapter4/next-example/src/pages/_document.tsx
function Document (line 3) | function Document() {
FILE: chapter4/next-example/src/pages/_error.tsx
function Error (line 3) | function Error({ statusCode }: { statusCode: number }) {
FILE: chapter4/next-example/src/pages/api/hello.ts
type Data (line 4) | interface Data {
function handler (line 8) | function handler(
FILE: chapter4/next-example/src/pages/hello.tsx
function Hello (line 1) | function Hello() {
FILE: chapter4/next-example/src/pages/hello/[greeting].tsx
function HelloGreeting (line 3) | function HelloGreeting({ greeting }: { greeting: string }) {
FILE: chapter4/next-example/src/pages/hello/world.tsx
function HelloWorld (line 1) | function HelloWorld() {
FILE: chapter4/next-example/src/pages/hi/[...props].tsx
function HiAll (line 5) | function HiAll({ props: serverProps }: { props: string[] }) {
FILE: chapter4/next-example/src/pages/todo/[id].tsx
function Todo (line 4) | function Todo({
FILE: chapter4/ssr-example/src/components/App.tsx
function App (line 7) | function App({ todos }: { todos: Array<TodoResponse> }) {
FILE: chapter4/ssr-example/src/components/Todo.tsx
function Todo (line 5) | function Todo({ todo }: { todo: TodoResponse }) {
FILE: chapter4/ssr-example/src/fetch/index.ts
type TodoResponse (line 3) | interface TodoResponse {
function fetchTodo (line 10) | async function fetchTodo() {
FILE: chapter4/ssr-example/src/index.tsx
function main (line 7) | async function main() {
FILE: chapter4/ssr-example/src/server.ts
constant PORT (line 14) | const PORT = process.env.PORT || 3000
function serverHandler (line 16) | async function serverHandler(req: IncomingMessage, res: ServerResponse) {
function main (line 77) | function main() {
FILE: chapter8/react-test/src/App.tsx
function App (line 5) | function App() {
FILE: chapter8/react-test/src/components/FetchComponent/index.test.tsx
constant MOCK_TODO_RESPONSE (line 7) | const MOCK_TODO_RESPONSE = {
FILE: chapter8/react-test/src/components/FetchComponent/index.tsx
type TodoResponse (line 3) | interface TodoResponse {
function FetchComponent (line 10) | function FetchComponent() {
FILE: chapter8/react-test/src/components/StateComponent/index.tsx
function InputComponent (line 3) | function InputComponent() {
FILE: chapter8/react-test/src/components/StaticComponent/index.tsx
function StaticComponent (line 23) | function StaticComponent() {
FILE: chapter8/react-test/src/hooks/useEffectDebugger.ts
type Props (line 3) | type Props = Record<string, unknown>
constant CONSOLE_PREFIX (line 5) | const CONSOLE_PREFIX = '[useEffectDebugger]'
function useEffectDebugger (line 7) | function useEffectDebugger(
FILE: chapter9/danger-react-app/src/App.js
function App (line 6) | function App() {
FILE: chapter9/deploy/aws/cra/src/App.tsx
function App (line 5) | function App() {
FILE: chapter9/deploy/aws/next/pages/_app.tsx
function MyApp (line 4) | function MyApp({ Component, pageProps }: AppProps) {
FILE: chapter9/deploy/aws/next/pages/api/hello.ts
type Data (line 4) | type Data = {
function handler (line 8) | function handler(
FILE: chapter9/deploy/digitalocean/cra/src/App.js
function App (line 4) | function App() {
FILE: chapter9/deploy/digitalocean/next/pages/_app.tsx
function MyApp (line 4) | function MyApp({ Component, pageProps }: AppProps) {
FILE: chapter9/deploy/digitalocean/next/pages/api/hello.ts
type Data (line 4) | type Data = {
function handler (line 8) | function handler(
FILE: chapter9/deploy/netlify/cra/src/App.tsx
function App (line 5) | function App() {
FILE: chapter9/deploy/netlify/next/pages/_app.tsx
function MyApp (line 4) | function MyApp({ Component, pageProps }: AppProps) {
FILE: chapter9/deploy/netlify/next/pages/api/hello.ts
type Data (line 4) | type Data = {
function handler (line 8) | function handler(
FILE: chapter9/deploy/netlify/next/pages/hello.tsx
function Hello (line 3) | function Hello({hello}: {hello: string}) {
FILE: chapter9/deploy/vercel/cra/src/App.tsx
function App (line 5) | function App() {
FILE: chapter9/deploy/vercel/next/pages/_app.tsx
function MyApp (line 4) | function MyApp({ Component, pageProps }: AppProps) {
FILE: chapter9/deploy/vercel/next/pages/api/hello.ts
type Data (line 4) | type Data = {
function handler (line 8) | function handler(
FILE: chapter9/zero-to-next/src/_app.tsx
function reportWebVitals (line 3) | function reportWebVitals(metric: NextWebVitalsMetric) {
function MyApp (line 8) | function MyApp({ Component, pageProps }: AppProps) {
FILE: chapter9/zero-to-next/src/components/todo/todo.tsx
function TodoComponent (line 12) | function TodoComponent({ todos }: { todos: Array<Todo> }) {
function TodoItem (line 22) | function TodoItem({ todo }: { todo: Todo }) {
FILE: chapter9/zero-to-next/src/hooks/useToggle.ts
function useToggle (line 6) | function useToggle(
FILE: chapter9/zero-to-next/src/pages/_document.tsx
function MyDocument (line 11) | function MyDocument() {
FILE: chapter9/zero-to-next/src/pages/index.tsx
function Index (line 7) | function Index({ todos }: { todos: Array<Todo> }) {
FILE: chapter9/zero-to-next/src/pages/todos/[id].tsx
function TodoItem (line 6) | function TodoItem({ todo }: { todo: Todo }) {
FILE: chapter9/zero-to-next/src/types/todo.ts
type Todo (line 1) | interface Todo {
FILE: chapter9/zero-to-next/src/utils/errors/index.ts
class NotFoundError (line 3) | class NotFoundError extends Error {
method constructor (line 4) | public constructor(private resourceId: unknown) {
method message (line 8) | public get message() {
function withGetServerSideProps (line 13) | function withGetServerSideProps(
Condensed preview — 394 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (313K chars).
[
{
"path": ".github/workflows/build.yaml",
"chars": 543,
"preview": "name: chapter9 build\nrun-name: ${{ github. actor }} has been added new commit.\n\non:\n push:\n branches-ignore:\n -"
},
{
"path": ".gitignore",
"chars": 1641,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": "README.md",
"chars": 1634,
"preview": "# react-deep-dive-example\n\n《모던 리액트 Deep Dive》 예제 코드입니다.\n\n## Table of Contents\n\n### 2장 리액트 핵심 요소 깊게 살펴보기 [📁](./chapter2)\n"
},
{
"path": "chapter10/react-gradual-demo/.eslintignore",
"chars": 32,
"preview": "node_modules\nbuild\nsrc/*/shared\n"
},
{
"path": "chapter10/react-gradual-demo/.eslintrc.js",
"chars": 171,
"preview": "module.exports = {\n extends: [\n '@titicaca/eslint-config-triple',\n '@titicaca/eslint-config-triple/frontend',\n "
},
{
"path": "chapter10/react-gradual-demo/.gitignore",
"chars": 42,
"preview": "node_modules\nbuild\n.DS_Store\nsrc/*/shared\n"
},
{
"path": "chapter10/react-gradual-demo/.prettierrc",
"chars": 35,
"preview": "\"@titicaca/prettier-config-triple\"\n"
},
{
"path": "chapter10/react-gradual-demo/LICENSE",
"chars": 1086,
"preview": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any pers"
},
{
"path": "chapter10/react-gradual-demo/README.md",
"chars": 591,
"preview": "# Demo of Gradual React Upgrades\n\nhttps://github.com/reactjs/react-gradual-upgrade-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 "
},
{
"path": "chapter10/react-gradual-demo/package.json",
"chars": 1377,
"preview": "{\n \"name\": \"react-gradual-demo\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"react-scripts\": \"^5."
},
{
"path": "chapter10/react-gradual-demo/public/index.html",
"chars": 310,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "chapter10/react-gradual-demo/src/index.js",
"chars": 215,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter10/react-gradual-demo/src/legacy/Greeting.js",
"chars": 1084,
"preview": "import React, {Component} from 'react';\nimport {findDOMNode} from 'react-dom';\n\nimport ThemeContext from './shared/Theme"
},
{
"path": "chapter10/react-gradual-demo/src/legacy/README.md",
"chars": 36,
"preview": "# legacy\n\n`react@16`을 기반으로 작성된 컴포넌트\n"
},
{
"path": "chapter10/react-gradual-demo/src/legacy/createLegacyRoot.js",
"chars": 531,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport ThemeContext from './shared/ThemeContext';\n\nexport "
},
{
"path": "chapter10/react-gradual-demo/src/legacy/package.json",
"chars": 126,
"preview": "{\n \"private\": true,\n \"name\": \"react@16 application\",\n \"dependencies\": {\n \"react\": \"16.8\",\n \"react-dom\": \"16.8\"\n"
},
{
"path": "chapter10/react-gradual-demo/src/modern/AboutPage.js",
"chars": 569,
"preview": "import React, {useContext} from 'react';\n\nimport Clock from './shared/Clock';\nimport ThemeContext from './shared/ThemeCo"
},
{
"path": "chapter10/react-gradual-demo/src/modern/App.js",
"chars": 918,
"preview": "import React, {useState, Suspense} from 'react';\n\nimport AboutPage from './AboutPage';\nimport ThemeContext from './share"
},
{
"path": "chapter10/react-gradual-demo/src/modern/HomePage.js",
"chars": 414,
"preview": "import React, {useContext} from 'react';\n\nimport ThemeContext from './shared/ThemeContext';\nimport Clock from './shared/"
},
{
"path": "chapter10/react-gradual-demo/src/modern/README.md",
"chars": 114,
"preview": "# modern\n\n`react@17`을 기반으로 작성된 컴포넌트가 모여있으며, 애플리케이션의 시작점이다. `react@17` 을 루트에 선언해 두면, 자식 컴포넌트의 리액트 버전은 17 외에도 가능하다.\n"
},
{
"path": "chapter10/react-gradual-demo/src/modern/index.js",
"chars": 390,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter10/react-gradual-demo/src/modern/lazyLegacyRoot.js",
"chars": 2362,
"preview": "import React, {useContext, useMemo, useRef, useLayoutEffect} from 'react';\n\nimport ThemeContext from './shared/ThemeCont"
},
{
"path": "chapter10/react-gradual-demo/src/modern/package.json",
"chars": 130,
"preview": "{\n \"private\": true,\n \"name\": \"react@17 application\",\n \"dependencies\": {\n \"react\": \"17.0.0\",\n \"react-dom\": \"17.0"
},
{
"path": "chapter10/react-gradual-demo/src/shared/Clock.js",
"chars": 344,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter10/react-gradual-demo/src/shared/README.md",
"chars": 86,
"preview": "# @shared\n\n`react@16`과 `react@17`에서 공통으로 사용하는 패키지. npm 에서 제공하는 리액트 라이브러리와 비슷하게 보면 된다.\n"
},
{
"path": "chapter10/react-gradual-demo/src/shared/ThemeContext.js",
"chars": 300,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter10/react-gradual-demo/src/shared/useTime.js",
"chars": 509,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/next13/.eslintignore",
"chars": 18,
"preview": ".next\nnode_modules"
},
{
"path": "chapter11/next13/.eslintrc.js",
"chars": 268,
"preview": "module.exports = {\n extends: [\n 'next/core-web-vitals',\n '@titicaca/eslint-config-triple',\n '@titicaca/eslint-"
},
{
"path": "chapter11/next13/.gitignore",
"chars": 385,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "chapter11/next13/.npmrc",
"chars": 22,
"preview": "legacy-peer-deps=true\n"
},
{
"path": "chapter11/next13/.prettierignore",
"chars": 18,
"preview": ".next\nnode_modules"
},
{
"path": "chapter11/next13/.prettierrc",
"chars": 35,
"preview": "\"@titicaca/prettier-config-triple\"\n"
},
{
"path": "chapter11/next13/README.md",
"chars": 1304,
"preview": "# Chapter8 react@18 nextjs@13 예제\n\nhttps://react-deep-dive-example-six.vercel.app/\n\nhttps://github.com/vercel/app-playgro"
},
{
"path": "chapter11/next13/app/api/hello/route.ts",
"chars": 180,
"preview": "export async function GET() {\n return new Response(JSON.stringify({ name: 'John Doe' }), {\n status: 200,\n headers"
},
{
"path": "chapter11/next13/app/api/posts/[id]/route.ts",
"chars": 637,
"preview": "import type { NextRequest } from 'next/server'\n\nexport async function GET(\n req: NextRequest,\n context: { params: { id"
},
{
"path": "chapter11/next13/app/api/posts/route.ts",
"chars": 350,
"preview": "import type { NextRequest } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n const response = awa"
},
{
"path": "chapter11/next13/app/api/users/[id]/route.ts",
"chars": 618,
"preview": "import type { NextRequest } from 'next/server'\n\nexport async function GET(\n request: NextRequest,\n context: { params: "
},
{
"path": "chapter11/next13/app/api/users/[id].ts",
"chars": 0,
"preview": ""
},
{
"path": "chapter11/next13/app/api/users/route.ts",
"chars": 283,
"preview": "export async function GET() {\n const response = await fetch('https://jsonplaceholder.typicode.com/users')\n const resul"
},
{
"path": "chapter11/next13/app/context/[id]/page.tsx",
"chars": 391,
"preview": "import Counter from '#components/Counter'\nimport { fetchUserById } from '#services/server'\n\nexport default async functio"
},
{
"path": "chapter11/next13/app/context/layout.tsx",
"chars": 699,
"preview": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { CounterProvider } from '#contex"
},
{
"path": "chapter11/next13/app/context/page.tsx",
"chars": 739,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/error/[id]/page.tsx",
"chars": 228,
"preview": "import { notFound } from 'next/navigation'\nimport { ReactNode } from 'react'\n\nexport default function Page(): ReactNode "
},
{
"path": "chapter11/next13/app/error/error.tsx",
"chars": 722,
"preview": "'use client'\n\nimport { useEffect } from 'react'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport d"
},
{
"path": "chapter11/next13/app/error/layout.tsx",
"chars": 589,
"preview": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { TabGroup } from '#components/Ta"
},
{
"path": "chapter11/next13/app/error/not-found.tsx",
"chars": 59,
"preview": "export default function NotFound() {\n return '404 입니다😭'\n}\n"
},
{
"path": "chapter11/next13/app/error/page.tsx",
"chars": 900,
"preview": "import ErrorButton from '#components/ErrorButton'\n\nexport default function Page() {\n return (\n <div className=\"space"
},
{
"path": "chapter11/next13/app/globals.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "chapter11/next13/app/grouped-layouts/(main)/layout.tsx",
"chars": 597,
"preview": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout("
},
{
"path": "chapter11/next13/app/grouped-layouts/(main)/page.tsx",
"chars": 669,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/grouped-layouts/(todos)/hello/page.tsx",
"chars": 253,
"preview": "export default async function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medi"
},
{
"path": "chapter11/next13/app/grouped-layouts/(todos)/layout.tsx",
"chars": 248,
"preview": "import { ReactNode } from 'react'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n retur"
},
{
"path": "chapter11/next13/app/grouped-layouts/(todos)/todos/page.tsx",
"chars": 518,
"preview": "import { fetchTodos } from '#services/server'\n\nexport default async function Page() {\n const todos = await fetchTodos()"
},
{
"path": "chapter11/next13/app/grouped-layouts/(users)/layout.tsx",
"chars": 247,
"preview": "import { ReactNode } from 'react'\n\nexport default async function Layout({ children }: { children: ReactNode }) {\n retur"
},
{
"path": "chapter11/next13/app/grouped-layouts/(users)/users/page.tsx",
"chars": 517,
"preview": "import { fetchUsers } from '#services/server'\n\nexport default async function Page() {\n const users = await fetchUsers()"
},
{
"path": "chapter11/next13/app/head/[userId]/head.tsx",
"chars": 451,
"preview": "import DefaultHeader from '#components/DefaultHeader'\nimport { API_URL_BASE } from '#services/constant'\n\nexport default "
},
{
"path": "chapter11/next13/app/head/[userId]/page.tsx",
"chars": 563,
"preview": "import SubPage from './sub'\nimport { API_URL_BASE } from '#services/constant'\n\nexport default async function Page({ para"
},
{
"path": "chapter11/next13/app/head/[userId]/sub.tsx",
"chars": 417,
"preview": "import { API_URL_BASE } from '#services/constant'\n\nexport default async function SubPage({\n params,\n}: {\n params: { us"
},
{
"path": "chapter11/next13/app/head/head.tsx",
"chars": 254,
"preview": "import DefaultHeader from '#components/DefaultHeader'\n\nexport default function Head() {\n return (\n <>\n <Default"
},
{
"path": "chapter11/next13/app/head/layout.tsx",
"chars": 588,
"preview": "import { ReactNode } from 'react'\nimport { fetchUsers } from '#services/server'\nimport { TabGroup } from '#components/Ta"
},
{
"path": "chapter11/next13/app/head/page.tsx",
"chars": 1583,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-6\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/head.tsx",
"chars": 287,
"preview": "import DefaultHeader from '../src/components/DefaultHeader'\n\nexport default function Head() {\n return (\n <>\n <D"
},
{
"path": "chapter11/next13/app/internal-api/hello/route.ts",
"chars": 240,
"preview": "import { NextRequest } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n return new Response(JSON."
},
{
"path": "chapter11/next13/app/isr/[id]/page.tsx",
"chars": 869,
"preview": "import { fetchPostById } from '#services/server'\n\nexport const dynamicParams = true\n\nexport const revalidate = 15 // rev"
},
{
"path": "chapter11/next13/app/isr/layout.tsx",
"chars": 620,
"preview": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\n\nconst ids = [{ id: '1' }, { id: '2' }"
},
{
"path": "chapter11/next13/app/isr/page.tsx",
"chars": 877,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/layout.tsx",
"chars": 597,
"preview": "import './globals.css'\nimport { ReactNode } from 'react'\nimport SideBar from '#components/Sidebar'\n\nexport default funct"
},
{
"path": "chapter11/next13/app/layouts/[userId]/page.tsx",
"chars": 373,
"preview": "import { fetchUserById } from '#services/server'\n\nexport default async function Page({ params }: { params: { userId: str"
},
{
"path": "chapter11/next13/app/layouts/layout.tsx",
"chars": 591,
"preview": "import { ReactNode } from 'react'\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services/"
},
{
"path": "chapter11/next13/app/layouts/page.tsx",
"chars": 506,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/loading/[userId]/page.tsx",
"chars": 433,
"preview": "import { sleep } from '#lib/utils'\nimport { fetchUserById } from '#services/server'\n\nexport default async function Page("
},
{
"path": "chapter11/next13/app/loading/layout.tsx",
"chars": 592,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services"
},
{
"path": "chapter11/next13/app/loading/loading.tsx",
"chars": 247,
"preview": "export default function Loading() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium "
},
{
"path": "chapter11/next13/app/loading/page.tsx",
"chars": 690,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/page.tsx",
"chars": 1484,
"preview": "import Link from 'next/link'\nimport { demos } from '../src/constant/menu'\n\nexport default function Page() {\n return (\n "
},
{
"path": "chapter11/next13/app/server-action/form/[id]/loading.tsx",
"chars": 246,
"preview": "export default function Loading() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium "
},
{
"path": "chapter11/next13/app/server-action/form/[id]/page.tsx",
"chars": 1695,
"preview": "import kv from '@vercel/kv'\nimport { revalidatePath } from 'next/cache'\n\ninterface Data {\n name: string\n age: number\n}"
},
{
"path": "chapter11/next13/app/server-action/form/page.tsx",
"chars": 975,
"preview": "export default function Page() {\n async function handleSubmit() {\n 'use server'\n\n console.log('해당 작업은 서버에서 수행합니다."
},
{
"path": "chapter11/next13/app/server-action/layout.tsx",
"chars": 422,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nexport default async function Layout"
},
{
"path": "chapter11/next13/app/server-action/page.tsx",
"chars": 640,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/server-action/start-transition/[id]/page.tsx",
"chars": 890,
"preview": "import kv from '@vercel/kv'\nimport { ClientButtonComponent } from '#components/server-action/client-component'\n\ninterfac"
},
{
"path": "chapter11/next13/app/ssg/[id]/page.tsx",
"chars": 484,
"preview": "import { fetchPostById } from '#services/server'\n\nexport async function generateStaticParams() {\n return [{ id: '1' }, "
},
{
"path": "chapter11/next13/app/ssg/layout.tsx",
"chars": 853,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nconst ids = [{ id: '1' }, { id: '2' "
},
{
"path": "chapter11/next13/app/ssg/page.tsx",
"chars": 760,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/ssr/[id]/page.tsx",
"chars": 395,
"preview": "import { fetchPostById } from '#services/server'\n\nexport default async function Page({ params }: { params: { id: string "
},
{
"path": "chapter11/next13/app/ssr/layout.tsx",
"chars": 588,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services"
},
{
"path": "chapter11/next13/app/ssr/page.tsx",
"chars": 911,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/streaming/[id]/components.tsx",
"chars": 669,
"preview": "import { sleep } from '#lib/utils'\nimport { fetchPosts, fetchUsers } from '#services/server'\n\nexport async function User"
},
{
"path": "chapter11/next13/app/streaming/[id]/page.tsx",
"chars": 645,
"preview": "import { Suspense } from 'react'\n\nimport { PostByUserId, Users } from './components'\n\nexport default async function Page"
},
{
"path": "chapter11/next13/app/streaming/layout.tsx",
"chars": 594,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\nimport { fetchUsers } from '#services"
},
{
"path": "chapter11/next13/app/streaming/page.tsx",
"chars": 1132,
"preview": "export default async function Page() {\n return (\n <div className=\"space-y-8\">\n <div className=\"space-y-4\">\n "
},
{
"path": "chapter11/next13/app/styles/css-modules/page.tsx",
"chars": 643,
"preview": "import styles from './styles.module.css'\n\nconst SkeletonCard = () => (\n <div className={styles.skeleton}>\n <div clas"
},
{
"path": "chapter11/next13/app/styles/css-modules/styles.module.css",
"chars": 899,
"preview": ".container {\n display: grid;\n grid-template-columns: repeat(1, minmax(0, 1fr));\n gap: 1.5rem /* 24px */;\n}\n\n@media (m"
},
{
"path": "chapter11/next13/app/styles/global-css/page.tsx",
"chars": 581,
"preview": "import './style.css'\n\nconst SkeletonCard = () => (\n <div className=\"skeleton\">\n <div className=\"skeleton-img\" />\n "
},
{
"path": "chapter11/next13/app/styles/global-css/style.css",
"chars": 899,
"preview": ".container {\n display: grid;\n grid-template-columns: repeat(1, minmax(0, 1fr));\n gap: 1.5rem /* 24px */;\n}\n\n@media (m"
},
{
"path": "chapter11/next13/app/styles/layout.tsx",
"chars": 654,
"preview": "import { ReactNode } from 'react'\n\nimport { TabGroup } from '#components/TabGroup'\n\nconst items = [\n {\n text: 'Globa"
},
{
"path": "chapter11/next13/app/styles/page.tsx",
"chars": 281,
"preview": "export default function Page() {\n return (\n <div className=\"space-y-4\">\n <h1 className=\"text-xl font-medium tex"
},
{
"path": "chapter11/next13/app/styles/styled-components/layout.tsx",
"chars": 259,
"preview": "import { ReactNode } from 'react'\n\nimport StyledComponentsRegistry from '#components/StyledComponentsRegistry'\n\nexport d"
},
{
"path": "chapter11/next13/app/styles/styled-components/page.tsx",
"chars": 636,
"preview": "import {\n SkeletonInner,\n SkeletonImg,\n SkeletonBtn,\n SkeletonLineOne,\n SkeletonLineTwo,\n Container,\n} from '#comp"
},
{
"path": "chapter11/next13/app/styles/styled-jsx/StyledRegistry.tsx",
"chars": 687,
"preview": "'use client'\n\nimport { ReactNode, useState } from 'react'\nimport { useServerInsertedHTML } from 'next/navigation'\nimport"
},
{
"path": "chapter11/next13/app/styles/styled-jsx/components.tsx",
"chars": 1336,
"preview": "'use client'\n\nexport const SkeletonCard = () => (\n <>\n <div className=\"skeleton\">\n <div className=\"skeleton-img"
},
{
"path": "chapter11/next13/app/styles/styled-jsx/layout.tsx",
"chars": 218,
"preview": "import { ReactNode } from 'react'\n\nimport StyledJsxRegistry from './StyledRegistry'\n\nexport default function Layout({ ch"
},
{
"path": "chapter11/next13/app/styles/styled-jsx/page.tsx",
"chars": 861,
"preview": "'use client'\n\nimport { SkeletonCard } from './components'\n\nexport default function Page() {\n return (\n <div classNam"
},
{
"path": "chapter11/next13/middleware.ts",
"chars": 397,
"preview": "import { NextRequest, NextResponse } from 'next/server'\n\nexport function middleware(request: NextRequest) {\n const requ"
},
{
"path": "chapter11/next13/next.config.js",
"chars": 166,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n reactStrictMode: true,\n experimental: {\n serverActio"
},
{
"path": "chapter11/next13/package.json",
"chars": 952,
"preview": "{\n \"name\": \"next13\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"dev:turbo\": \"n"
},
{
"path": "chapter11/next13/postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "chapter11/next13/src/components/Counter.tsx",
"chars": 517,
"preview": "'use client'\n\nimport { useCallback } from 'react'\n\nimport { useCounter } from '../context/counter'\n\nconst Counter = () ="
},
{
"path": "chapter11/next13/src/components/DefaultHeader.tsx",
"chars": 644,
"preview": "import { memo } from 'react'\n\nfunction DefaultHeader() {\n return (\n <>\n <meta name=\"viewport\" content=\"width=de"
},
{
"path": "chapter11/next13/src/components/ErrorButton.tsx",
"chars": 526,
"preview": "'use client'\n\nimport { useCallback, useState } from 'react'\n\nexport default function ErrorButton() {\n const [clicked, s"
},
{
"path": "chapter11/next13/src/components/Sidebar.tsx",
"chars": 2667,
"preview": "'use client'\n\nimport Link from 'next/link'\nimport { useSelectedLayoutSegment } from 'next/navigation'\nimport { clsx } fr"
},
{
"path": "chapter11/next13/src/components/StyledComponentsRegistry.tsx",
"chars": 742,
"preview": "'use client'\n\nimport { ReactNode, useState } from 'react'\nimport { useServerInsertedHTML } from 'next/navigation'\nimport"
},
{
"path": "chapter11/next13/src/components/Tab.tsx",
"chars": 840,
"preview": "'use client'\n\nimport { clsx } from 'clsx'\nimport Link from 'next/link'\nimport { useSelectedLayoutSegment } from 'next/na"
},
{
"path": "chapter11/next13/src/components/TabGroup.tsx",
"chars": 353,
"preview": "import { Tab } from './Tab'\n\nexport interface Item {\n text: string\n slug?: string\n}\n\nexport const TabGroup = ({ path, "
},
{
"path": "chapter11/next13/src/components/components.ts",
"chars": 1085,
"preview": "'use client'\n\nimport styled from 'styled-components'\n\nexport const Container = styled.div`\n display: grid;\n grid-templ"
},
{
"path": "chapter11/next13/src/components/server-action/client-component.tsx",
"chars": 511,
"preview": "'use client'\nimport { useCallback, useTransition } from 'react'\nimport { updateData } from '#server-action'\nimport { Ske"
},
{
"path": "chapter11/next13/src/constant/menu.ts",
"chars": 2418,
"preview": "export interface Item {\n name: string\n slug: string\n description: string\n}\n\nexport const demos: Array<{ name: string;"
},
{
"path": "chapter11/next13/src/context/counter.tsx",
"chars": 673,
"preview": "'use client'\n\nimport {\n createContext,\n Dispatch,\n SetStateAction,\n useState,\n useContext,\n ReactNode,\n} from 'rea"
},
{
"path": "chapter11/next13/src/lib/utils.ts",
"chars": 103,
"preview": "export async function sleep(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"
},
{
"path": "chapter11/next13/src/server-action/index.ts",
"chars": 361,
"preview": "'use server'\n\nimport kv from '@vercel/kv'\nimport { revalidatePath } from 'next/cache'\nimport { cookies } from 'next/head"
},
{
"path": "chapter11/next13/src/services/constant.ts",
"chars": 119,
"preview": "export const API_URL_BASE = process.env.VERCEL_URL\n ? 'https://' + process.env.VERCEL_URL\n : 'http://localhost:3000'\n"
},
{
"path": "chapter11/next13/src/services/server.ts",
"chars": 1611,
"preview": "interface User {\n id: number\n name: string\n email: string\n address: {\n street: string\n suite: string\n city:"
},
{
"path": "chapter11/next13/tailwind.config.js",
"chars": 1757,
"preview": "const colors = require('tailwindcss/colors')\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: "
},
{
"path": "chapter11/next13/tsconfig.json",
"chars": 968,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \"./\",\n \"paths\": {\n \"#app/*\": [\"app/*\"],\n \"#components/*\": [\"src/com"
},
{
"path": "chapter11/server-components-demo/.gitignore",
"chars": 372,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "chapter11/server-components-demo/.nvmrc",
"chars": 12,
"preview": "lts/hydrogen"
},
{
"path": "chapter11/server-components-demo/.prettierignore",
"chars": 262,
"preview": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n/dist\n\n# misc\n.DS_Store\n.eslintcach"
},
{
"path": "chapter11/server-components-demo/.prettierrc.js",
"chars": 371,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/CODE_OF_CONDUCT.md",
"chars": 3355,
"preview": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and"
},
{
"path": "chapter11/server-components-demo/Dockerfile",
"chars": 178,
"preview": "FROM node:lts-hydrogen\n\nWORKDIR /opt/notes-app\n\nCOPY package.json package-lock.json ./\n\nRUN npm install --legacy-peer-de"
},
{
"path": "chapter11/server-components-demo/LICENSE",
"chars": 1086,
"preview": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any pers"
},
{
"path": "chapter11/server-components-demo/README.md",
"chars": 309,
"preview": "# Demo of sever components\n\nhttps://github.com/reactjs/server-components-demo 프로젝트의 일부 불필요한 내용을 제거하고 간단하게 변경한 프로젝트입니다.\n\n"
},
{
"path": "chapter11/server-components-demo/credentials.js",
"chars": 152,
"preview": "module.exports = {\n host: process.env.DB_HOST || 'localhost',\n database: 'notesapi',\n user: 'notesadmin',\n password:"
},
{
"path": "chapter11/server-components-demo/docker-compose.yml",
"chars": 756,
"preview": "version: \"3.8\"\nservices:\n postgres:\n image: postgres:13\n environment:\n POSTGRES_USER: notesadmin\n POSTG"
},
{
"path": "chapter11/server-components-demo/notes/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "chapter11/server-components-demo/package.json",
"chars": 2002,
"preview": "{\n \"name\": \"react-notes\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"engines\": {\n \"node\": \">=14.9.0\"\n },\n \"licens"
},
{
"path": "chapter11/server-components-demo/public/index.html",
"chars": 1027,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"description\" content=\"React with "
},
{
"path": "chapter11/server-components-demo/public/style.css",
"chars": 12891,
"preview": "/* -------------------------------- CSSRESET --------------------------------*/\n/* CSS Reset adapted from https://dev.to"
},
{
"path": "chapter11/server-components-demo/scripts/build.js",
"chars": 1685,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/scripts/init_db.sh",
"chars": 296,
"preview": "#!/bin/bash\nset -e\n\npsql -v ON_ERROR_STOP=1 --username \"$POSTGRES_USER\" --dbname \"$POSTGRES_DB\" <<-EOSQL\n DROP TABLE IF"
},
{
"path": "chapter11/server-components-demo/scripts/seed.js",
"chars": 2778,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/server/api.server.js",
"chars": 5310,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/server/package.json",
"chars": 54,
"preview": "{\n \"type\": \"commonjs\",\n \"main\": \"./api.server.js\"\n}\n"
},
{
"path": "chapter11/server-components-demo/src/App.js",
"chars": 1473,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/EditButton.js",
"chars": 882,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/Note.js",
"chars": 1983,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/NoteEditor.js",
"chars": 3376,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/NoteList.js",
"chars": 1191,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/NoteListSkeleton.js",
"chars": 843,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/NotePreview.js",
"chars": 394,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/NoteSkeleton.js",
"chars": 2520,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/SearchField.js",
"chars": 1109,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/SidebarNote.js",
"chars": 989,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/SidebarNoteContent.js",
"chars": 2227,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/Spinner.js",
"chars": 416,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/TextWithMarkdown.js",
"chars": 767,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/db.js",
"chars": 448,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/framework/bootstrap.js",
"chars": 872,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter11/server-components-demo/src/framework/router.js",
"chars": 2906,
"preview": "/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found i"
},
{
"path": "chapter2/react/.eslintignore",
"chars": 18,
"preview": "build\nnode_modules"
},
{
"path": "chapter2/react/.eslintrc.js",
"chars": 171,
"preview": "module.exports = {\n extends: [\n '@titicaca/eslint-config-triple',\n '@titicaca/eslint-config-triple/frontend',\n "
},
{
"path": "chapter2/react/.npmrc",
"chars": 23,
"preview": "auto-install-peers=true"
},
{
"path": "chapter2/react/.prettierignore",
"chars": 18,
"preview": "build\nnode_modules"
},
{
"path": "chapter2/react/.prettierrc",
"chars": 35,
"preview": "\"@titicaca/prettier-config-triple\"\n"
},
{
"path": "chapter2/react/README.md",
"chars": 22,
"preview": "# Chapter2\n\n챕터2 예제 코드\n"
},
{
"path": "chapter2/react/package.json",
"chars": 1029,
"preview": "{\n \"name\": \"chapter2\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@babel/plugin-syntax-flow\": \"^"
},
{
"path": "chapter2/react/public/index.html",
"chars": 1721,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.i"
},
{
"path": "chapter2/react/public/manifest.json",
"chars": 492,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n "
},
{
"path": "chapter2/react/public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "chapter2/react/src/App.tsx",
"chars": 507,
"preview": "import { Outlet, Link } from 'react-router-dom'\n\nexport default function App() {\n return (\n <div>\n <h1>Chapter2"
},
{
"path": "chapter2/react/src/index.tsx",
"chars": 883,
"preview": "import { BrowserRouter, Routes, Route } from 'react-router-dom'\nimport ReactDOM from 'react-dom/client'\n\nimport App from"
},
{
"path": "chapter2/react/src/react-app-env.d.ts",
"chars": 40,
"preview": "/// <reference types=\"react-scripts\" />\n"
},
{
"path": "chapter2/react/src/routes/2-1.tsx",
"chars": 1361,
"preview": "import { ReactNode } from 'react'\n\nfunction A({\n children,\n required,\n}: {\n required?: boolean\n children?: ReactNode"
},
{
"path": "chapter2/react/src/routes/2-4.tsx",
"chars": 1171,
"preview": "import React from 'react'\n\n// props 타입을 선언한다.\ninterface SampleProps {\n required?: boolean\n text: string\n}\n\n// state 타입"
},
{
"path": "chapter2/react/src/routes/2-5.tsx",
"chars": 669,
"preview": "import { Component } from 'react'\n\ntype Props = Record<string, never>\n\ninterface State {\n count: number\n}\n\nclass Sample"
},
{
"path": "chapter2/react/src/routes/2-7.tsx",
"chars": 1369,
"preview": "import React from 'react'\n\ninterface State {\n count: number\n}\n\ntype Props = Record<string, never>\n\nexport class ReactCo"
},
{
"path": "chapter2/react/src/routes/2-8.tsx",
"chars": 920,
"preview": "import React from 'react'\n\ninterface State {\n count: number\n}\n\ntype Props = Record<string, never>\n\nexport class ReactPu"
},
{
"path": "chapter2/react/tsconfig.json",
"chars": 506,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n "
},
{
"path": "chapter4/next-example/.eslintignore",
"chars": 19,
"preview": ".next\nnode_modules\n"
},
{
"path": "chapter4/next-example/.eslintrc.js",
"chars": 199,
"preview": "module.exports = {\n extends: [\n 'next/core-web-vitals',\n '@titicaca/eslint-config-triple',\n '@titicaca/eslint-"
},
{
"path": "chapter4/next-example/.gitignore",
"chars": 385,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "chapter4/next-example/.prettierignore",
"chars": 19,
"preview": ".next\nnode_modules\n"
},
{
"path": "chapter4/next-example/.prettierrc",
"chars": 35,
"preview": "\"@titicaca/prettier-config-triple\"\n"
},
{
"path": "chapter4/next-example/README.md",
"chars": 73,
"preview": "# next-example\n\n## Introduction\n\n`create-next-app`으로 만들어진 예제 애플리케이션 입니다.\n"
},
{
"path": "chapter4/next-example/next.config.js",
"chars": 118,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n reactStrictMode: true,\n}\n\nmodule.exports = nextConfig\n"
},
{
"path": "chapter4/next-example/package.json",
"chars": 687,
"preview": "{\n \"name\": \"my-app\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next "
},
{
"path": "chapter4/next-example/src/pages/404.tsx",
"chars": 282,
"preview": "import { useCallback } from 'react'\n\nexport default function My404Page() {\n const handleClick = useCallback(() => {\n "
},
{
"path": "chapter4/next-example/src/pages/500.tsx",
"chars": 304,
"preview": "import { useCallback } from 'react'\n\nexport default function My500Page() {\n const handleClick = useCallback(() => {\n "
},
{
"path": "chapter4/next-example/src/pages/_app.tsx",
"chars": 148,
"preview": "import type { AppProps } from 'next/app'\n\nexport default function App({ Component, pageProps }: AppProps) {\n return <Co"
},
{
"path": "chapter4/next-example/src/pages/_document.tsx",
"chars": 231,
"preview": "import { Html, Head, Main, NextScript } from 'next/document'\n\nexport default function Document() {\n return (\n <Html "
},
{
"path": "chapter4/next-example/src/pages/_error.tsx",
"chars": 381,
"preview": "import { NextPageContext } from 'next'\n\nfunction Error({ statusCode }: { statusCode: number }) {\n return (\n <>\n "
},
{
"path": "chapter4/next-example/src/pages/api/hello.ts",
"chars": 311,
"preview": "// Next.js API route support: https://nextjs.org/docs/api-routes/introduction\nimport type { NextApiRequest, NextApiRespo"
},
{
"path": "chapter4/next-example/src/pages/hello/[greeting].tsx",
"chars": 325,
"preview": "import { NextPageContext } from 'next'\n\nexport default function HelloGreeting({ greeting }: { greeting: string }) {\n re"
},
{
"path": "chapter4/next-example/src/pages/hello/world.tsx",
"chars": 67,
"preview": "export default function HelloWorld() {\n return <>hello world</>\n}\n"
},
{
"path": "chapter4/next-example/src/pages/hello.tsx",
"chars": 227,
"preview": "export default function Hello() {\n console.log(typeof window === 'undefined' ? '서버' : '클라이언트') // eslint-disable-line n"
},
{
"path": "chapter4/next-example/src/pages/hi/[...props].tsx",
"chars": 782,
"preview": "import { useRouter } from 'next/router'\nimport { useEffect } from 'react'\nimport { NextPageContext } from 'next'\n\nexport"
},
{
"path": "chapter4/next-example/src/pages/index.tsx",
"chars": 486,
"preview": "import type { NextPage } from 'next'\nimport Link from 'next/link'\n\nconst Home: NextPage = () => {\n return (\n <ul>\n "
},
{
"path": "chapter4/next-example/src/pages/todo/[id].tsx",
"chars": 783,
"preview": "import Link from 'next/link'\nimport { NextPageContext } from 'next'\n\nexport default function Todo({\n todo,\n}: {\n todo:"
},
{
"path": "chapter4/next-example/src/styles/Home.module.css",
"chars": 4784,
"preview": ".main {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: center;\n padding: 6"
}
]
// ... and 194 more files (download for full content)
About this extraction
This page contains the full source code of the wikibook/react-deep-dive-example GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 394 files (253.5 KB), approximately 86.2k tokens, and a symbol index with 222 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.