├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── chapter_14 │ ├── ThemeContext.jsx │ ├── DarkOrLight.jsx │ └── MainContent.jsx ├── setupTests.js ├── App.test.js ├── chapter_04 │ └── Clock.jsx ├── chapter_03 │ ├── Book.jsx │ └── Library.jsx ├── chapter_13 │ ├── ProfileCard.jsx │ └── Card.jsx ├── chapter_07 │ ├── useCounter.jsx │ └── Accommodate.jsx ├── index.css ├── reportWebVitals.js ├── chapter_08 │ └── ConfirmButton.jsx ├── chapter_12 │ ├── TemperatureInput.jsx │ └── Calculator.jsx ├── App.js ├── chapter_10 │ └── AttendanceBook.jsx ├── App.css ├── chapter_05 │ ├── CommentList.jsx │ └── Comment.jsx ├── chapter_09 │ ├── LandingPage.jsx │ └── Toolbar.jsx ├── chapter_06 │ ├── Notification.jsx │ └── NotificationList.jsx ├── chapter_11 │ └── SignUp.jsx ├── index.js ├── chapter_15 │ └── Blocks.jsx └── logo.svg ├── .gitignore ├── README.md └── package.json /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soaple/first-met-react-practice-v18/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soaple/first-met-react-practice-v18/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soaple/first-met-react-practice-v18/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/chapter_14/ThemeContext.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ThemeContext = React.createContext(); 4 | ThemeContext.displayName = "ThemeContext"; 5 | 6 | export default ThemeContext; 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/chapter_04/Clock.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Clock(props) { 4 | return ( 5 |
6 |

안녕, 리액트!

7 |

현재 시간: {new Date().toLocaleTimeString()}

8 |
9 | ); 10 | } 11 | 12 | export default Clock; 13 | -------------------------------------------------------------------------------- /src/chapter_03/Book.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Book(props) { 4 | return ( 5 |
6 |

{`이 책의 이름은 ${props.name}입니다.`}

7 |

{`이 책은 총 ${props.numOfPage}페이지로 이뤄져 있습니다.`}

8 |
9 | ); 10 | } 11 | 12 | export default Book; 13 | -------------------------------------------------------------------------------- /src/chapter_13/ProfileCard.jsx: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | 3 | function ProfileCard(props) { 4 | return ( 5 | 6 |

안녕하세요, 소플입니다.

7 |

저는 리액트를 사용해서 개발하고 있습니다.

8 |
9 | ); 10 | } 11 | 12 | export default ProfileCard; 13 | -------------------------------------------------------------------------------- /src/chapter_03/Library.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Book from "./Book"; 3 | 4 | function Library(props) { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default Library; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 소플의 처음 만난 리액트 실습 소스코드 (리액트 v18) 2 | 3 | 웹 애플리케이션을 개발모드로 실행합니다. 4 | ```bash 5 | $ npm start 6 | ``` 7 | 8 | 이후 [http://localhost:3000](http://localhost:3000)에서 접속 가능합니다. 9 | 10 | --- 11 | 12 | ## 🚨 현재 리액트 19 정식 릴리즈로 인해 `create-react-app`이 제대로 작동하지 않습니다. 13 | 14 | 아래 명령어를 참고해서 Yarn 설치하고, Yarn을 사용해서 리액트 앱을 생성하고 실습을 진행해주세요! 15 | 16 | ```bash 17 | npm install -g yarn 18 | yarn create react-app my-app 19 | ``` 20 | -------------------------------------------------------------------------------- /src/chapter_07/useCounter.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function useCounter(initialValue) { 4 | const [count, setCount] = useState(initialValue); 5 | 6 | const increaseCount = () => setCount((count) => count + 1); 7 | const decreaseCount = () => setCount((count) => Math.max(count - 1, 0)); 8 | 9 | return [count, increaseCount, decreaseCount]; 10 | } 11 | 12 | export default useCounter; 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/chapter_08/ConfirmButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function ConfirmButton(props) { 4 | const [isConfirmed, setIsConfirmed] = useState(false); 5 | 6 | const handleConfirm = () => { 7 | setIsConfirmed((prevIsConfirmed) => !prevIsConfirmed); 8 | }; 9 | 10 | return ( 11 | 14 | ); 15 | } 16 | 17 | export default ConfirmButton; 18 | -------------------------------------------------------------------------------- /src/chapter_13/Card.jsx: -------------------------------------------------------------------------------- 1 | function Card(props) { 2 | const { title, backgroundColor, children } = props; 3 | 4 | return ( 5 |
14 | {title &&

{title}

} 15 | {children} 16 |
17 | ); 18 | } 19 | 20 | export default Card; 21 | -------------------------------------------------------------------------------- /src/chapter_12/TemperatureInput.jsx: -------------------------------------------------------------------------------- 1 | const scaleNames = { 2 | c: "섭씨", 3 | f: "화씨", 4 | }; 5 | 6 | function TemperatureInput(props) { 7 | const handleChange = (event) => { 8 | props.onTemperatureChange(event.target.value); 9 | }; 10 | 11 | return ( 12 |
13 | 14 | 온도를 입력해주세요(단위:{scaleNames[props.scale]}): 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default TemperatureInput; 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import logo from './logo.svg'; 2 | import './App.css'; 3 | 4 | function App() { 5 | return ( 6 |
7 |
8 | logo 9 |

10 | Edit src/App.js and save to reload. 11 |

12 | 18 | Learn React 19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/chapter_10/AttendanceBook.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const students = [ 4 | { 5 | id: 1, 6 | name: "Inje", 7 | }, 8 | { 9 | id: 2, 10 | name: "Steve", 11 | }, 12 | { 13 | id: 3, 14 | name: "Bill", 15 | }, 16 | { 17 | id: 4, 18 | name: "Jeff", 19 | }, 20 | ]; 21 | 22 | function AttendanceBook(props) { 23 | return ( 24 | 29 | ); 30 | } 31 | 32 | export default AttendanceBook; 33 | -------------------------------------------------------------------------------- /src/chapter_14/DarkOrLight.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import ThemeContext from "./ThemeContext"; 3 | import MainContent from "./MainContent"; 4 | 5 | function DarkOrLight(props) { 6 | const [theme, setTheme] = useState("light"); 7 | 8 | const toggleTheme = useCallback(() => { 9 | if (theme == "light") { 10 | setTheme("dark"); 11 | } else if (theme == "dark") { 12 | setTheme("light"); 13 | } 14 | }, [theme]); 15 | 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default DarkOrLight; 24 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/chapter_05/CommentList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Comment from "./Comment"; 3 | 4 | const comments = [ 5 | { 6 | name: "이인제", 7 | comment: "안녕하세요, 소플입니다.", 8 | }, 9 | { 10 | name: "유재석", 11 | comment: "리액트 재미있어요~!", 12 | }, 13 | { 14 | name: "강민경", 15 | comment: "저도 리액트 배워보고 싶어요!!", 16 | }, 17 | ]; 18 | 19 | function CommentList(props) { 20 | return ( 21 |
22 | {comments.map((comment) => { 23 | return ( 24 | 25 | ); 26 | })} 27 |
28 | ); 29 | } 30 | 31 | export default CommentList; 32 | -------------------------------------------------------------------------------- /src/chapter_09/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Toolbar from "./Toolbar"; 3 | 4 | function LandingPage(props) { 5 | const [isLoggedIn, setIsLoggedIn] = useState(false); 6 | 7 | const onClickLogin = () => { 8 | setIsLoggedIn(true); 9 | }; 10 | 11 | const onClickLogout = () => { 12 | setIsLoggedIn(false); 13 | }; 14 | 15 | return ( 16 |
17 | 22 |
소플과 함께하는 리액트 공부!
23 |
24 | ); 25 | } 26 | 27 | export default LandingPage; 28 | -------------------------------------------------------------------------------- /src/chapter_14/MainContent.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import ThemeContext from "./ThemeContext"; 3 | 4 | function MainContent(props) { 5 | const { theme, toggleTheme } = useContext(ThemeContext); 6 | 7 | return ( 8 |
17 |

안녕하세요, 테마 변경이 가능한 웹사이트 입니다.

18 | 19 |
20 | ); 21 | } 22 | 23 | export default MainContent; 24 | -------------------------------------------------------------------------------- /src/chapter_09/Toolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | wrapper: { 5 | padding: 16, 6 | display: "flex", 7 | flexDirection: "row", 8 | borderBottom: "1px solid grey", 9 | }, 10 | greeting: { 11 | marginRight: 8, 12 | }, 13 | }; 14 | 15 | function Toolbar(props) { 16 | const { isLoggedIn, onClickLogin, onClickLogout } = props; 17 | 18 | return ( 19 |
20 | {isLoggedIn && 환영합니다!} 21 | 22 | {isLoggedIn ? ( 23 | 24 | ) : ( 25 | 26 | )} 27 |
28 | ); 29 | } 30 | 31 | export default Toolbar; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "first-met-react-practice-v18", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "5.0.1", 12 | "styled-components": "^5.3.5", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/chapter_07/Accommodate.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import useCounter from "./useCounter"; 3 | 4 | const MAX_CAPACITY = 10; 5 | 6 | function Accommodate(props) { 7 | const [isFull, setIsFull] = useState(false); 8 | const [count, increaseCount, decreaseCount] = useCounter(0); 9 | 10 | useEffect(() => { 11 | console.log("======================"); 12 | console.log("useEffect() is called."); 13 | console.log(`isFull: ${isFull}`); 14 | }); 15 | 16 | useEffect(() => { 17 | setIsFull(count >= MAX_CAPACITY); 18 | console.log(`Current count value: ${count}`); 19 | }, [count]); 20 | 21 | return ( 22 |
23 |

{`총 ${count}명 수용했습니다.`}

24 | 25 | 28 | 29 | 30 | {isFull &&

정원이 가득찼습니다.

} 31 |
32 | ); 33 | } 34 | 35 | export default Accommodate; 36 | -------------------------------------------------------------------------------- /src/chapter_06/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | wrapper: { 5 | margin: 8, 6 | padding: 8, 7 | display: "flex", 8 | flexDirection: "row", 9 | border: "1px solid grey", 10 | borderRadius: 16, 11 | }, 12 | messageText: { 13 | color: "black", 14 | fontSize: 16, 15 | }, 16 | }; 17 | 18 | class Notification extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = {}; 23 | } 24 | 25 | componentDidMount() { 26 | console.log(`${this.props.id} componentDidMount() called.`); 27 | } 28 | 29 | componentDidUpdate() { 30 | console.log(`${this.props.id} componentDidUpdate() called.`); 31 | } 32 | 33 | componentWillUnmount() { 34 | console.log(`${this.props.id} componentWillUnmount() called.`); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | {this.props.message} 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default Notification; 47 | -------------------------------------------------------------------------------- /src/chapter_11/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function SignUp(props) { 4 | const [name, setName] = useState(""); 5 | const [gender, setGender] = useState("남자"); 6 | 7 | const handleChangeName = (event) => { 8 | setName(event.target.value); 9 | }; 10 | 11 | const handleChangeGender = (event) => { 12 | setGender(event.target.value); 13 | }; 14 | 15 | const handleSubmit = (event) => { 16 | alert(`이름: ${name}, 성별: ${gender}`); 17 | event.preventDefault(); 18 | }; 19 | 20 | return ( 21 |
22 | 26 |
27 | 34 | 35 |
36 | ); 37 | } 38 | 39 | export default SignUp; 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | import Library from './chapter_03/Library'; 8 | import Clock from './chapter_04/Clock'; 9 | import CommentList from './chapter_05/CommentList'; 10 | import NotificationList from './chapter_06/NotificationList'; 11 | import Accommodate from './chapter_07/Accommodate'; 12 | import ConfirmButton from './chapter_08/ConfirmButton'; 13 | import LandingPage from './chapter_09/LandingPage'; 14 | import AttendanceBook from './chapter_10/AttendanceBook'; 15 | import SignUp from './chapter_11/SignUp'; 16 | import Calculator from './chapter_12/Calculator'; 17 | import ProfileCard from './chapter_13/ProfileCard'; 18 | import DarkOrLight from './chapter_14/DarkOrLight'; 19 | import Blocks from './chapter_15/Blocks'; 20 | 21 | const root = ReactDOM.createRoot(document.getElementById('root')); 22 | root.render( 23 | 24 | 25 | 26 | ); 27 | 28 | // If you want to start measuring performance in your app, pass a function 29 | // to log results (for example: reportWebVitals(console.log)) 30 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 31 | reportWebVitals(); 32 | -------------------------------------------------------------------------------- /src/chapter_05/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | wrapper: { 5 | margin: 8, 6 | padding: 8, 7 | display: "flex", 8 | flexDirection: "row", 9 | border: "1px solid grey", 10 | borderRadius: 16, 11 | }, 12 | imageContainer: {}, 13 | image: { 14 | width: 50, 15 | height: 50, 16 | borderRadius: 25, 17 | }, 18 | contentContainer: { 19 | marginLeft: 8, 20 | display: "flex", 21 | flexDirection: "column", 22 | justifyContent: "center", 23 | }, 24 | nameText: { 25 | color: "black", 26 | fontSize: 16, 27 | fontWeight: "bold", 28 | }, 29 | commentText: { 30 | color: "black", 31 | fontSize: 16, 32 | }, 33 | }; 34 | 35 | function Comment(props) { 36 | return ( 37 |
38 |
39 | 43 |
44 | 45 |
46 | {props.name} 47 | {props.comment} 48 |
49 |
50 | ); 51 | } 52 | 53 | export default Comment; 54 | -------------------------------------------------------------------------------- /src/chapter_15/Blocks.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Wrapper = styled.div` 4 | padding: 1rem; 5 | display: flex; 6 | flex-direction: row; 7 | align-items: flex-start; 8 | justify-content: flex-start; 9 | background-color: lightgrey; 10 | `; 11 | 12 | const Block = styled.div` 13 | padding: ${(props) => props.padding}; 14 | border: 1px solid black; 15 | border-radius: 1rem; 16 | background-color: ${(props) => props.backgroundColor}; 17 | color: white; 18 | font-size: 2rem; 19 | font-weight: bold; 20 | text-align: center; 21 | `; 22 | 23 | const blockItems = [ 24 | { 25 | label: "1", 26 | padding: "1rem", 27 | backgroundColor: "red", 28 | }, 29 | { 30 | label: "2", 31 | padding: "3rem", 32 | backgroundColor: "green", 33 | }, 34 | { 35 | label: "3", 36 | padding: "2rem", 37 | backgroundColor: "blue", 38 | }, 39 | ]; 40 | 41 | function Blocks(props) { 42 | return ( 43 | 44 | {blockItems.map((blockItem) => { 45 | return ( 46 | 50 | {blockItem.label} 51 | 52 | ); 53 | })} 54 | 55 | ); 56 | } 57 | 58 | export default Blocks; 59 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/chapter_06/NotificationList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Notification from "./Notification"; 3 | 4 | const reservedNotifications = [ 5 | { 6 | id: 1, 7 | message: "안녕하세요, 오늘 일정을 알려드립니다.", 8 | }, 9 | { 10 | id: 2, 11 | message: "점심식사 시간입니다.", 12 | }, 13 | { 14 | id: 3, 15 | message: "이제 곧 미팅이 시작됩니다.", 16 | }, 17 | ]; 18 | 19 | var timer; 20 | 21 | class NotificationList extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | this.state = { 26 | notifications: [], 27 | }; 28 | } 29 | 30 | componentDidMount() { 31 | const { notifications } = this.state; 32 | timer = setInterval(() => { 33 | if (notifications.length < reservedNotifications.length) { 34 | const index = notifications.length; 35 | notifications.push(reservedNotifications[index]); 36 | this.setState({ 37 | notifications: notifications, 38 | }); 39 | } else { 40 | this.setState({ 41 | notifications: [], 42 | }); 43 | clearInterval(timer); 44 | } 45 | }, 1000); 46 | } 47 | 48 | componentWillUnmount() { 49 | if (timer) { 50 | clearInterval(timer); 51 | } 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 | {this.state.notifications.map((notification) => { 58 | return ( 59 | 64 | ); 65 | })} 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default NotificationList; 72 | -------------------------------------------------------------------------------- /src/chapter_12/Calculator.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import TemperatureInput from "./TemperatureInput"; 3 | 4 | function BoilingVerdict(props) { 5 | if (props.celsius >= 100) { 6 | return

물이 끓습니다.

; 7 | } 8 | return

물이 끓지 않습니다.

; 9 | } 10 | 11 | function toCelsius(fahrenheit) { 12 | return ((fahrenheit - 32) * 5) / 9; 13 | } 14 | 15 | function toFahrenheit(celsius) { 16 | return (celsius * 9) / 5 + 32; 17 | } 18 | 19 | function tryConvert(temperature, convert) { 20 | const input = parseFloat(temperature); 21 | if (Number.isNaN(input)) { 22 | return ""; 23 | } 24 | const output = convert(input); 25 | const rounded = Math.round(output * 1000) / 1000; 26 | return rounded.toString(); 27 | } 28 | 29 | function Calculator(props) { 30 | const [temperature, setTemperature] = useState(""); 31 | const [scale, setScale] = useState("c"); 32 | 33 | const handleCelsiusChange = (temperature) => { 34 | setTemperature(temperature); 35 | setScale("c"); 36 | }; 37 | 38 | const handleFahrenheitChange = (temperature) => { 39 | setTemperature(temperature); 40 | setScale("f"); 41 | }; 42 | 43 | const celsius = 44 | scale === "f" ? tryConvert(temperature, toCelsius) : temperature; 45 | const fahrenheit = 46 | scale === "c" ? tryConvert(temperature, toFahrenheit) : temperature; 47 | 48 | return ( 49 |
50 | 55 | 60 | 61 |
62 | ); 63 | } 64 | 65 | export default Calculator; 66 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------