├── 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 |
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 |
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 |
25 | {students.map((student, index) => {
26 | return - {student.name}
;
27 | })}
28 |
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 |
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 |
--------------------------------------------------------------------------------