├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── create-project ├── docs ├── .babelrc ├── .editorconfig ├── .gitignore ├── assets │ └── react-logo.svg ├── package.json ├── posts │ ├── batch.md │ ├── hooks.md │ ├── installation.md │ ├── renderingAPI.md │ ├── startTransition.md │ ├── strictMode.md │ ├── suspense.md │ └── welcome.md ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.jsx │ ├── Components │ │ ├── Content.jsx │ │ ├── Contents │ │ │ ├── BatchContent.jsx │ │ │ ├── HooksContent.jsx │ │ │ ├── InstallationContent.jsx │ │ │ ├── RenderingAPIContent.jsx │ │ │ ├── StrictModeContent.jsx │ │ │ ├── SuspenseContent.jsx │ │ │ ├── Transition.jsx │ │ │ ├── Welcome.jsx │ │ │ ├── WorkingContent.jsx │ │ │ └── index.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── Fake.jsx │ │ ├── Layout.jsx │ │ ├── Loader │ │ │ ├── PageSpinner.jsx │ │ │ ├── SectionSpinner.jsx │ │ │ └── loader.css │ │ ├── NavBar.jsx │ │ ├── NavItem.jsx │ │ ├── Post.jsx │ │ ├── SideBar.jsx │ │ └── User.jsx │ ├── Utils │ │ └── Api.js │ ├── index.jsx │ └── style.css ├── webpack.config.js └── yarn.lock ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── cli.js ├── index.js └── main.js └── templates ├── javascript ├── .babelrc ├── .gitignore ├── package.json ├── public │ └── index.html ├── src │ ├── App.jsx │ └── index.jsx ├── webpack.config.js └── yarn.lock └── typescript ├── .babelrc ├── .gitignore ├── package.json ├── public └── index.html ├── src ├── App.tsx └── index.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | *.iml 3 | coverage/ 4 | examples/ 5 | node_modules/ 6 | typings/ 7 | sandbox/ 8 | test/ 9 | bower.json 10 | CODE_OF_CONDUCT.md 11 | COLLABORATOR_GUIDE.md 12 | CONTRIBUTING.md 13 | COOKBOOK.md 14 | ECOSYSTEM.md 15 | Gruntfile.js 16 | karma.conf.js 17 | webpack.*.js 18 | sauce_connect.log 19 | docs 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hong-JunHyeok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://img.shields.io/badge/NPM-link-red)](https://abit.ly/cli-npm-link) 2 | [![Docs](https://img.shields.io/badge/Docs-link-blue)](https://react18-boilerplate.vercel.app/) 3 | 4 | 5 | ![gradient (13)](https://user-images.githubusercontent.com/48292190/161219484-1b1ef1c7-0933-4a9d-8e98-64d759f2ba15.png) 6 | 7 | ![](https://media2.giphy.com/media/G0fSY3gyRigBfHSVi3/giphy.gif?cid=790b7611a47f207f8a29e4d7ae9365438a50c88a0c26db88&rid=giphy.gif&ct=g) 8 | 9 | # Description 10 | 11 | > The repository contains Docs that tell you what features have been added to React 18, and a CLI that allows you to build apps quickly and easily 12 | 13 | # Installation 14 | 15 | React 18 Boilerplate provides a CLI for building apps. 16 | 17 | ```shell 18 | # When using NPM 19 | npm install -g create-react18-boilerplate 20 | # When using yarn 21 | yarn global add create-react18-boilerplate 22 | ``` 23 | 24 | You can run a script with npx 25 | 26 | ```shell 27 | npx create-react18-boilerplate 28 | ``` 29 | 30 | # Usage 31 | 32 | ```shell 33 | mkdir [project_name] 34 | cd [project_name] 35 | create-react18-boilerplate # Create a project when entering a command 36 | ``` 37 | 38 | # Scripts 39 | 40 | CRB (create-react18-boilerplate) creates a webpack-based customizable project. The scripts below are basic functions and can be changed at any time. 41 | 42 | ## Build 43 | 44 | ```shell 45 | npm run build 46 | ``` 47 | 48 | ## Dev mode start 49 | 50 | ```shell 51 | npm run dev 52 | ``` 53 | -------------------------------------------------------------------------------- /bin/create-project: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require = require('esm')(module /*, options*/); 4 | require('../src/cli').cli(process.argv); 5 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | ["@babel/preset-react", { "runtime": "automatic" }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/patches 3 | !/.yarn/plugins 4 | !/.yarn/releases 5 | !/.yarn/sdks 6 | 7 | # Swap the comments on the following lines if you don't wish to use zero-installs 8 | # Documentation here: https://yarnpkg.com/features/zero-installs 9 | !/.yarn/cache 10 | #/.pnp.* 11 | node_modules 12 | dist 13 | .yarn 14 | -------------------------------------------------------------------------------- /docs/assets/react-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | React Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react18-boilerplate-docs", 3 | "type": "module", 4 | "packageManager": "yarn@3.1.0", 5 | "scripts": { 6 | "build": "webpack", 7 | "dev": "webpack serve" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.26.1", 11 | "highlight.js": "^11.5.0", 12 | "react": "^18.0.0", 13 | "react-dom": "^18.0.0", 14 | "react-router-dom": "^6.2.2", 15 | "remark-html": "^15.0.1" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.17.8", 19 | "@babel/plugin-proposal-class-properties": "^7.16.7", 20 | "@babel/preset-env": "^7.16.11", 21 | "@babel/preset-react": "^7.16.7", 22 | "@svgr/webpack": "^6.2.1", 23 | "babel-loader": "^8.2.4", 24 | "css-loader": "^6.7.1", 25 | "file-loader": "^6.2.0", 26 | "html-loader": "^3.1.0", 27 | "html-webpack-plugin": "^5.5.0", 28 | "markdown-loader": "^8.0.0", 29 | "remark-frontmatter": "^4.0.1", 30 | "style-loader": "^3.3.1", 31 | "webpack": "^5.70.0", 32 | "webpack-cli": "^4.9.2", 33 | "webpack-dev-server": "^4.7.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/posts/batch.md: -------------------------------------------------------------------------------- 1 | ## 자동 배치란 더 나은 리렌더링을 위해 여러 개의 상태 업데이트를 한번에 처리하는 방식입니다. 2 | 3 | 기존에 리액트에서는 이벤트 핸들러 내에서만 상태 업데이트를 일괄처리 4 | 했습니다. 즉, Promise, setTimeout등에서는 일괄 처리가 되지 않았다는 5 | 뜻입니다. 하지만 이제 React 18부터는 출처에 관계없이 일괄적으로 상태 6 | 업데이트가 진행되니까 좀 더 효율적인 렌더링을 경험할 수 있습니다. 7 | 8 | ```js 9 | // After React 18 updates inside of timeouts, promises, 10 | // native event handlers or any other event are batched. 11 | 12 | function handleClick() { 13 | setCount((c) => c + 1); 14 | setFlag((f) => !f); 15 | // React will only re-render once at the end (that's batching!) 16 | } 17 | 18 | setTimeout(() => { 19 | setCount((c) => c + 1); 20 | setFlag((f) => !f); 21 | // React will only re-render once at the end (that's batching!) 22 | }, 1000); 23 | ``` 24 | 25 | 만약, 자동 일괄 처리기능을 삭제하고 싶다면 flushSync를 사용하면 됩니다. 26 | 해당 콜백에 상태 업데이트 로직을 넣게되면 상태는 하나하나 렌더링 될 27 | 것입니다. 28 | 29 | ```js 30 | import { flushSync } from "react-dom"; 31 | 32 | function handleClick() { 33 | flushSync(() => { 34 | setCounter((c) => c + 1); 35 | }); 36 | // React has updated the DOM by now 37 | flushSync(() => { 38 | setFlag((f) => !f); 39 | }); 40 | // React has updated the DOM by now 41 | } 42 | ``` 43 | 44 | 자세한 내용은 [여기](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching)를 참고해주세요. 45 | -------------------------------------------------------------------------------- /docs/posts/hooks.md: -------------------------------------------------------------------------------- 1 | # 새로운 Hooks 2 | 3 | ### useId 4 | 5 | _사전 지식_ 6 | 7 | > Hydration은 구성 요소를 렌더링하고 `이벤트 핸들러`를 연결하는 과정을 의미합니다. 8 | > Hydration 이후에만 사용자가 애플리케이션과 상호작용 가능합니다. 9 | 10 | 접근성을 지원하기 위해서 `aria`및 `a11y`등의 API가 브라우저에 널리 사용된다. 이러한 API들은 구성 요소를 연결하기 위해 ID를 기반으로 합니다. 만일 `Math.random()`같은 메서드로 ID를 생성한다면, 다른 외부 라이브러리를 사용하여 필요한 때마다 ID를 생성합니다. 그러나 Server-Side와 Client-Side는 ID의 불일치로 인해서 문제가 발생합니다. 11 | 12 | 지금 직면한 상황을 정리하자면 다음과 같습니다. 13 | 14 | 1. 서버 측에서 렌더링한 다음, Hydration하면 ID 불일치가 발생할 수 있습니다. 15 | 2. 페이지의 한 부분에서 서버 측에서 렌더링하고 페이지의 다른 부분에서 클라이언트 측에서 렌더링하는 경우 두 ID가 다를 수 있으며 동일해야 합니다. 16 | 3. 조건부로 ID가 있는 무언가를 렌더링하면 ID 불일치가 발생할 수도 있습니다. 17 | 18 | 위의 문제를 해결하기 위해서 `useId` Hook이 등장했습니다. 서버 렌더링 및 수화 중에 안정적인 ID를 생성합니다. 서버 렌더링 콘텐츠 외부에서는 전역 카운터로 대체됩니다. 19 | 20 | ```js 21 | import React, { useId } from "react"; 22 | 23 | function App() { 24 | const id = useId(); 25 | return ( 26 | <> 27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 | 40 | ); 41 | } 42 | ``` 43 | 44 | ### useTransition 45 | 46 | `useTransition`과 `startTransition`으로 일부 상태 업데이트를 긴급하지 않은 것으로 표시할 수 있습니다. 47 | 자세한 내용은 [여기]("https://react18-boilerplate.vercel.app/#/transition")를 참고해주세요. 48 | 49 | ### useDeferredValue 50 | 51 | 이 훅을 사용하면 트리의 긴급하지 않은 부분의 재렌더링을 연기할 수 있습니다. debouncing과 비슷하지만 그에 비해서 몇가지 장점이 있는데, 일정 시간 지연이 없기 때문에 React는 첫 번째 렌더가 화면에 반영되는 즉시 지연 렌더링을 시도합니다. 이는 인터럽트 가능하며 사용자 입력을 차단하지 않습니다. [자세한 내용](https://reactjs.org/docs/hooks-reference.html#usedeferredvalue)을 살펴보세요. 52 | 53 | ### useSyncExternalStore 54 | 55 | useSyncExternalStore는 외부 저장소가 저장소에 대한 업데이트를 동기식으로 강제하여 동시 읽기를 지원할 수 있게 해주는 새로운 후크입니다. 외부 데이터 소스에 대한 구독을 구현할 때 useEffect가 필요하지 않으며 React 외부의 상태와 통합되는 모든 라이브러리에 권장됩니다. 여기 [문서](https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore)를 참조하십시오. 56 | 57 | > **Note** 58 | > useSyncExternalStore는 어플리케이션코드가 아닌 라이브러리에서 사용하는 것을 목적으로 하고 있습니다. 59 | 60 | ### useInsertionEffect 61 | 62 | useInsertionEffect는 CSS-in-JS 라이브러리가 렌더링에 스타일을 삽입하는 성능 문제를 해결할 수 있는 새로운 후크입니다. CSS-in-JS 라이브러리를 구축하지 않은 경우 이 라이브러리를 사용할 수 없습니다. 이 후크는 DOM이 변환된 후 실행되지만 레이아웃 효과를 보기 전에 새 레이아웃을 읽습니다. 이렇게 하면 React 17 이하에 이미 존재하는 문제가 해결되지만 React 18에서는 React가 동시 렌더링 중에 브라우저에 반환되므로 레이아웃을 다시 계산할 수 있는 기회가 생기기 때문에 더욱 중요합니다. [여기](https://reactjs.org/docs/hooks-reference.html#useinsertioneffect)를 참조해 주세요. 63 | 64 | > **Note** 65 | > useInsertionEffect는 응용 프로그램 코드가 아닌 라이브러리에서 사용하는 것을 목적으로 합니다. 66 | -------------------------------------------------------------------------------- /docs/posts/installation.md: -------------------------------------------------------------------------------- 1 | # React18, 빠르게 시작하는 방법 2 | 3 | [Create React App](https://create-react-app.dev/)을 사용해서 앱을 빌드하는 방법도 있지만, 몇몇 경우 **과한 크기의 앱을 생성할 수**도 있습니다. 4 | 그런 경우에는 쉽고 가벼고 커스텀가능한 React 18앱을 쉽고 빠르게 생성해주는 라이브러리인 [Create React18 Boilerplate](https://www.npmjs.com/package/create-react18-boilerplate)를 추천합니다. 5 | 6 | 이번 챕터에서는 [Create React18 Boilerplate](https://www.npmjs.com/package/create-react18-boilerplate)를 사용하는 방법을 알아보도록 하겠습니다. 7 | 8 | 먼저 설치를 해보도록 하겠습니다. 9 | 10 | ```shell 11 | # When using NPM 12 | npm install -g create-react18-boilerplate 13 | # When using yarn 14 | yarn global add create-react18-boilerplate 15 | ``` 16 | 17 | 설치가 끝났다면 거의 다 왔습니다. 18 | 먼저 App을 설치할 디렉토리를 생성해준 다음 앱을 생성해보겠습니다. 19 | 20 | ```shell 21 | mkdir [project_name] 22 | cd [project_name] 23 | create-react18-boilerplate # 해당 디렉토리에 보일러플레이트 앱을 생성해줍니다. 24 | ``` 25 | 26 | 혹은 npx로 즉시 실행할 수 있습니다. 27 | 28 | ```shell 29 | npx create-react18-boilerplate 30 | ``` 31 | 32 | ## 정말 끝입니다! 이제 스크립트를 사용해서 프로젝트를 빌드해보세요. 33 | -------------------------------------------------------------------------------- /docs/posts/renderingAPI.md: -------------------------------------------------------------------------------- 1 | # 새로운 클라이언트 및 서버 렌더링 API 2 | 3 | ## React DOM Client 4 | 5 | **이제는 `react-dom/client`를 사용합니다.** 6 | 7 | - `createRoot render` : 기존 React17에서 `ReactDOM.render`를 대신하여 사용합니다. React18의 새로운 기능은 `createRoot render`가 없으면 동작하지 않습니다. 8 | 9 | - `hydrateRoot` : 서버 사이드 렌더링 된 애플리케이션을 Hydration하는 방법입니다. 기존의 `ReactDOM.hydrate` 대신 사용합니다. React18의 새로운 기능은 `hydrateRoot`가 없으면 동작하지 않습니다. 10 | 11 | ## React DOM Server 12 | 13 | **새로운 API는 `react-dom/server`에서 내보내져 서버에서의 스트리밍 서스펜스를 완전히 지원합니다.** 14 | 15 | - `renderToPipeableStream`: 노드 환경에서 스트리밍을 위한 것입니다. 16 | 17 | - `renderToReadableStream`: Deno 및 Cloudflare 작업자와 같은 최신 에지 런타임 환경용. 18 | -------------------------------------------------------------------------------- /docs/posts/startTransition.md: -------------------------------------------------------------------------------- 1 | # Transition 2 | 3 | 해당 기능은 상태 업데이트를 할 때 우선순위를 정하는데에 있어서 도움을 줍니다. 대표적으로 상태 업데이트는 크게 두 가지로 나뉘게되며 이는 다음과 같습니다. 4 | 5 | - **Urgent Update** : `긴급한 업데이트`이며, 즉각적으로 상태가 변하는 것을 기대하는 업데이트입니다. 6 | 7 | - **Transition Update** : `긴급하지 않은 업데이트`이며, 변화에 따른 모든 업데이트가 뷰에 즉각적으로 일어나는 것을 기대하지 않습니다. 8 | 9 | 키보드 입력과 같이 빈번히 일어나는 이벤트에 큰 화면이 업데이트가 되어야 한다면 렌더링에 큰 지장이 있을 것이고 이는 사용자의 입장에서 봤을 때 매우 좋지못한 UX로 남을 수 있습니다. 그 예시로 입력 폼과 결과 참이 있는데, 입력창은 네이티브 이벤트를 발생하는 컴포넌트이므로 즉각적인 업데이트가 요구되지만, 그에 반해 결과창은 입력창보다 업데이트가 느린 것에 대해 자연스럽게 받아들이게 됩니다. 10 | 11 | 보통의 경우, 해당 기능을 다음과 같은 코드로 구현을 할 것입니다. 12 | 13 | ```js 14 | // Urgent: Show what was typed 15 | setInputValue(input); 16 | 17 | // Not urgent: Show the results 18 | setSearchQuery(input); 19 | ``` 20 | 21 | 하지만 리액트는 해당 업데이트를 동일한 우선순위에서 처리하기 때문에 렌더링하는 과정이 더욱 느려지게 됩니다. 22 | 23 | 리액트 18은 `startTransition`이라는 기능을 제공하여, 상태 업데이트에 대한 우선순위를 정해줄 수 있습니다. 24 | 25 | ```js 26 | import { startTransition } from "react"; 27 | 28 | // Urgent: Show what was typed 29 | setInputValue(input); 30 | 31 | // Mark any state updates inside as transitions 32 | startTransition(() => { 33 | // Transition: Show the results 34 | setSearchQuery(input); 35 | }); 36 | ``` 37 | 38 | 해당 함수의 콜백으로 상태 업데이트를 진행하게 되면 긴급 업데이트가 진행된 후 다 끝난 뒤에 업데이트만 발생하게 됩니다. Transition을 이용하여 효율적인 렌더링이 가능하게 되었습니다. 또한, `useTransition` 훅을 사용하여 백그라운드에서 진행되고 있음을 알려주고 싶을때 유용하게 사용할 수 있습니다. 39 | 40 | ```js 41 | import { useTransition } from 'react'; 42 | 43 | const [isPending, startTransition] = useTransition(); 44 | ... 45 | {isPending && } 46 | ... 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/posts/strictMode.md: -------------------------------------------------------------------------------- 1 | # 엄격 모드 (Strict Mode)에서 새로운 Effect가 추가되었습니다. 2 | 3 | `StrictMode`는 응용 프로그램의 잠재적인 문제를 강조 표시하는 도구입니다. 4 | `StrictMode`는 UI를 렌더링하지 않고하위 항목에 대한 추가 검사 및 경고를 활성화합니다. 5 | 6 | React18이 릴리스됨과 동시에 `StrictMode`는 `Strict Effects` 모드라고 불리는 추가 동작을 할 수 있게 되었습니다. 7 | `Strict Effects`이 활성화 되어있는 경우에는 React개발 모드에서 새로 마운트된 컴포넌트에 대해 이중 Effect를 호출합니다. `(mount -> unmount -> mount)` 8 | 9 | 이러한 효과를 추가한 이유는 컴포넌트를 여러 번 탄력적으로 마운트 및 언마운트되어야 하는 때가 있습니다. 10 | 11 | 예를 들어서 화면에서 이동한 후, 뒤로가기를 하게되면 React는 이전 화면을 즉시 표시해야합니다. 이를 위해서 React는 이전과 동일한 구성 요소 상태를 사용하여 트리를 마운트 해제하고 다시 마운트합니다. 다시 로드하는 사이에 구성요소를 정확하게 유지하여서 우수한 개발 경험을 제공할 수 있습니다. 12 | 13 | 이 기능을 사용하면 React앱에 더 나은 성능을 제공하지만, 여러 번 마운트되고 파괴되는 효과에 대해서 탄력적이게 동작해야 합니다. 이를 위해서 React18은 `StrictMode`에 대한 새로운 개발 전용 체크를 도입했습니다. 이 검사는 구성 요소가 처음 마운트될 때마다 모든 구성 요소를 자동으로 언마운트하고 두 번째 마운트의 이전 상태를 복원합니다. 14 | 15 | Strict Mode 변경 전에는 React 컴포넌트를 마운트하여 다음과 같은 Effect를 생성했습니다. 16 | 17 | ``` 18 | * React mounts the component. 19 | * Layout effects are created. 20 | * Effects are created. 21 | ``` 22 | 23 | React18에서는 개발 모드에서 컴포넌트의 마운트 해제 및 재마운트를 시뮬레이션 합니다. 24 | 25 | ``` 26 | * React mounts the component. 27 | * Layout effects are created. 28 | * Effects are created. 29 | * React simulates unmounting the component. 30 | * Layout effects are destroyed. 31 | * Effects are destroyed. 32 | * React simulates mounting the component with the previous state. 33 | * Layout effects are created. 34 | * Effects are created. 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/posts/suspense.md: -------------------------------------------------------------------------------- 1 | ## Suspense로 선언적 프로그래밍을 할 수 있습니다. 2 | 3 | 기존에는 컴포넌트를 렌더링 하기위해 준비해야하는 상태이면 로딩상태를 따로 구현해서 반복하는 작업을 했었어야 했습니다. 하지만 React팀에서 Suspense라는 기능을 제공하면서 이 반복되는 작업을 최소화하고 선언형 프로그래밍을 할 수있게 되었습니다. 4 | 5 | 따로 컴포넌트를 렌더링하는데 시간이 필요한 컴포넌트를 Suspense로 감싸면 fallback으로 전달한 컴포넌트를 준비단계에 보여지게 됩니다. 코드는 아래와 같습니다. 6 | 7 | ```js 8 | }> 9 | 10 | 11 | ``` 12 | 13 | ### 새로운 기능 : Suspense가 SSR(Server-Side Rendering)를 지원합니다. 14 | 15 | 기본적으로 SSR은 다음과 같은 FLOW로 진행됩니다. 16 | 17 | 1. 서버에서 앱 전체를 위한 데이터를 불러온다. 18 | 2. 서버세어 HTML로 렌더링 한 다음 이를 응답으로 돌려준다. 19 | 3. 클라이언트에서 JS코드를 실행한다. 20 | 4. 클라이언트에서 서버에서 만들어진 HTML과 JS를 결합한다. (HYDRATION 이라고 함) 21 | 22 | Suspense는 이 페이지 단위로 진행되는 SSR을 서로 독립적으로 실행하고 서로 차단하지 않는 더 작은 독립 장치로 프로그램을 나눌 수 있게됩니다. 23 | 24 | ![](https://blog.kakaocdn.net/dn/p1adX/btq7GA1y9jr/DMA51F4wBrYHM5CTiga8bk/img.png) 25 | 위 사진 자료를 보면, 기존 SSR은 1,2,3,4 **리소스가 모두 다 로드**되어야만 합니다. 사용자 측면에서는 긴 기다림일 뿐더러 비효율적입니다. 26 | 그래서 리액트 팀은 Code Splitting으로 문제 해결을 시도했습니다. 27 | 하지만 이 `React.lazy`와 `Suspense`는 SSR에서는 지원이 되지 않았지만 28 | **React 18부터는 SSR에서 코드 스플리팅이 가능해졌습니다.** 29 | -------------------------------------------------------------------------------- /docs/posts/welcome.md: -------------------------------------------------------------------------------- 1 | ![](https://d585tldpucybw.cloudfront.net/sfimages/default-source/blogs/templates/social/reactt-light_1200x628.png?sfvrsn=43eb5f2a_2) 2 | 3 | # React 18 변경점 알아보기 4 | 5 | 이번 V18 업데이트는 효율적인 렌더링에 중점을 둔 업데이트입니다 Suspense, Trasition, Automatic Batch등 새로운 기능들이 추가됨에 있어서 6 | 개발자로 하여금 좀 더 쾌적한 React 개발환경을 제공합니다. 7 | 8 | ### React V18의 [새로운 기능](https://reactjs.org/blog/2022/03/29/react-v18.html) 알아보기 9 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hong-JunHyeok/create-react18-boilerplate/ad8f00a4df09c25d93705e6dc56060c805803f3c/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React 18 BoilerPlate 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Routes, Route } from "react-router-dom"; 3 | import "./style.css"; 4 | 5 | import Layout from "./Components/Layout"; 6 | import NavBar from "./Components/NavBar"; 7 | import SideBar from "./Components/SideBar"; 8 | import Content from "./Components/Content"; 9 | import Posts from "./Components/Contents"; 10 | 11 | class App extends React.Component { 12 | render() { 13 | return ( 14 | Loading...}> 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } 29 | /> 30 | } 33 | /> 34 | } /> 35 | } 38 | /> 39 | } /> 40 | Not Found} /> 41 | 42 | 43 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /docs/src/Components/Content.jsx: -------------------------------------------------------------------------------- 1 | const Content = ({ children }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default Content; 6 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/BatchContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { flushSync } from "react-dom"; 3 | import md from "../../../posts/batch.md"; 4 | 5 | const modes = { BATCH: "BATCH", FLUSH: "FLUSH" }; 6 | 7 | const BatchContent = () => { 8 | const [mode, setMode] = React.useState(modes.BATCH); 9 | const [number, setNumber] = React.useState(0); 10 | const [flag, setFlag] = React.useState(false); 11 | 12 | function handleChangeMode() { 13 | if (mode === modes.BATCH) { 14 | setMode(modes.FLUSH); 15 | } else { 16 | setMode(modes.BATCH); 17 | } 18 | } 19 | 20 | React.useEffect(() => { 21 | if (mode === "BATCH") { 22 | const timeoutId = setTimeout(() => { 23 | // NOTE 24 | // eact will only re-render once at the end 25 | setNumber((c) => c + 1); 26 | setFlag((f) => !f); 27 | }, 1000); 28 | 29 | return () => clearTimeout(timeoutId); 30 | } else if (mode === "FLUSH") { 31 | const timeoutId = setTimeout(() => { 32 | // NOTE 33 | // React will render twice, once for each state update 34 | flushSync(() => { 35 | setNumber((c) => c + 1); 36 | setFlag((f) => !f); 37 | }); 38 | }, 1000); 39 | 40 | return () => clearTimeout(timeoutId); 41 | } else { 42 | throw new Error(`Unhandled mode type (${mode})`); 43 | } 44 | }); 45 | 46 | return ( 47 | <> 48 |

자동 일괄처리

49 |

50 | 이벤트 핸들러 내에서만 일괄처리(Batch)되던 setState가 이제는 모든 51 | 코드에서 적용됩니다. 52 |

53 |
54 |
55 |
Number
56 |
Flag
57 |
58 |
59 |
{number}
60 |
{flag ? "True" : "False"}
61 |
62 |
63 | 66 |

67 | 위 예제의 코드는 68 | 72 | 여기 73 | 74 | 를 참고해주세요. 75 |

76 |
81 | 82 | ); 83 | }; 84 | 85 | export default BatchContent; 86 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/HooksContent.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/hooks.md"; 2 | 3 | const HooksContent = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default HooksContent; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/InstallationContent.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/installation.md"; 2 | 3 | const InstallationContent = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default InstallationContent; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/RenderingAPIContent.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/renderingAPI.md"; 2 | 3 | const Transition = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default Transition; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/StrictModeContent.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/strictMode.md"; 2 | 3 | const StrictModeContent = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default StrictModeContent; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/SuspenseContent.jsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import Fake from "../Fake"; 4 | import ErrorBoundary from "../ErrorBoundary"; 5 | import SectionSpinner from "../Loader/SectionSpinner"; 6 | import md from "../../../posts/suspense.md"; 7 | 8 | const SuspenseContent = () => { 9 | const navigate = useNavigate(); 10 | 11 | return ( 12 | <> 13 |

개선된 기능 : Suspense

14 | 15 |

16 | 데이터를 가져오기 위한 Suspense는 <Suspense>를 사용하여 선언적으로 17 | 데이터를 비롯한 무엇이든 “기다릴” 수 있도록 해주는 새로운 기능입니다. 이 18 | 기능은 이미지, 스크립트, 그 밖의 비동기 작업을 기다리는 데에도 사용될 수 19 | 있습니다. 20 |

21 | 22 | }> 23 | 24 | 25 | 26 | 29 | 30 | 31 |

32 | 위 예제의 코드는 33 | 37 | 여기 38 | 39 | 를 참고해주세요. 40 |

41 | 42 |
47 | 48 | ); 49 | }; 50 | 51 | export default SuspenseContent; 52 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/Transition.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/startTransition.md"; 2 | 3 | const Transition = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default Transition; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/Welcome.jsx: -------------------------------------------------------------------------------- 1 | import md from "../../../posts/welcome.md"; 2 | 3 | const Welcome = () => { 4 | return ( 5 | <> 6 |
11 | 12 | ); 13 | }; 14 | 15 | export default Welcome; 16 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/WorkingContent.jsx: -------------------------------------------------------------------------------- 1 | const Working = () => { 2 | return ( 3 | <> 4 |

현재 이 포스트는 작업중입니다.

5 |

빠른 시일 내에 업데이트 할 수 있도록 하겠습니다.

6 | 7 | ); 8 | }; 9 | 10 | export default Working; 11 | -------------------------------------------------------------------------------- /docs/src/Components/Contents/index.jsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | 3 | const Transition = lazy(() => import("./Transition")); 4 | const BatchContent = lazy(() => import("./BatchContent")); 5 | const SuspenseContent = lazy(() => import("./SuspenseContent")); 6 | const WorkingContent = lazy(() => import("./WorkingContent")); 7 | const RenderingAPIContent = lazy(() => import("./RenderingAPIContent")); 8 | const StrictModeContent = lazy(() => import("./StrictModeContent")); 9 | const HooksContent = lazy(() => import("./HooksContent")); 10 | const Welcome = lazy(() => import("./Welcome")); 11 | const InstallationContent = lazy(() => import("./InstallationContent")); 12 | 13 | export default { 14 | Transition, 15 | BatchContent, 16 | SuspenseContent, 17 | WorkingContent, 18 | RenderingAPIContent, 19 | StrictModeContent, 20 | HooksContent, 21 | Welcome, 22 | InstallationContent, 23 | }; 24 | -------------------------------------------------------------------------------- /docs/src/Components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | return { hasError: true }; 11 | } 12 | 13 | render() { 14 | if (this.state.hasError) { 15 | return

컴포넌트 렌더링 실패

; 16 | } 17 | 18 | return this.props.children; 19 | } 20 | } 21 | 22 | export default ErrorBoundary; 23 | -------------------------------------------------------------------------------- /docs/src/Components/Fake.jsx: -------------------------------------------------------------------------------- 1 | import { fetchData } from "../Utils/Api"; 2 | 3 | const resource = fetchData(); 4 | const Fake = () => { 5 | resource.fake.read(); 6 | 7 | return ( 8 | <> 9 |

10 | 컴포넌트가 성공적으로 렌더링 되었습니다. 11 |

12 | 13 | ); 14 | }; 15 | 16 | export default Fake; 17 | -------------------------------------------------------------------------------- /docs/src/Components/Layout.jsx: -------------------------------------------------------------------------------- 1 | const Layout = ({ children }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /docs/src/Components/Loader/PageSpinner.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hong-JunHyeok/create-react18-boilerplate/ad8f00a4df09c25d93705e6dc56060c805803f3c/docs/src/Components/Loader/PageSpinner.jsx -------------------------------------------------------------------------------- /docs/src/Components/Loader/SectionSpinner.jsx: -------------------------------------------------------------------------------- 1 | import ReactIcon from "../../../assets/react-logo.svg"; 2 | import "./loader.css"; 3 | 4 | const SectionSpinner = () => { 5 | return ( 6 |
7 | 8 |

LOADING ...

9 |
10 | ); 11 | }; 12 | 13 | export default SectionSpinner; 14 | -------------------------------------------------------------------------------- /docs/src/Components/Loader/loader.css: -------------------------------------------------------------------------------- 1 | .loader-icon { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | 8 | .loader-icon > svg { 9 | animation: rotate_image 4s linear infinite; 10 | transform-origin: 50% 50%; 11 | } 12 | .loader-icon > p { 13 | letter-spacing: 10px; 14 | font-weight: bold; 15 | } 16 | 17 | @keyframes rotate_image { 18 | 100% { 19 | transform: rotate(360deg); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/src/Components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import ReactIcon from "../../assets/react-logo.svg"; 3 | 4 | const NavBar = () => { 5 | return ( 6 |
7 | 8 | 9 |

10 | 11 | React 18 BoilerPlate 12 | 13 |

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default NavBar; 20 | -------------------------------------------------------------------------------- /docs/src/Components/NavItem.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | 3 | const NavItem = ({ to, children }) => { 4 | return ( 5 | 8 | isActive ? `nav_link activated link` : `nav_link link` 9 | } 10 | > 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default NavItem; 17 | -------------------------------------------------------------------------------- /docs/src/Components/Post.jsx: -------------------------------------------------------------------------------- 1 | import { fetchData } from "../Utils/Api"; 2 | 3 | const resource = fetchData(); 4 | 5 | const Post = () => { 6 | const posts = resource.post.read(); 7 | 8 | const mapPosts = posts.map((post) => ( 9 |
10 |

{post.title}

11 |

{post.body}

12 |
13 | )); 14 | 15 | return <>{mapPosts}; 16 | }; 17 | 18 | export default Post; 19 | -------------------------------------------------------------------------------- /docs/src/Components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import NavItem from "./NavItem"; 2 | 3 | const SideBar = () => { 4 | return ( 5 |
6 |
    7 |
  • 8 | 시작하기 9 |
  • 10 |
  • 11 | 어떻게 시작하나요? 12 |
  • 13 |
  • 14 | Automatic Batch 15 |
  • 16 |
  • 17 | Suspense 18 |
  • 19 |
  • 20 | Transition 21 |
  • 22 |
  • 23 | 새로운 클라이언트 및 서버 렌더링 API 24 |
  • 25 |
  • 26 | 새로운 Strict Mode동작 27 |
  • 28 |
  • 29 | 새로운 Hooks 30 |
  • 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default SideBar; 37 | -------------------------------------------------------------------------------- /docs/src/Components/User.jsx: -------------------------------------------------------------------------------- 1 | import { fetchData } from "../Utils/Api"; 2 | 3 | const resource = fetchData(); 4 | 5 | const User = () => { 6 | const users = resource.user.read(); 7 | 8 | const mapUsers = users.map((user) => ( 9 |
10 |

11 | {user.name}({user.email}) 12 |

13 |

{user.body}

14 |
15 | )); 16 | 17 | return <>{mapUsers}; 18 | }; 19 | 20 | export default User; 21 | -------------------------------------------------------------------------------- /docs/src/Utils/Api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const fetchData = () => { 4 | const userPromise = fetchUser(); 5 | const postPromise = fetchPosts(); 6 | const fakePromise = fetchFake(); 7 | 8 | return { 9 | user: wrapPromise(userPromise), 10 | post: wrapPromise(postPromise), 11 | fake: wrapPromise(fakePromise), 12 | }; 13 | }; 14 | 15 | const wrapPromise = (promise) => { 16 | let status = "pending"; 17 | 18 | let result; 19 | 20 | let suspender = promise.then( 21 | (response) => { 22 | status = "success"; 23 | result = response; 24 | }, 25 | (error) => { 26 | status = "error"; 27 | result = error; 28 | } 29 | ); 30 | 31 | return { 32 | read() { 33 | console.log(status); 34 | if (status === "pending") { 35 | throw suspender; 36 | } else if (status === "success") { 37 | return result; 38 | } else if (status === "error") { 39 | throw result; 40 | } 41 | }, 42 | }; 43 | }; 44 | 45 | const fetchUser = () => { 46 | return axios 47 | .get("https://jsonplaceholder.typicode.com/users") 48 | .then((response) => response.data) 49 | .catch(console.error); 50 | }; 51 | 52 | const fetchPosts = () => { 53 | return axios 54 | .get("https://jsonplaceholder.typicode.com/posts") 55 | .then((response) => response.data) 56 | .catch(console.error); 57 | }; 58 | 59 | const fetchFake = () => { 60 | return new Promise((resolve, reject) => { 61 | setTimeout(() => { 62 | if (Math.round(Math.random()) === 0) resolve(true); 63 | else return reject(false); 64 | }, 1500); 65 | }).then((response) => response); 66 | }; 67 | -------------------------------------------------------------------------------- /docs/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { HashRouter } from "react-router-dom"; 3 | import App from "./App"; 4 | 5 | const container = document.getElementById("root"); 6 | const root = createRoot(container); 7 | 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /docs/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-bg-color: #20232a; 3 | --main-pr-color: #61dafb; 4 | --main-bg-gray: #f2f2f2; 5 | 6 | --content-text-color: #282c34; 7 | --content-size: 800px; 8 | --content-size-sm: 500px; 9 | --sidebar-size: 400px; 10 | 11 | --content-font-size-small: 12px; 12 | --content-font-size-middle: 16px; 13 | --content-font-size-big: 24px; 14 | } 15 | 16 | html, 17 | body { 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | a:not(.link) { 24 | background-color: rgba(187, 239, 253, 0.3); 25 | border-bottom: 1px solid rgba(39, 36, 36, 0.2); 26 | color: #1a1a1a; 27 | } 28 | 29 | * > code { 30 | padding: 0 3px; 31 | font-size: 0.94em; 32 | word-break: break-word; 33 | background: rgba(255, 229, 100, 0.2); 34 | color: #1a1a1a; 35 | } 36 | 37 | pre { 38 | padding: 1rem; 39 | } 40 | 41 | img { 42 | width: 100%; 43 | } 44 | 45 | h1 { 46 | margin: 0.67em 0; 47 | color: #282c34; 48 | margin-bottom: 0; 49 | font-size: 60px; 50 | line-height: 65px; 51 | font-weight: 700; 52 | } 53 | 54 | h2 { 55 | font-size: 26px; 56 | font-weight: 300; 57 | color: #6d6d6d; 58 | } 59 | 60 | p { 61 | margin-top: 30px; 62 | font-size: 17px; 63 | line-height: 1.7; 64 | max-width: 42em; 65 | } 66 | 67 | h3 { 68 | display: block; 69 | font-size: 1.5em; 70 | margin-block-start: 0.83em; 71 | margin-block-end: 0.83em; 72 | margin-inline-start: 0px; 73 | margin-inline-end: 0px; 74 | font-weight: bold; 75 | } 76 | 77 | pre > code, 78 | pre { 79 | background: rgb(40, 44, 52); 80 | color: rgb(255, 255, 255); 81 | border-radius: 10px; 82 | overflow: auto; 83 | tab-size: 1.5em; 84 | font-size: 1rem; 85 | } 86 | 87 | blockquote { 88 | background-color: rgba(255, 229, 100, 0.3); 89 | border-left-color: #ffe564; 90 | border-left-width: 9px; 91 | border-left-style: solid; 92 | padding: 20px 45px 20px 26px; 93 | margin-bottom: 30px; 94 | margin-top: 20px; 95 | margin-left: -30px; 96 | margin-right: -30px; 97 | } 98 | 99 | .title { 100 | margin: 0; 101 | height: 100%; 102 | display: flex; 103 | align-items: center; 104 | cursor: pointer; 105 | user-select: none; 106 | } 107 | 108 | .title > h1 { 109 | font-size: 2rem; 110 | padding: 0px; 111 | margin: 0px; 112 | } 113 | .title > h1 > a { 114 | margin-left: 1rem; 115 | text-decoration: none; 116 | color: var(--main-pr-color); 117 | } 118 | 119 | .title:hover { 120 | color: white; 121 | } 122 | 123 | .main { 124 | margin: 0 auto; 125 | padding-top: 5rem; 126 | display: flex; 127 | justify-content: center; 128 | } 129 | 130 | .layout { 131 | width: 100%; 132 | min-height: 100vh; 133 | } 134 | 135 | .nav_bar { 136 | position: fixed; 137 | width: 100%; 138 | height: 4rem; 139 | padding: 0.5rem 1rem; 140 | background-color: var(--main-bg-color); 141 | z-index: 999; 142 | } 143 | 144 | .nav_bar > .title { 145 | margin: 0 auto; 146 | } 147 | 148 | .side_bar { 149 | min-height: 100vh; 150 | min-width: var(--sidebar-size); 151 | background-color: var(--main-bg-gray); 152 | position: fixed; 153 | left: 0; 154 | } 155 | 156 | .side_bar > ul { 157 | list-style: none; 158 | } 159 | 160 | .nav_link { 161 | text-decoration: none; 162 | color: var(--content-text-color); 163 | font-size: var(--content-font-size-middle); 164 | line-height: 30px; 165 | } 166 | 167 | .activated { 168 | font-weight: bold; 169 | } 170 | 171 | .activated::before { 172 | content: ""; 173 | width: 4px; 174 | height: 24px; 175 | border-right: 4px solid var(--main-pr-color); 176 | padding-left: 16px; 177 | position: absolute; 178 | left: 0; 179 | } 180 | 181 | .box-container { 182 | border-radius: 5px; 183 | overflow: hidden; 184 | box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; 185 | } 186 | 187 | .head { 188 | padding: 1rem 0.3rem; 189 | display: flex; 190 | background-color: var(--main-bg-color); 191 | color: var(--main-pr-color); 192 | font-size: var(--content-font-size-big); 193 | font-weight: bold; 194 | } 195 | 196 | .head > .name { 197 | flex: 1; 198 | padding: 0 1rem; 199 | } 200 | 201 | .body { 202 | display: flex; 203 | background-color: var(--main-bg-gray); 204 | font-size: var(--content-font-size-middle); 205 | } 206 | 207 | .body > .value { 208 | padding: 1rem 1rem; 209 | flex: 1; 210 | } 211 | 212 | .btn { 213 | border: none; 214 | color: var(--content-text-color); 215 | padding: 0.6rem 0.8rem; 216 | display: inline-block; 217 | font-size: 16px; 218 | background-color: rgb(97, 218, 251); 219 | padding: 10px 25px; 220 | white-space: nowrap; 221 | transition: background-color 0.2s ease-out 0s; 222 | cursor: pointer; 223 | } 224 | 225 | .change { 226 | margin-top: 1rem; 227 | } 228 | 229 | .content { 230 | flex-wrap: wrap; 231 | word-break: break-all; 232 | width: var(--content-size); 233 | min-width: var(--content-size); 234 | margin: 0 auto; 235 | padding: 1rem; 236 | overflow-y: auto; 237 | } 238 | 239 | .suspense_block { 240 | border-left-width: 9px; 241 | border-left-style: solid; 242 | padding: 20px 45px 20px 26px; 243 | margin-bottom: 30px; 244 | margin-top: 20px; 245 | margin-left: -30px; 246 | margin-right: -30px; 247 | } 248 | 249 | .warn { 250 | background-color: rgba(255, 229, 100, 0.3); 251 | border-left-color: #ffe564; 252 | } 253 | .success { 254 | background-color: rgba(187, 239, 253, 0.3); 255 | border-left-color: 1px solid rgba(0, 0, 0, 0.2); 256 | } 257 | 258 | @media (max-width: 1600px) { 259 | .main { 260 | display: flex; 261 | flex-direction: column-reverse; 262 | justify-content: start; 263 | } 264 | 265 | .side_bar { 266 | position: relative; 267 | width: 100%; 268 | min-height: 0; 269 | height: auto; 270 | background-color: var(--main-bg-gray); 271 | overflow-y: auto; 272 | } 273 | 274 | .content { 275 | min-width: 80%; 276 | min-width: 0; 277 | min-height: 80vh; 278 | margin: 0 auto; 279 | padding: 1rem; 280 | overflow-y: auto; 281 | display: flex; 282 | align-items: flex-start; 283 | justify-content: start; 284 | flex-direction: column; 285 | } 286 | 287 | .post_title { 288 | margin: 0.67em 0; 289 | color: #282c34; 290 | margin-bottom: 0; 291 | font-size: 3em; 292 | font-weight: 700; 293 | } 294 | 295 | .post_subtitle { 296 | font-size: 18px; 297 | font-weight: 300; 298 | color: #6d6d6d; 299 | } 300 | 301 | .box-container { 302 | width: 100%; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import HTMLWebpackPlugin from "html-webpack-plugin"; 3 | import RemarkHTML from "remark-html"; 4 | import RemarkFrontmatter from "remark-frontmatter"; 5 | 6 | export default { 7 | name: "React-18_Boiler-Plate", 8 | mode: "development", 9 | entry: "./src/index.jsx", 10 | output: { 11 | filename: "bundle.[hash].js", 12 | path: path.resolve("dist"), 13 | publicPath: "/", 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | exclude: /node_modules/, 20 | use: "babel-loader", 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: ["style-loader", "css-loader"], 25 | }, 26 | { 27 | test: /\.(png)$/, 28 | use: [ 29 | { 30 | loader: "file-loader", 31 | options: { 32 | name: "images/[name].[ext]?[hash]", 33 | }, 34 | }, 35 | ], 36 | }, 37 | { 38 | test: /\.svg$/, 39 | use: ["@svgr/webpack"], 40 | }, 41 | { 42 | test: /\.md$/, 43 | use: [ 44 | { 45 | loader: "html-loader", 46 | }, 47 | { 48 | loader: "markdown-loader", 49 | options: { 50 | remarkOptions: { 51 | plugins: [RemarkFrontmatter, RemarkHTML], 52 | }, 53 | }, 54 | }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | resolve: { 60 | extensions: [".js", ".jsx"], 61 | }, 62 | plugins: [ 63 | new HTMLWebpackPlugin({ 64 | template: "./public/index.html", 65 | }), 66 | ], 67 | devServer: { 68 | static: { 69 | directory: path.join(path.resolve(), "public"), 70 | }, 71 | compress: true, 72 | port: 3080, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react18-boilerplate", 3 | "version": "1.0.4", 4 | "description": "Build the smallest React18 application.", 5 | "main": "src/index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "bin": { 10 | "create-react18-boilerplate": "bin/create-project", 11 | "create-project": "bin/create-project" 12 | }, 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "scripts": { 17 | "prepublish": "npm-auto-version", 18 | "postpublish": "git push origin --tags" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Hong-JunHyeok/react18-boilerplate.git" 23 | }, 24 | "keywords": [ 25 | "cli", 26 | "create-react18-boilerplate" 27 | ], 28 | "author": { 29 | "name": "Hong-JunHyeok", 30 | "email": "edb1631@naver.com", 31 | "url": "https://github.com/Hong-JunHyeok" 32 | }, 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/Hong-JunHyeok/react18-boilerplate/issues" 36 | }, 37 | "homepage": "https://github.com/Hong-JunHyeok/react18-boilerplate#readme", 38 | "files": [ 39 | "bin/", 40 | "src/", 41 | "templates/" 42 | ], 43 | "devDependencies": { 44 | "create-flex-plugin": "^4.3.4" 45 | }, 46 | "dependencies": { 47 | "arg": "^5.0.1", 48 | "chalk": "^2.4.1", 49 | "esm": "^3.2.25", 50 | "execa": "^6.1.0", 51 | "inquirer": "^8.2.2", 52 | "listr": "^0.14.3", 53 | "ncp": "^2.0.0", 54 | "npm-auto-version": "^1.0.0", 55 | "pkg-install": "^1.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React 18 BoilerPlate 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | import arg from "arg"; 2 | import inquirer from "inquirer"; 3 | import { createProject } from "./main"; 4 | 5 | function parseArgumentsIntoOptions(rawArgs) { 6 | const args = arg( 7 | { 8 | "--skip": Boolean, 9 | "--typescript": String, 10 | "--yarn": String, 11 | 12 | "-s": "--skip", 13 | "-t": "--typescript", 14 | "-y": "--yarn", 15 | }, 16 | { 17 | argv: rawArgs.slice(2), 18 | } 19 | ); 20 | 21 | return { 22 | skipPrompt: args["--skip"] || false, 23 | template: args._[0], 24 | packageManager: args["--yarn"] || args["-y"], 25 | }; 26 | } 27 | 28 | async function promptForMissingOptions(options) { 29 | const defaultTemplate = "JavaScript"; 30 | const defaultPackageManager = "npm"; 31 | 32 | if (options.skipPrompt) { 33 | return { 34 | ...options, 35 | template: options.template || defaultTemplate, 36 | }; 37 | } 38 | 39 | const questions = []; 40 | if (!options.template) { 41 | questions.push({ 42 | type: "list", 43 | name: "template", 44 | message: "Please choose which project template to use", 45 | choices: ["JavaScript", "TypeScript"], 46 | default: defaultTemplate, 47 | }); 48 | } 49 | 50 | if (!options.packageManager) { 51 | questions.push({ 52 | type: "list", 53 | name: "packageManager", 54 | message: "Which package manager do you prefer?", 55 | choices: ["NPM", "Yarn"], 56 | default: defaultPackageManager, 57 | }); 58 | } 59 | 60 | const answer = await inquirer.prompt(questions); 61 | return { 62 | ...options, 63 | template: options.template || answer.template, 64 | packageManager: options.packageManager || answer.packageManager, 65 | }; 66 | } 67 | 68 | export async function cli(args) { 69 | let options = parseArgumentsIntoOptions(args); 70 | options = await promptForMissingOptions(options); 71 | await createProject(options); 72 | } 73 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require = require("esm")(module); 2 | require("./cli").cli(process.argv); 3 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs"; 3 | import ncp from "ncp"; 4 | import path from "path"; 5 | import { promisify } from "util"; 6 | import Listr from "listr"; 7 | import { projectInstall } from "pkg-install"; 8 | 9 | const access = promisify(fs.access); 10 | const copy = promisify(ncp); 11 | 12 | async function copyTemplateFiles(options) { 13 | return copy(options.templateDirectory, options.targetDirectory, { 14 | clobber: false, 15 | }); 16 | } 17 | 18 | async function initGit(options) { 19 | const result = await execa("git", ["init"], { 20 | cwd: options.targetDirectory, 21 | }); 22 | if (result.failed) { 23 | return Promise.reject(new Error("Failed to initialize git")); 24 | } 25 | return; 26 | } 27 | 28 | export async function createProject(options) { 29 | options = { 30 | ...options, 31 | targetDirectory: options.targetDirectory || process.cwd(), 32 | }; 33 | 34 | const templateDir = path.resolve(__filename, "../../templates", options.template.toLowerCase()); 35 | options.templateDirectory = templateDir; 36 | 37 | try { 38 | await access(templateDir, fs.constants.R_OK); 39 | } catch (err) { 40 | console.error("%s Invalid template name", chalk.red.bold("ERROR")); 41 | process.exit(1); 42 | } 43 | const tasks = new Listr([ 44 | { 45 | title: "Copy Project Files", 46 | task: () => copyTemplateFiles(options), 47 | }, 48 | { 49 | title: "Install dependencies", 50 | task: () => 51 | projectInstall({ 52 | cwd: options.targetDirectory, 53 | prefer: options.packageManager.toLowerCase(), 54 | }), 55 | }, 56 | ]); 57 | 58 | await tasks.run(); 59 | 60 | options.packageManager.toLowerCase() === "npm" ? npmBuildScripts(options) : yarnBuildScripts(options); 61 | 62 | return true; 63 | } 64 | 65 | function npmBuildScripts(options) { 66 | console.log("%s Project ready", chalk.green.bold("DONE")); 67 | console.log(`Success! Created Project at ${options.targetDirectory}`); 68 | console.log(`${chalk.cyan("You can run several commands: ")}`); 69 | 70 | console.log(` 71 | ${chalk.cyan("npm run dev")} 72 | Starts the development server. 73 | 74 | ${chalk.cyan("npm run build")} 75 | Bundles the app into static files for production. 76 | `); 77 | } 78 | 79 | function yarnBuildScripts(options) { 80 | console.log("%s Project ready", chalk.green.bold("DONE")); 81 | console.log(`Success! Created Project at ${options.targetDirectory}`); 82 | console.log(`${chalk.cyan("You can run several commands: ")}`); 83 | 84 | console.log(` 85 | ${chalk.cyan("yarn dev")} 86 | Starts the development server. 87 | 88 | ${chalk.cyan("yarn run build")} 89 | Bundles the app into static files for production. 90 | `); 91 | } 92 | -------------------------------------------------------------------------------- /templates/javascript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]] 3 | } 4 | -------------------------------------------------------------------------------- /templates/javascript/.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/patches 3 | !/.yarn/plugins 4 | !/.yarn/releases 5 | !/.yarn/sdks 6 | 7 | # Swap the comments on the following lines if you don't wish to use zero-installs 8 | # Documentation here: https://yarnpkg.com/features/zero-installs 9 | !/.yarn/cache 10 | #/.pnp.* 11 | node_modules 12 | dist 13 | .yarn 14 | -------------------------------------------------------------------------------- /templates/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react18-boilerplate", 3 | "scripts": { 4 | "build": "webpack", 5 | "dev": "webpack serve --open" 6 | }, 7 | "dependencies": { 8 | "axios": "^0.26.1", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-router-dom": "^6.2.2" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.17.8", 15 | "@babel/plugin-proposal-class-properties": "^7.16.7", 16 | "@babel/preset-env": "^7.16.11", 17 | "@babel/preset-react": "^7.16.7", 18 | "@svgr/webpack": "^6.2.1", 19 | "babel-loader": "^8.2.4", 20 | "css-loader": "^6.7.1", 21 | "file-loader": "^6.2.0", 22 | "html-webpack-plugin": "^5.5.0", 23 | "style-loader": "^3.3.1", 24 | "webpack": "^5.70.0", 25 | "webpack-cli": "^4.9.2", 26 | "webpack-dev-server": "^4.7.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/javascript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React 18 BoilerPlate 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/javascript/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class App extends React.Component { 4 | render() { 5 | return <>React 18 BoilerPlate; 6 | } 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /templates/javascript/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | 4 | const container = document.getElementById("root"); 5 | const root = createRoot(container); 6 | 7 | root.render(); 8 | -------------------------------------------------------------------------------- /templates/javascript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HTMLWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | name: "React-18_Boiler-Plate", 6 | mode: "development", 7 | entry: "./src/index.jsx", 8 | output: { 9 | filename: "bundle.[chunkhash].js", 10 | path: path.resolve("dist"), 11 | publicPath: "/", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | use: "babel-loader", 19 | }, 20 | { 21 | test: /\.css$/, 22 | use: ["style-loader", "css-loader"], 23 | }, 24 | { 25 | test: /\.(png)$/, 26 | use: [ 27 | { 28 | loader: "file-loader", 29 | options: { 30 | name: "images/[name].[ext]?[chunkhash]", 31 | }, 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.svg$/, 37 | use: ["@svgr/webpack"], 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | extensions: [".js", ".jsx"], 43 | }, 44 | plugins: [ 45 | new HTMLWebpackPlugin({ 46 | template: "./public/index.html", 47 | }), 48 | ], 49 | devServer: { 50 | static: { 51 | directory: path.join(__dirname, "public"), 52 | }, 53 | compress: true, 54 | port: 3080, 55 | open: true, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /templates/typescript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "browsers": ["last 2 versions", ">= 5% in KR"] } }], 4 | "@babel/react", 5 | "@babel/typescript" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /templates/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | /.yarn/* 2 | !/.yarn/patches 3 | !/.yarn/plugins 4 | !/.yarn/releases 5 | !/.yarn/sdks 6 | 7 | # Swap the comments on the following lines if you don't wish to use zero-installs 8 | # Documentation here: https://yarnpkg.com/features/zero-installs 9 | !/.yarn/cache 10 | #/.pnp.* 11 | node_modules 12 | dist 13 | .yarn 14 | -------------------------------------------------------------------------------- /templates/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react18-boilerplate", 3 | "scripts": { 4 | "build": "webpack", 5 | "dev": "webpack serve --open" 6 | }, 7 | "dependencies": { 8 | "axios": "^0.26.1", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-router-dom": "^6.2.2" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.17.8", 15 | "@babel/plugin-proposal-class-properties": "^7.16.7", 16 | "@babel/preset-env": "^7.16.11", 17 | "@babel/preset-react": "^7.16.7", 18 | "@babel/preset-typescript": "^7.16.7", 19 | "@svgr/webpack": "^6.2.1", 20 | "@types/react": "^17.0.43", 21 | "@types/react-dom": "^17.0.14", 22 | "babel-loader": "^8.2.4", 23 | "css-loader": "^6.7.1", 24 | "file-loader": "^6.2.0", 25 | "fork-ts-checker-webpack-plugin": "^7.2.1", 26 | "html-webpack-plugin": "^5.5.0", 27 | "style-loader": "^3.3.1", 28 | "ts-loader": "^9.2.8", 29 | "typescript": "^4.6.3", 30 | "webpack": "^5.70.0", 31 | "webpack-cli": "^4.9.2", 32 | "webpack-dev-server": "^4.7.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React 18 BoilerPlate 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class App extends React.Component { 4 | render() { 5 | return <>React 18 BoilerPlate Typescript; 6 | } 7 | } 8 | 9 | export default App; 10 | -------------------------------------------------------------------------------- /templates/typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | 4 | const container = document.getElementById("root"); 5 | const root = createRoot(container); 6 | 7 | root.render(); 8 | -------------------------------------------------------------------------------- /templates/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noResolve": false, 7 | "noImplicitAny": false, 8 | "removeComments": false, 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "allowSyntheticDefaultImports": true, 13 | "keyofStringsOnly": true 14 | }, 15 | "typeRoots": ["node_modules/@types", "src/@type"], 16 | "exclude": [ 17 | "node_modules", 18 | "build", 19 | "scripts", 20 | "acceptance-tests", 21 | "webpack", 22 | "jest", 23 | "src/setupTests.ts", 24 | "./node_modules/**/*" 25 | ], 26 | "include": ["./src/**/*", "@type"] 27 | } 28 | -------------------------------------------------------------------------------- /templates/typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HTMLWebpackPlugin = require("html-webpack-plugin"); 3 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 4 | 5 | module.exports = { 6 | name: "React-18_Boiler-Plate", 7 | mode: "development", 8 | entry: "./src/index.tsx", 9 | output: { 10 | filename: "bundle.[chunkhash].js", 11 | path: path.resolve("dist"), 12 | publicPath: "/", 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(ts|tsx)$/, 18 | exclude: /node_modules/, 19 | use: [ 20 | "babel-loader", 21 | { 22 | loader: "ts-loader", 23 | options: { 24 | transpileOnly: true, 25 | }, 26 | }, 27 | ], 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: ["style-loader", "css-loader"], 32 | }, 33 | { 34 | test: /\.(png)$/, 35 | use: [ 36 | { 37 | loader: "file-loader", 38 | options: { 39 | name: "images/[name].[ext]?[chunkhash]", 40 | }, 41 | }, 42 | ], 43 | }, 44 | { 45 | test: /\.svg$/, 46 | use: ["@svgr/webpack"], 47 | }, 48 | ], 49 | }, 50 | resolve: { 51 | extensions: [".js", ".jsx", ".ts", ".tsx"], 52 | }, 53 | plugins: [ 54 | new HTMLWebpackPlugin({ 55 | template: "./public/index.html", 56 | }), 57 | new ForkTsCheckerWebpackPlugin(), 58 | ], 59 | devServer: { 60 | static: { 61 | directory: path.join(__dirname, "public"), 62 | }, 63 | compress: true, 64 | port: 3080, 65 | }, 66 | }; 67 | --------------------------------------------------------------------------------