[modern-react] 리액트 스터디 파일 추가
This commit is contained in:
38
modern-react/my-react/src/App.css
Normal file
38
modern-react/my-react/src/App.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
37
modern-react/my-react/src/App.js
Normal file
37
modern-react/my-react/src/App.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// 로고/스타일시트 가져오기
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
// 앱 컴포넌트 정의
|
||||
function App() {
|
||||
const attrs = {
|
||||
href: 'https://wings.msn.to/',
|
||||
download: false,
|
||||
target: '_blank',
|
||||
rel: 'help'
|
||||
};
|
||||
// 렌더링할 내용 생성
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
안녕, 리액트!!
|
||||
</a>
|
||||
<a href={attrs.href} download={attrs.download}
|
||||
target={attrs.target} rel={attrs.rel}>지원 페이지로 이동하기</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 앱 컴포넌트 내보내기
|
||||
export default App;
|
||||
30
modern-react/my-react/src/App.test.js
Normal file
30
modern-react/my-react/src/App.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// import { render, screen } from '@testing-library/react';
|
||||
// import App from './App';
|
||||
|
||||
// // 테스트케이스 정의
|
||||
// test('renders learn react link', () => {
|
||||
// // 컴포넌트 렌더링
|
||||
// render(<App />);
|
||||
// // 테스트 대상 요소 검색 및 획득
|
||||
// const linkElement = screen.getByText(/learn react/i);
|
||||
// // 렌더링 결과의 정확성 검증
|
||||
// expect(linkElement).toBeInTheDocument();
|
||||
// });
|
||||
|
||||
|
||||
|
||||
// Code 9-1-4
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
// 테스트케이스 정의
|
||||
test('renders learn react link', () => {
|
||||
const { debug, baseElement } = render(<App />);
|
||||
debug(baseElement);
|
||||
// 컴포넌트 렌더링
|
||||
render(<App />);
|
||||
// 테스트 대상 요소 검색 및 획득
|
||||
const linkElement = screen.getByText(/안녕, 리액트!!/i);
|
||||
// 렌더링 결과의 정확성 검증
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
31
modern-react/my-react/src/AppClass.js
Normal file
31
modern-react/my-react/src/AppClass.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
// AppClass 컴포넌트 정의
|
||||
class AppClass extends React.Component {
|
||||
// 렌더링할 내용 정의하기
|
||||
render() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AppClass 컴포넌트 내보내기
|
||||
export default AppClass;
|
||||
5
modern-react/my-react/src/chap02/class.css
Normal file
5
modern-react/my-react/src/chap02/class.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.foo {
|
||||
color: White;
|
||||
background-color: Blue;
|
||||
padding: 3px;
|
||||
}
|
||||
9
modern-react/my-react/src/chap03/Download.js
Normal file
9
modern-react/my-react/src/chap03/Download.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import dl_icon from '../image/dl.png';
|
||||
|
||||
export default function Download({ slug }) {
|
||||
return (
|
||||
<a href={`https://github.com/wikibook/${slug}/`}>
|
||||
<img src={dl_icon} alt="Sample Download" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
25
modern-react/my-react/src/chap03/EventArgs.js
Normal file
25
modern-react/my-react/src/chap03/EventArgs.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default function EventArgs() {
|
||||
// 자체 인수를 추가한 이벤트 핸들러
|
||||
const current = (e, type) => {
|
||||
const d = new Date();
|
||||
switch(type) {
|
||||
case 'date':
|
||||
console.log(`${e.target.id}: ${d.toLocaleDateString()}`);
|
||||
break;
|
||||
case 'time':
|
||||
console.log(`${e.target.id}: ${d.toLocaleTimeString()}`);
|
||||
break;
|
||||
default:
|
||||
console.log(`${e.target.id}: ${d.toLocaleString()}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{/* 화살표 함수를 통해 핸들러를 호출 */}
|
||||
<button id="dt" onClick={e => current(e, 'datetime')}>현재 날짜 및 시각</button>
|
||||
<button id="date" onClick={e => current(e, 'date')}>현재 날짜</button>
|
||||
<button id="time" onClick={e => current(e, 'time')}>현재 시각</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
modern-react/my-react/src/chap03/EventArgs2.js
Normal file
25
modern-react/my-react/src/chap03/EventArgs2.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default function EventArgs2() {
|
||||
const current = e => {
|
||||
const type = e.target.dataset.type;
|
||||
const d = new Date();
|
||||
switch(type) {
|
||||
case 'date':
|
||||
console.log(`${e.target.id}: ${d.toLocaleDateString()}`);
|
||||
break;
|
||||
case 'time':
|
||||
console.log(`${e.target.id}: ${d.toLocaleTimeString()}`);
|
||||
break;
|
||||
default:
|
||||
console.log(`${e.target.id}: ${d.toLocaleString()}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{/* 출력할 날짜 및 시각 유형을 고유 데이터 속성으로 지정 */}
|
||||
<button id="dt" data-type="datetime" onClick={current}>현재 날짜 및 시각</button>
|
||||
<button id="date" data-type="date" onClick={current}>현재 날짜</button>
|
||||
<button id="time" data-type="time" onClick={current}>현재 시각</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
modern-react/my-react/src/chap03/EventBasic.js
Normal file
25
modern-react/my-react/src/chap03/EventBasic.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default function EventBasic({ type }) {
|
||||
// click 이벤트 핸들러
|
||||
const current = () => {
|
||||
const d = new Date();
|
||||
// type 속성 값에 따라 현재 날짜 및 시각을 로그에 출력한다.
|
||||
switch(type) {
|
||||
case 'date':
|
||||
console.log(d.toLocaleDateString());
|
||||
break;
|
||||
case 'time':
|
||||
console.log(d.toLocaleTimeString());
|
||||
break;
|
||||
default:
|
||||
console.log(d.toLocaleString());
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 버튼 클릭 시 current 함수 호출 */}
|
||||
<button onClick={current}>현재 시각 가져오기</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
modern-react/my-react/src/chap03/EventCompare.css
Normal file
15
modern-react/my-react/src/chap03/EventCompare.css
Normal file
@@ -0,0 +1,15 @@
|
||||
#outer {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin-left: 100px;
|
||||
padding: 10px;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
#inner {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-left: 40px;
|
||||
padding: 10px;
|
||||
border: 1px solid red
|
||||
}
|
||||
23
modern-react/my-react/src/chap03/EventCompare.js
Normal file
23
modern-react/my-react/src/chap03/EventCompare.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
import './EventCompare.css';
|
||||
|
||||
export default function EventCompare() {
|
||||
const [result, setResult] = useState('');
|
||||
// mouseenter/mouseleave 이벤트의 정보를 result에 반영
|
||||
const handleIn = e => setResult(r => `${r}Enter:${e.target.id}<br />`);
|
||||
const handleOut= e => setResult(r => `${r}Leave:${e.target.id}<br />`);
|
||||
return (
|
||||
<>
|
||||
<div id="outer"
|
||||
onMouseEnter={handleIn} onMouseLeave={handleOut}
|
||||
// onMouseOver={handleIn} onMouseOut={handleOut}
|
||||
>
|
||||
외부(outer)
|
||||
<p id="inner">
|
||||
내부(inner)
|
||||
</p>
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{__html: result}}></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
modern-react/my-react/src/chap03/EventError.js
Normal file
10
modern-react/my-react/src/chap03/EventError.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function EventError({ src, alt }) {
|
||||
const [path, setPath] = useState(src);
|
||||
// 이미지를 불러올 수 없는 경우 오류 이미지 표시
|
||||
const handleError = () => setPath('./image/noimage.jpg');
|
||||
return (
|
||||
<img src={path} alt={alt} onError={handleError} />
|
||||
);
|
||||
}
|
||||
17
modern-react/my-react/src/chap03/EventKey.js
Normal file
17
modern-react/my-react/src/chap03/EventKey.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function EventKey() {
|
||||
// Ctrl + q로 도움말 메시지 표시
|
||||
const handleKey = e => {
|
||||
if (e.ctrlKey && e.key === 'q') {
|
||||
alert('이름은 20자 이내로 입력해 주세요.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<label>
|
||||
이름:
|
||||
<input type="text" size="20" onKeyDown={handleKey} />
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
13
modern-react/my-react/src/chap03/EventMouse.js
Normal file
13
modern-react/my-react/src/chap03/EventMouse.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function EventMouse({ beforeSrc, afterSrc, alt }) {
|
||||
// 현재 표시 중인 이미지
|
||||
const [current, setCurrent] = useState(beforeSrc);
|
||||
// mouseover/mouseleave 이벤트 핸들러를 준비한다.
|
||||
const handleEnter = () => setCurrent(afterSrc);
|
||||
const handleLeave = () => setCurrent(beforeSrc);
|
||||
return (
|
||||
<img src={current} alt={alt}
|
||||
onMouseEnter={handleEnter} onMouseLeave={handleLeave} />
|
||||
);
|
||||
}
|
||||
7
modern-react/my-react/src/chap03/EventObj.js
Normal file
7
modern-react/my-react/src/chap03/EventObj.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function EventObj() {
|
||||
// 클릭 시 이벤트 오브젝트를 로그에 출력
|
||||
const handleClick = e => console.log(e);
|
||||
return (
|
||||
<button onClick={handleClick}>클릭</button>
|
||||
);
|
||||
}
|
||||
23
modern-react/my-react/src/chap03/EventOnce.js
Normal file
23
modern-react/my-react/src/chap03/EventOnce.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function EventOnce() {
|
||||
// 클릭 여부를 관리하기 위한 플래그
|
||||
const [clicked, setClicked] = useState(false);
|
||||
// 오늘의 운세(점수)
|
||||
const [result, setResult] = useState('-');
|
||||
const handleClick = e => {
|
||||
// 클릭하지 않은 경우에만 운세를 계산한다.
|
||||
if (!clicked) {
|
||||
setResult(Math.floor(Math.random() * 100 + 1));
|
||||
// 플래그 반전
|
||||
setClicked(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleClick}>결과 보기</button>
|
||||
<p>오늘의 운세는 {result}점입니다.</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
modern-react/my-react/src/chap03/EventPassive.css
Normal file
6
modern-react/my-react/src/chap03/EventPassive.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.box {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border: 1px solid #000;
|
||||
overflow: scroll;
|
||||
}
|
||||
45
modern-react/my-react/src/chap03/EventPassive.js
Normal file
45
modern-react/my-react/src/chap03/EventPassive.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
import './EventPassive.css';
|
||||
|
||||
export default function EventPassive() {
|
||||
const handleWheel = e => e.preventDefault();
|
||||
const divRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const div = divRef.current;
|
||||
div.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return (() => {
|
||||
div.removeEventListener('wheel', handleWheel);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="box"
|
||||
onWheel={handleWheel}>예를 들어 Wheel 이벤트를 핸들러에서...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// export default function EventPassive() {
|
||||
// const handleWheel = e => e.preventDefault();
|
||||
// // <div> 요소에 대한 참조 가져오기
|
||||
// const divRef = useRef(null);
|
||||
// useEffect(() => {
|
||||
// // 컴포넌트 시작 시 리스너 설정
|
||||
// const div = divRef.current;
|
||||
// div.addEventListener('wheel', handleWheel, { passive: false });
|
||||
// return (() => {
|
||||
// // 컴포넌트 폐기 시 리스너도 함께 폐기
|
||||
// div.removeEventListener('wheel', handleWheel);
|
||||
// });
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <div ref={divRef} className="box"
|
||||
// onWheel={handleWheel}
|
||||
// >
|
||||
// 예를 들어 Wheel 이벤트를 핸들러에서...
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
9
modern-react/my-react/src/chap03/EventPoint.css
Normal file
9
modern-react/my-react/src/chap03/EventPoint.css
Normal file
@@ -0,0 +1,9 @@
|
||||
#main {
|
||||
position:absolute;
|
||||
margin:50px;
|
||||
top:20px;
|
||||
left:20px;
|
||||
height: 150px;
|
||||
width: 500px;
|
||||
border: solid 1px #000;
|
||||
}
|
||||
26
modern-react/my-react/src/chap03/EventPoint.js
Normal file
26
modern-react/my-react/src/chap03/EventPoint.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useState } from 'react';
|
||||
import './EventPoint.css';
|
||||
|
||||
export default function EventPoint() {
|
||||
const [screen, setScreen] = useState({ x: 0, y: 0 });
|
||||
const [page, setPage] = useState({ x: 0, y: 0 });
|
||||
const [client, setClient] = useState({ x: 0, y: 0 });
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
|
||||
// 포인터 위치를 각각의 기준에 따라 표시
|
||||
const handleMousemove = e => {
|
||||
setScreen({ x: e.screenX, y: e.screenY });
|
||||
setPage({ x: e.pageX, y: e.pageY });
|
||||
setClient({ x: e.clientX, y: e.clientY });
|
||||
setOffset({ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY });
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="main" onMouseMove={handleMousemove}>
|
||||
screen: {screen.x}/{screen.y}<br />
|
||||
page: {page.x}/{page.y}<br />
|
||||
client: {client.x}/{client.y}<br />
|
||||
offset: {offset.x}/{offset.y}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
modern-react/my-react/src/chap03/EventPropagation.css
Normal file
26
modern-react/my-react/src/chap03/EventPropagation.css
Normal file
@@ -0,0 +1,26 @@
|
||||
#parent {
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
margin-left: 50px;
|
||||
padding: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#my {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
margin-left: 40px;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid red
|
||||
}
|
||||
|
||||
#child {
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-left: 40px;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid black
|
||||
}
|
||||
88
modern-react/my-react/src/chap03/EventPropagation.js
Normal file
88
modern-react/my-react/src/chap03/EventPropagation.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import './EventPropagation.css';
|
||||
|
||||
export default function EventPropagation() {
|
||||
const handleParent = () => alert('#parent run...');
|
||||
const handleMy = () => alert('#my run...');
|
||||
const handleChild = () => alert('#child run...');
|
||||
|
||||
return (
|
||||
<div id="parent" onClick={handleParent}>
|
||||
부모 요소
|
||||
<div id="my" onClick={handleMy}>
|
||||
현재 요소
|
||||
<a id="child" href="https://wikibook.co.kr/" onClick={handleChild}>
|
||||
자식 요소
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// export default function EventPropagation() {
|
||||
// const handleParent = () => alert('#parent run...');
|
||||
// const handleMy = () => alert('#my run...');
|
||||
// const handleChild = () => alert('#child run...');
|
||||
|
||||
// return (
|
||||
// <div id="parent" onClickCapture={handleParent}>
|
||||
// 부모 요소
|
||||
// <div id="my" onClickCapture={handleMy}>
|
||||
// 현재 요소
|
||||
// <a id="child" href="https://wikibook.co.kr/" onClickCapture={handleChild}>
|
||||
// 자식 요소
|
||||
// </a>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// export default function EventPropagation() {
|
||||
// const handleParent = () => alert('#parent run...');
|
||||
// const handleMy = () => alert('#my run...');
|
||||
// const handleChild = e => {
|
||||
// e.stopPropagation();
|
||||
// alert('#child run...');
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// return (
|
||||
// <div id="parent" onClickCapture={handleParent}>
|
||||
// 부모 요소
|
||||
// <div id="my" onClickCapture={handleMy}>
|
||||
// 현재 요소
|
||||
// <a id="child" href="https://wikibook.co.kr/" onClickCapture={handleChild}>
|
||||
// 자식 요소
|
||||
// </a>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// export default function EventPropagation() {
|
||||
// const handleParent = () => alert('#parent run...');
|
||||
// const handleMy = () => alert('#my run...');
|
||||
// const handleChild = e => {
|
||||
// e.preventDefault();
|
||||
// alert('#child run...');
|
||||
// };
|
||||
|
||||
|
||||
// return (
|
||||
// <div id="parent" onClickCapture={handleParent}>
|
||||
// 부모 요소
|
||||
// <div id="my" onClickCapture={handleMy}>
|
||||
// 현재 요소
|
||||
// <a id="child" href="https://wikibook.co.kr/" onClickCapture={handleChild}>
|
||||
// 자식 요소
|
||||
// </a>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
19
modern-react/my-react/src/chap03/ForFilter.js
Normal file
19
modern-react/my-react/src/chap03/ForFilter.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ForFilter({ src }) {
|
||||
const lowPrice = src.filter(book => book.price < 25000);
|
||||
return (
|
||||
<dl>
|
||||
{lowPrice.map(elem => (
|
||||
<React.Fragment key={elem.isbn}>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
|
||||
{elem.title}({elem.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{elem.summary}</dd>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
84
modern-react/my-react/src/chap03/ForItem.js
Normal file
84
modern-react/my-react/src/chap03/ForItem.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import Download from './Download';
|
||||
|
||||
export default function ForItem({ book }) {
|
||||
return (
|
||||
<>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${book.isbn}.jpg`}>
|
||||
{book.title}({book.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{book.summary}</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 3-2-13
|
||||
// if 문
|
||||
// export default function ForItem({ book }) {
|
||||
// let dd;
|
||||
// // download 속성의 유무에 따라 태그를 분기한다.
|
||||
// if (book.download) {
|
||||
// dd = <dd>{book.summary}<Download slug={book.slug} /></dd>;
|
||||
// } else {
|
||||
// dd = <dd>{book.summary}</dd>;
|
||||
// }
|
||||
// return (
|
||||
// <>
|
||||
// <dt>
|
||||
// <a href={`https://wikibook.co.kr/images/cover/s/${book.isbn}.jpg`}>
|
||||
// {book.title}({book.price}원)
|
||||
// </a>
|
||||
// </dt>
|
||||
// {/* 생성해둔 태그 삽입 */}
|
||||
// {dd}
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 3-2-14
|
||||
// 즉시 함수
|
||||
// export default function ForItem({ book }) {
|
||||
// return (
|
||||
// <>
|
||||
// <dt>
|
||||
// <a href={`https://wikibook.co.kr/images/cover/s/${book.isbn}.jpg`}>
|
||||
// {book.title}({book.price}원)
|
||||
// </a>
|
||||
// </dt>
|
||||
// {(() => {
|
||||
// if (book.download) {
|
||||
// return <dd>{book.summary}<Download slug={book.slug} /></dd>
|
||||
// } else {
|
||||
// return <dd>{book.summary}</dd>
|
||||
// }
|
||||
// })()}
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 3-2-15
|
||||
// ?:, && 연산자
|
||||
// export default function ForItem({ book }) {
|
||||
// return (
|
||||
// <>
|
||||
// <dt>
|
||||
// <a href={`https://wikibook.co.kr/images/cover/s/${book.isbn}.jpg`}>
|
||||
// {book.title}({book.price}원)
|
||||
// </a>
|
||||
// </dt>
|
||||
// <dd>
|
||||
// {book.summary}
|
||||
// {book.download ? <Download isbn={book.isbn} /> : null}
|
||||
// {/* {book.download && <Download isbn={book.isbn} />} */}
|
||||
// {/* {book.download || '×' } */}
|
||||
// </dd>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
42
modern-react/my-react/src/chap03/ForList.js
Normal file
42
modern-react/my-react/src/chap03/ForList.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
// 도서 정보는 Props(src)를 통해 수신
|
||||
export default function ForList({ src }) {
|
||||
return (
|
||||
// 도서 정보(src 속성)를 <dt>/<dd> 목록으로 정형화
|
||||
<dl>
|
||||
{src.map(elem => (
|
||||
<>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
|
||||
{elem.title}({elem.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{elem.summary}</dd>
|
||||
</>
|
||||
))}
|
||||
|
||||
{/* {src.map(elem => (
|
||||
<React.Fragment key={elem.isbn}>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
|
||||
{elem.title}({elem.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{elem.summary}</dd>
|
||||
</React.Fragment>
|
||||
))} */}
|
||||
|
||||
{/* {src.map((elem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
|
||||
{elem.title}({elem.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{elem.summary}</dd>
|
||||
</React.Fragment>
|
||||
))} */}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
11
modern-react/my-react/src/chap03/ForNest.js
Normal file
11
modern-react/my-react/src/chap03/ForNest.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import ForItem from './ForItem';
|
||||
|
||||
export default function ForNest({ src }) {
|
||||
return (
|
||||
<dl>
|
||||
{src.map(elem =>
|
||||
<ForItem book={elem} key={elem.isbn} />
|
||||
)}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
19
modern-react/my-react/src/chap03/ForSort.js
Normal file
19
modern-react/my-react/src/chap03/ForSort.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ForSort({ src }) {
|
||||
src.sort((m, n) => m.price - n.price);
|
||||
return (
|
||||
<dl>
|
||||
{src.map(elem => (
|
||||
<React.Fragment key={elem.isbn}>
|
||||
<dt>
|
||||
<a href={`https://wikibook.co.kr/images/cover/s/${elem.isbn}.jpg`}>
|
||||
{elem.title}({elem.price}원)
|
||||
</a>
|
||||
</dt>
|
||||
<dd>{elem.summary}</dd>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
29
modern-react/my-react/src/chap03/ListTemplate.js
Normal file
29
modern-react/my-react/src/chap03/ListTemplate.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ListTemplate({ src, children }) {
|
||||
return (
|
||||
<dl>
|
||||
{src.map(elem => (
|
||||
<React.Fragment key={elem.isbn}>
|
||||
{/* {children} */}
|
||||
{children(elem)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 렌더 프롭(Render Props)
|
||||
// export default function ListTemplate({ src, render }) {
|
||||
// return (
|
||||
// <dl>
|
||||
// {src.map(elem => (
|
||||
// <React.Fragment key={elem.isbn}>
|
||||
// {render(elem)}
|
||||
// </React.Fragment>
|
||||
// ))}
|
||||
// </dl>
|
||||
// );
|
||||
// }
|
||||
40
modern-react/my-react/src/chap03/MyHello.js
Normal file
40
modern-react/my-react/src/chap03/MyHello.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Code 3-1-1
|
||||
// export default function MyHello(props) {
|
||||
// return (
|
||||
// <div>안녕하세요, {props.myName}님!</div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 3-1-3
|
||||
// export default function MyHello({ myName }) {
|
||||
// return (
|
||||
// <div>안녕하세요, {myName}님!</div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default function MyHello({ myName = '김철수' }) {
|
||||
// return (
|
||||
// <div>안녕하세요, {myName}님!</div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 3-3-14
|
||||
// PropTypes 가져오기
|
||||
function MyHello(props) {
|
||||
return (
|
||||
<div>안녕하세요, {props.myName}님!</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 타입 정보 선언
|
||||
MyHello.propTypes = {
|
||||
myName: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default MyHello;
|
||||
20
modern-react/my-react/src/chap03/SelectStyle.css
Normal file
20
modern-react/my-react/src/chap03/SelectStyle.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.box {
|
||||
display: block;
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
overflow: auto;
|
||||
margin: 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: black;
|
||||
background-color: skyblue;
|
||||
border: 5px solid blue;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: white;
|
||||
background-color: black;
|
||||
border: 5px solid gray;
|
||||
}
|
||||
46
modern-react/my-react/src/chap03/SelectStyle.js
Normal file
46
modern-react/my-react/src/chap03/SelectStyle.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import './SelectStyle.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default function SelectStyle({ mode }) {
|
||||
return (
|
||||
// mode 속성에 따라 스타일 클래스 전환
|
||||
<div className={`box ${mode === 'light' ? 'light' : 'dark'}`}>
|
||||
Hello World!
|
||||
</div>
|
||||
|
||||
// <div className={mode === 'light' ? 'light' : 'dark'}>
|
||||
// Hello World!
|
||||
// </div>
|
||||
|
||||
// <div className={(mode !== 'light') && 'dark'}>
|
||||
// Hello World!
|
||||
// </div>
|
||||
|
||||
// <div className={cn('box', mode === 'light' ? 'light' : 'dark')}>
|
||||
// Hello World!
|
||||
// </div>
|
||||
|
||||
// <div className={cn(
|
||||
// 'box',
|
||||
// {
|
||||
// light: mode === 'light',
|
||||
// dark: mode === 'dark'
|
||||
// }
|
||||
// )}>
|
||||
// Hello World!
|
||||
// </div>
|
||||
|
||||
// <div className={cn(
|
||||
// 'box',
|
||||
// [
|
||||
// 'panel',
|
||||
// {
|
||||
// light: mode === 'light',
|
||||
// dark: mode === 'dark'
|
||||
// }
|
||||
// ]
|
||||
// )}>
|
||||
// Hello World!
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
51
modern-react/my-react/src/chap03/StateBasic.js
Normal file
51
modern-react/my-react/src/chap03/StateBasic.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function StateBasic({ init }) {
|
||||
// Props(init)로 State(count) 초기화하기
|
||||
const [count, setCount] = useState(init);
|
||||
// [카운트] 버튼 클릭 시 카운트 값을 증가시킨다.
|
||||
console.log(`count is ${count}.`);
|
||||
const handleClick = () => setCount(count + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={handleClick}>카운트</button>
|
||||
<p>{count}번 클릭했습니다.</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 3-3-28
|
||||
// export default function StateBasic({ init }) {
|
||||
// const [count, setCount] = useState(init);
|
||||
// // [카운트] 버튼 클릭 시 카운트 값을 증가시킨다.
|
||||
// const handleClick = () => {
|
||||
// setCount(count + 1);
|
||||
// setCount(count + 1);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <button onClick={handleClick}>카운트</button>
|
||||
// <p>{count}번 클릭했습니다.</p>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default function StateBasic({ init }) {
|
||||
// const [count, setCount] = useState(init);
|
||||
// // [카운트] 버튼 클릭 시 카운트 값을 증가시킨다.
|
||||
// const handleClick = () => {
|
||||
// setCount(c => c + 1);
|
||||
// setCount(c => c + 1);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <button onClick={handleClick}>카운트</button>
|
||||
// <p>{count}번 클릭했습니다.</p>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
5
modern-react/my-react/src/chap03/StateCounter.css
Normal file
5
modern-react/my-react/src/chap03/StateCounter.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.cnt {
|
||||
margin-right: 5px;
|
||||
width: 50px;
|
||||
font-size: xx-large;
|
||||
}
|
||||
11
modern-react/my-react/src/chap03/StateCounter.js
Normal file
11
modern-react/my-react/src/chap03/StateCounter.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import './StateCounter.css';
|
||||
|
||||
export default function StateCounter({ step, onUpdate }) {
|
||||
// 버튼 클릭으로 상위 State(count)에 step 값만큼 추가
|
||||
const handleClick = () => onUpdate(step);
|
||||
return (
|
||||
<button className="cnt" onClick={handleClick}>
|
||||
<span>{step}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
18
modern-react/my-react/src/chap03/StateParent.js
Normal file
18
modern-react/my-react/src/chap03/StateParent.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useState } from 'react';
|
||||
import StateCounter from './StateCounter';
|
||||
|
||||
export default function StateParent() {
|
||||
// 카운트 합계를 나타내는 count를 초기화한다.
|
||||
const [count, setCount] = useState(0);
|
||||
// State 값(count)을 갱신하기 위한 update 함수를 준비한다.
|
||||
const update = step => setCount(c => c + step);
|
||||
return (
|
||||
<>
|
||||
{/* StateCounter 컴포넌트에 update 함수를 전달 */}
|
||||
<p>총 개수: {count}</p>
|
||||
<StateCounter step={1} onUpdate={update} />
|
||||
<StateCounter step={5} onUpdate={update} />
|
||||
<StateCounter step={-1} onUpdate={update} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
modern-react/my-react/src/chap03/StyledPanel.js
Normal file
14
modern-react/my-react/src/chap03/StyledPanel.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function StyledPanel({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
margin: 50,
|
||||
padding: 20,
|
||||
border: '1px solid #000',
|
||||
width: 'fit-content',
|
||||
boxShadow: '10px 5px 5px #999',
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
modern-react/my-react/src/chap03/TitledPanel.js
Normal file
37
modern-react/my-react/src/chap03/TitledPanel.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// export default function TitledPanel({ title, body }) {
|
||||
// return (
|
||||
// <div style={{
|
||||
// margin: 50,
|
||||
// padding: 5,
|
||||
// border: '1px solid #000',
|
||||
// width: 'fit-content',
|
||||
// boxShadow: '10px 5px 5px #999',
|
||||
// backgroundColor: '#fff'
|
||||
// }}>
|
||||
// {title}
|
||||
// <hr />
|
||||
// {body}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// key 속성이 title/body인 요소를 가져온다.
|
||||
export default function TitledPanel({ children }) {
|
||||
const title = children.find(elem => elem.key === 'title');
|
||||
const body = children.find(elem => elem.key === 'body')
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
margin: 50,
|
||||
padding: 5,
|
||||
border: '1px solid #000',
|
||||
width: 'fit-content',
|
||||
boxShadow: '10px 5px 5px #999',
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
{title}
|
||||
<hr />
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
modern-react/my-react/src/chap03/TypeProp.js
Normal file
37
modern-react/my-react/src/chap03/TypeProp.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export function Member() {}
|
||||
function TypeProp(props) {
|
||||
console.log(props);
|
||||
return <p>결과는 콘솔에서 확인하기 바란다.</p>;
|
||||
}
|
||||
|
||||
TypeProp.propTypes = {
|
||||
// Member형 속성
|
||||
prop1: PropTypes.instanceOf(Member),
|
||||
// 남성, 여성, 기타 중 하나
|
||||
prop2: PropTypes.oneOf(['남성', '여성', '기타']),
|
||||
// 문자열, 숫자, 부울 값 중 선택 가능
|
||||
prop3: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
// 숫자형 배열
|
||||
prop4: PropTypes.arrayOf(PropTypes.number),
|
||||
// 숫자형 객체
|
||||
prop5: PropTypes.objectOf(PropTypes.number),
|
||||
// name, age, sex 프로퍼티를 가진 오브젝트
|
||||
prop6: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
age: PropTypes.number,
|
||||
sex: PropTypes.oneOf(['남성', '여성', '기타']),
|
||||
}),
|
||||
prop7: PropTypes.exact({
|
||||
name: PropTypes.string.isRequired,
|
||||
age: PropTypes.number,
|
||||
sex: PropTypes.oneOf(['남성', '여성', '기타']),
|
||||
}),
|
||||
};
|
||||
|
||||
export default TypeProp;
|
||||
43
modern-react/my-react/src/chap03/books.js
Normal file
43
modern-react/my-react/src/chap03/books.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const books = [
|
||||
{
|
||||
isbn: '9791158395124',
|
||||
title: '게임 개발을 위한 미드저니, 스테이블 디퓨전 완벽 활용법',
|
||||
slug: 'genai-game',
|
||||
price: 28000,
|
||||
summary: '생성형 AI를 활용한 게임 캐릭터, 배경, 아이템 제작부터 유니티 실전 프로젝트까지',
|
||||
download: true,
|
||||
},
|
||||
{
|
||||
isbn: '9791158395117',
|
||||
title: '디자인을 위한 미드저니 완벽 활용법',
|
||||
slug: 'midjourney-design',
|
||||
price: 24000,
|
||||
summary: '광고부터 캐릭터, 로고, 일러스트레이션, 표지, 포스터, 타이포까지 독창적인 디자인 만들기',
|
||||
download: false,
|
||||
},
|
||||
{
|
||||
isbn: '9791158395032',
|
||||
title: '만들면서 배우는 블렌더 3D 입문',
|
||||
slug: 'blender-basic',
|
||||
price: 28000,
|
||||
summary: '블렌더 기초, 모델링, 머티리얼, 애니메이션, 렌더링까지',
|
||||
download: true,
|
||||
},
|
||||
{
|
||||
isbn: '9791158395018',
|
||||
title: '모던 그로스 마케팅',
|
||||
slug: 'mgm',
|
||||
price: 24000,
|
||||
summary: '비용은 최소화하고 매출은 극대화하는 생존 마케팅 전략',
|
||||
download: false,
|
||||
},
|
||||
{
|
||||
isbn: '9791158395025',
|
||||
title: '도메인 스토리텔링',
|
||||
slug: 'domain-storytelling',
|
||||
price: 28000,
|
||||
summary: '도메인 주도 소프트웨어 구축을 위한 스토리텔링과 스토리 시각화 기법',
|
||||
download: true,
|
||||
},
|
||||
];
|
||||
export default books;
|
||||
3
modern-react/my-react/src/chap04/FormBasic.css
Normal file
3
modern-react/my-react/src/chap04/FormBasic.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.invalid {
|
||||
background-color: #f00;
|
||||
}
|
||||
393
modern-react/my-react/src/chap04/FormBasic.js
Normal file
393
modern-react/my-react/src/chap04/FormBasic.js
Normal file
@@ -0,0 +1,393 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export default function FormBasic() {
|
||||
// 기본값 준비
|
||||
const defaultValues = {
|
||||
name: '홍길동',
|
||||
email: 'admin@example.com',
|
||||
gender: 'male',
|
||||
memo: ''
|
||||
};
|
||||
|
||||
// 폼 초기화
|
||||
const { register, handleSubmit,
|
||||
formState: { errors} } = useForm({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
// 제출 시 처리
|
||||
const onsubmit = data => console.log(data);
|
||||
const onerror = err => console.log(err);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
{/* 검증 규칙 등을 폼에 연결 */}
|
||||
<div>
|
||||
<label htmlFor="name">이름::</label><br/>
|
||||
<input id="name" type="text"
|
||||
{...register('name', {
|
||||
required: '이름은 필수 입력 항목입니다.',
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: '이름은 20자 이내로 작성해주세요.'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<div>{errors.name?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="gender">성별:</label><br/>
|
||||
<label>
|
||||
<input type="radio" value="male"
|
||||
{...register('gender', {
|
||||
required: '성별은 필수입니다.',
|
||||
})} />남성
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" value="female"
|
||||
{...register('gender', {
|
||||
required: '성펼은 필수입니다.',
|
||||
})} />여성
|
||||
</label>
|
||||
<div>{errors.gender?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email">이메일 주소:</label><br/>
|
||||
<input id="email" type="email"
|
||||
{...register('email', {
|
||||
required: '이메일 주소는 필수 입력사항입니다.',
|
||||
pattern: {
|
||||
value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
|
||||
message: '이메일 주소 형식이 잘못되었습니다.'
|
||||
}
|
||||
})} />
|
||||
<div>{errors.email?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="memo">비고:</label><br/>
|
||||
<textarea id="memo"
|
||||
{...register('memo', {
|
||||
required: '비고는 필수 입력 항목입니다.',
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: '비고는 10자 이상으로 작성해주세요.'
|
||||
}
|
||||
})} />
|
||||
<div>{errors.memo?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">제출하기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 4-3-3
|
||||
// import { useForm } from 'react-hook-form';
|
||||
|
||||
// export default function FormBasic() {
|
||||
// // 기본값 준비
|
||||
// const defaultValues = {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// };
|
||||
|
||||
// // 폼 초기화
|
||||
// const { register, handleSubmit,
|
||||
// formState: { errors} } = useForm({
|
||||
// defaultValues
|
||||
// });
|
||||
|
||||
// // 제출 시 처리
|
||||
// const onsubmit = data => console.log(data);
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// {/* 검증 규칙 등을 폼에 연결 */}
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name', {
|
||||
// required: '이름은 필수 입력 항목입니다.',
|
||||
// maxLength: {
|
||||
// value: 20,
|
||||
// message: '이름은 20자 이내로 작성해주세요.'
|
||||
// }
|
||||
// })}
|
||||
// />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender', {
|
||||
// required: '성별은 필수입니다.',
|
||||
// })} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender', {
|
||||
// required: '성펼은 필수입니다.',
|
||||
// })} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email', {
|
||||
// required: '이메일 주소는 필수 입력사항입니다.',
|
||||
// pattern: {
|
||||
// value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
|
||||
// message: '이메일 주소 형식이 잘못되었습니다.'
|
||||
// }
|
||||
// })} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo', {
|
||||
// required: '비고는 필수 입력 항목입니다.',
|
||||
// minLength: {
|
||||
// value: 10,
|
||||
// message: '비고는 10자 이상으로 작성해주세요.'
|
||||
// },
|
||||
// validate: {
|
||||
// ng: (value, formValues) => {
|
||||
// // 부적절한 단어 준비
|
||||
// const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
// // 입력 문자열에 부적절한 단어가 포함되어 있는지 판단
|
||||
// for (const ng of ngs) {
|
||||
// if (value.includes(ng)) {
|
||||
// return '비고에 적절하지 않은 단어가 포함되어 있습니다.';
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// },
|
||||
// })} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit">제출하기</button>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-3-4
|
||||
// import { useForm } from 'react-hook-form';
|
||||
|
||||
// export default function FormBasic() {
|
||||
// // 기본값 준비
|
||||
// const defaultValues = {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// };
|
||||
|
||||
// // 폼 초기화
|
||||
// const { register, handleSubmit,
|
||||
// formState: { errors, isDirty, isValid } } = useForm({
|
||||
// defaultValues
|
||||
// });
|
||||
|
||||
// // 제출 시 처리
|
||||
// const onsubmit = data => console.log(data);
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// {/* 검증 규칙 등을 폼에 연결 */}
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name', {
|
||||
// required: '이름은 필수 입력 항목입니다.',
|
||||
// maxLength: {
|
||||
// value: 20,
|
||||
// message: '이름은 20자 이내로 작성해주세요.'
|
||||
// }
|
||||
// })}
|
||||
// />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender', {
|
||||
// required: '성별은 필수입니다.',
|
||||
// })} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender', {
|
||||
// required: '성펼은 필수입니다.',
|
||||
// })} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email', {
|
||||
// required: '이메일 주소는 필수 입력사항입니다.',
|
||||
// pattern: {
|
||||
// value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
|
||||
// message: '이메일 주소 형식이 잘못되었습니다.'
|
||||
// }
|
||||
// })} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo', {
|
||||
// required: '비고는 필수 입력 항목입니다.',
|
||||
// minLength: {
|
||||
// value: 10,
|
||||
// message: '비고는 10자 이상으로 작성해주세요.'
|
||||
// },
|
||||
// validate: {
|
||||
// ng: (value, formValues) => {
|
||||
// // 부적절한 단어 준비
|
||||
// const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
// // 입력 문자열에 부적절한 단어가 포함되어 있는지 판단
|
||||
// for (const ng of ngs) {
|
||||
// if (value.includes(ng)) {
|
||||
// return '비고에 적절하지 않은 단어가 포함되어 있습니다.';
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// },
|
||||
// })} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit"
|
||||
// disabled={!isDirty || !isValid}>제출하기</button>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-3-5
|
||||
// import { useForm } from 'react-hook-form';
|
||||
|
||||
// export default function FormBasic() {
|
||||
// // 기본값 준비
|
||||
// const defaultValues = {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// };
|
||||
|
||||
// // 폼 초기화
|
||||
// const { register, handleSubmit,
|
||||
// formState: { errors, isDirty, isValid, isSubmitting } } = useForm({
|
||||
// defaultValues
|
||||
// });
|
||||
|
||||
// // 제출 시 4000밀리초로 처리(더미 지연 처리)
|
||||
// const onsubmit = data => {
|
||||
// return new Promise(resolve => {
|
||||
// setTimeout(() => {
|
||||
// resolve();
|
||||
// console.log(data);
|
||||
// }, 4000);
|
||||
// });
|
||||
// };
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// {/* 검증 규칙 등을 폼에 연결 */}
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name', {
|
||||
// required: '이름은 필수 입력 항목입니다.',
|
||||
// maxLength: {
|
||||
// value: 20,
|
||||
// message: '이름은 20자 이내로 작성해주세요.'
|
||||
// }
|
||||
// })}
|
||||
// />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender', {
|
||||
// required: '성별은 필수입니다.',
|
||||
// })} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender', {
|
||||
// required: '성펼은 필수입니다.',
|
||||
// })} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email', {
|
||||
// required: '이메일 주소는 필수 입력사항입니다.',
|
||||
// pattern: {
|
||||
// value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
|
||||
// message: '이메일 주소 형식이 잘못되었습니다.'
|
||||
// }
|
||||
// })} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo', {
|
||||
// required: '비고는 필수 입력 항목입니다.',
|
||||
// minLength: {
|
||||
// value: 10,
|
||||
// message: '비고는 10자 이상으로 작성해주세요.'
|
||||
// },
|
||||
// validate: {
|
||||
// ng: (value, formValues) => {
|
||||
// // 부적절한 단어 준비
|
||||
// const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
// // 입력 문자열에 부적절한 단어가 포함되어 있는지 판단
|
||||
// for (const ng of ngs) {
|
||||
// if (value.includes(ng)) {
|
||||
// return '비고에 적절하지 않은 단어가 포함되어 있습니다.';
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// },
|
||||
// })} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit"
|
||||
// disabled={!isDirty || !isValid || isSubmitting}>제출하기</button>
|
||||
// {isSubmitting && <div>...제출 중...</div>}
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
31
modern-react/my-react/src/chap04/FormCheck.js
Normal file
31
modern-react/my-react/src/chap04/FormCheck.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormCheck() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
agreement: true
|
||||
});
|
||||
|
||||
// 체크박스 변경 시 입력값 State에 반영
|
||||
const handleFormCheck = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.checked
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`동의 확인: ${form.agreement ? '동의': '동의하지 않음'}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<label htmlFor="agreement">동의합니다:</label>
|
||||
<input id="agreement" name="agreement" type="checkbox"
|
||||
checked={form.agreement}
|
||||
onChange={handleFormCheck} /><br />
|
||||
<button type="button" onClick={show}>보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
59
modern-react/my-react/src/chap04/FormCheckMulti.js
Normal file
59
modern-react/my-react/src/chap04/FormCheckMulti.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormCheckMulti() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
animal: ['dog', 'hamster']
|
||||
});
|
||||
|
||||
// 체크박스 변경 시 입력값 State에 반영
|
||||
const handleFormMulti = e => {
|
||||
const fa = form.animal;
|
||||
// 체크 시 배열에 값 추가, 체크 해제 시 삭제
|
||||
if (e.target.checked) {
|
||||
fa.push(e.target.value);
|
||||
} else {
|
||||
fa.splice(fa.indexOf(e.target.value), 1);
|
||||
}
|
||||
// 편집된 배열을 State에 반영
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: fa
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`좋아하는 동물:${form.animal}`);
|
||||
};
|
||||
|
||||
// 개별 체크박스에 체크 여부 반영
|
||||
return (
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>좋아하는 동물:</legend>
|
||||
<label htmlFor="animal_dog">개</label>
|
||||
<input id="animal_dog" name="animal"
|
||||
type="checkbox" value="dog"
|
||||
checked={form.animal.includes('dog')}
|
||||
onChange={handleFormMulti} /><br />
|
||||
<label htmlFor="animal_cat">고양이</label>
|
||||
<input id="animal_cat" name="animal"
|
||||
type="checkbox" value="cat"
|
||||
checked={form.animal.includes('cat')}
|
||||
onChange={handleFormMulti} /><br />
|
||||
<label htmlFor="animal_hamster">햄스터</label>
|
||||
<input id="animal_hamster" name="animal"
|
||||
type="checkbox" value="hamster"
|
||||
checked={form.animal.includes('hamster')}
|
||||
onChange={handleFormMulti} /><br />
|
||||
<label htmlFor="animal_rabbit">토끼</label>
|
||||
<input id="animal_rabbit" name="animal"
|
||||
type="checkbox" value="rabbit"
|
||||
checked={form.animal.includes('rabbit')}
|
||||
onChange={handleFormMulti} /><br />
|
||||
</fieldset>
|
||||
<button type="button" onClick={show}>보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
25
modern-react/my-react/src/chap04/FormFile.js
Normal file
25
modern-react/my-react/src/chap04/FormFile.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export default function FormFile() {
|
||||
// 파일 입력창에 대한 참조
|
||||
const file = useRef(null);
|
||||
|
||||
// [보내기] 버튼 클릭 후 파일 정보 로그 출력
|
||||
function show() {
|
||||
const fs = file.current.files;
|
||||
// 획득한 파일군을 순서대로 스캔
|
||||
for(const f of fs){
|
||||
console.log(`파일명:${f.name}`);
|
||||
console.log(`종류:${f.type}`);
|
||||
console.log(`크기:${Math.trunc(f.size / 1024)}KB`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input type="file" ref={file} multiple />
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
47
modern-react/my-react/src/chap04/FormList.js
Normal file
47
modern-react/my-react/src/chap04/FormList.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormList() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
animal: ['dog', 'hamster']
|
||||
});
|
||||
|
||||
// 셀렉트 박스 변경 시 입력값을 State에 반영
|
||||
const handleFormList = e => {
|
||||
// 선택값을 저장하기 위한 배열
|
||||
const data = [];
|
||||
// <option> 요소를 순차적으로 스캔하여 선택 상태의 값을 배열에 추가한다.
|
||||
const opts = e.target.options;
|
||||
for (const opt of opts) {
|
||||
if (opt.selected) {
|
||||
data.push(opt.value);
|
||||
}
|
||||
}
|
||||
// 최종 결과를 State에 반영
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: data
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`좋아하는 동물:${form.animal}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<label htmlFor="animal">좋아하는 동물:</label><br />
|
||||
<select id="animal" name="animal"
|
||||
value={form.animal}
|
||||
size="4" multiple={true}
|
||||
onChange={handleFormList}>
|
||||
<option value="dog">개</option>
|
||||
<option value="cat">고양이</option>
|
||||
<option value="hamster">햄스터</option>
|
||||
<option value="rabbit">토끼</option>
|
||||
</select>
|
||||
<button type="button" onClick={show}>보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
46
modern-react/my-react/src/chap04/FormRadio.js
Normal file
46
modern-react/my-react/src/chap04/FormRadio.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormRadio() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
os: 'windows'
|
||||
});
|
||||
|
||||
// 라디오 버튼 변경 시 입력 값을 State에 반영
|
||||
const handleForm = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`사용OS:${form.os}`);
|
||||
};
|
||||
|
||||
// State의 현재 값에 따라 checked 속성 값을 결정한다.
|
||||
return (
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>사용OS:</legend>
|
||||
<label htmlFor="os_win">Windows</label>
|
||||
<input id="os_win" name="os"
|
||||
type="radio" value="windows"
|
||||
checked={form.os === 'windows'}
|
||||
onChange={handleForm} /><br />
|
||||
<label htmlFor="os_mac">macOS</label>
|
||||
<input id="os_mac" name="os"
|
||||
type="radio" value="mac"
|
||||
checked={form.os === 'mac'}
|
||||
onChange={handleForm} /><br />
|
||||
<label htmlFor="os_lin">Linux</label>
|
||||
<input id="os_lin" name="os"
|
||||
type="radio" value="linux"
|
||||
checked={form.os === 'linux'}
|
||||
onChange={handleForm} />
|
||||
</fieldset>
|
||||
<button type="button" onClick={show}>보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
36
modern-react/my-react/src/chap04/FormSelect.js
Normal file
36
modern-react/my-react/src/chap04/FormSelect.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormSelect() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
animal: 'dog'
|
||||
});
|
||||
|
||||
// 선택 상자 변경 시 입력값을 State에 반영
|
||||
const handleForm = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`좋아하는 동물:${form.animal}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<label htmlFor="animal">좋아하는 동물:</label>
|
||||
<select id="animal" name="animal"
|
||||
value={form.animal}
|
||||
onChange={handleForm}>
|
||||
<option value="dog">개</option>
|
||||
<option value="cat">고양이</option>
|
||||
<option value="hamster">햄스터</option>
|
||||
<option value="rabbit">토끼</option>
|
||||
</select>
|
||||
<button type="button" onClick={show}>보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
33
modern-react/my-react/src/chap04/FormTextarea.js
Normal file
33
modern-react/my-react/src/chap04/FormTextarea.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function FormTextarea() {
|
||||
// State 초기화
|
||||
const [form, setForm] = useState({
|
||||
comment: `다양한 폼 요소를 리액트로 구현하는 방법에 대해서 알아보겠습니다. \n참고로 <input> 요소에서는 type 속성을 변경하여 숫자 스피너, 날짜 입력 박스 등 다양한 입력 박스를 표현할 수 있습니다.`
|
||||
});
|
||||
|
||||
// 텍스트 영역 변경 시 입력 값을 State에 반영
|
||||
const handleForm = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭 시 입력값 로그 출력
|
||||
const show = () => {
|
||||
console.log(`댓글: ${form.comment}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<label htmlFor="comment">댓글: </label><br />
|
||||
<textarea id="comment" name="comment"
|
||||
cols="30" rows="7"
|
||||
value={form.comment}
|
||||
onChange={handleForm}></textarea><br />
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
408
modern-react/my-react/src/chap04/FormYup.js
Normal file
408
modern-react/my-react/src/chap04/FormYup.js
Normal file
@@ -0,0 +1,408 @@
|
||||
// import { useForm } from 'react-hook-form';
|
||||
// import { yupResolver } from '@hookform/resolvers/yup';
|
||||
// import * as yup from 'yup';
|
||||
|
||||
// /* eslint-disable no-template-curly-in-string */
|
||||
// // 검증 규칙 준비
|
||||
// const schema = yup.object({
|
||||
// name: yup
|
||||
// .string()
|
||||
// .label('이름')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .max(20, '${label}은 ${max}자 이내로 입력하세요.'),
|
||||
// gender: yup
|
||||
// .string()
|
||||
// .label('성별')
|
||||
// .required('${label}은 필수 입력입니다.'),
|
||||
// email: yup
|
||||
// .string()
|
||||
// .label('이메일 주소')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .email('${label}의 형식이 잘못되었습니다.'),
|
||||
// memo: yup
|
||||
// .string()
|
||||
// .label('비고')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .min(10, '${label}은 ${min}자 이상으로 입력하세요.')
|
||||
// // .test('ng',
|
||||
// // ({ label }) => `${label}にNGワードが含まれています`,
|
||||
// // value => {
|
||||
// // const ngs = ['暴力', '死', 'グロ'];
|
||||
// // for (const ng of ngs) {
|
||||
// // if (value.includes(ng)) {
|
||||
// // return false;
|
||||
// // }
|
||||
// // }
|
||||
// // return true;
|
||||
// // })
|
||||
// // .ng()
|
||||
// });
|
||||
// /* eslint-enable no-template-curly-in-string */
|
||||
|
||||
// export default function FormYup() {
|
||||
// const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
// defaultValues: {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// },
|
||||
// // Yup에게 검증을 맡기다
|
||||
// resolver: yupResolver(schema),
|
||||
// });
|
||||
|
||||
// // 제출 시 처리 준비
|
||||
// const onsubmit = data => console.log(data);
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name')} />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender')} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender')} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email')} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo')} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit">제출하기</button>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// Code 4-3-8
|
||||
// import { useForm } from 'react-hook-form';
|
||||
// import { yupResolver } from '@hookform/resolvers/yup';
|
||||
// import * as yup from 'yup';
|
||||
|
||||
// /* eslint-disable no-template-curly-in-string */
|
||||
// // 검증 규칙 준비
|
||||
// const schema = yup.object({
|
||||
// name: yup
|
||||
// .string()
|
||||
// .label('이름')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .max(20, '${label}은 ${max}자 이내로 입력하세요.'),
|
||||
// gender: yup
|
||||
// .string()
|
||||
// .label('성별')
|
||||
// .required('${label}은 필수 입력입니다.'),
|
||||
// email: yup
|
||||
// .string()
|
||||
// .label('이메일 주소')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .email('${label}의 형식이 잘못되었습니다.'),
|
||||
// memo: yup
|
||||
// .string()
|
||||
// .label('비고')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .min(10, '${label}은 ${min}자 이상으로 입력하세요.')
|
||||
// .test('ng',
|
||||
// ({ label }) => `${label}에 적절하지 않은 단어가 포함되어 있습니다.`,
|
||||
// value => {
|
||||
// // 부적절한 단어 준비
|
||||
// const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
// // 입력 문자열에 부적절한 단어가 포함되었는지 판단
|
||||
// for (const ng of ngs) {
|
||||
// if (value.includes(ng)) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// })
|
||||
// });
|
||||
// /* eslint-enable no-template-curly-in-string */
|
||||
|
||||
// export default function FormYup() {
|
||||
// const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
// defaultValues: {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// },
|
||||
// // Yup에게 검증을 맡기다
|
||||
// resolver: yupResolver(schema),
|
||||
// });
|
||||
|
||||
// // 제출 시 처리 준비
|
||||
// const onsubmit = data => console.log(data);
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name')} />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender')} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender')} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email')} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo')} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit">제출하기</button>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-3-9
|
||||
// import { useForm } from 'react-hook-form';
|
||||
// import { yupResolver } from '@hookform/resolvers/yup';
|
||||
// import * as yup from 'yup';
|
||||
|
||||
// // ng 규칙 추가
|
||||
// yup.addMethod(yup.string, 'ng', function() {
|
||||
// return this.test('ng',
|
||||
// ({ label }) => `${label}에 적절하지 않은 단어가 포함되어 있습니다.`,
|
||||
// value => {
|
||||
// const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
// for (const ng of ngs) {
|
||||
// if (value.includes(ng)) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
|
||||
// /* eslint-disable no-template-curly-in-string */
|
||||
// // 검증 규칙 준비
|
||||
// const schema = yup.object({
|
||||
// name: yup
|
||||
// .string()
|
||||
// .label('이름')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .max(20, '${label}은 ${max}자 이내로 입력하세요.'),
|
||||
// gender: yup
|
||||
// .string()
|
||||
// .label('성별')
|
||||
// .required('${label}은 필수 입력입니다.'),
|
||||
// email: yup
|
||||
// .string()
|
||||
// .label('이메일 주소')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .email('${label}의 형식이 잘못되었습니다.'),
|
||||
// // memo 필드에 ng 규칙 적용
|
||||
// memo: yup
|
||||
// .string()
|
||||
// .label('비고')
|
||||
// .required('${label}은 필수 입력입니다.')
|
||||
// .min(10, '${label}은 ${min}자 이상으로 입력하세요.')
|
||||
// .ng()
|
||||
// });
|
||||
// /* eslint-enable no-template-curly-in-string */
|
||||
|
||||
// export default function FormYup() {
|
||||
// const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
// defaultValues: {
|
||||
// name: '홍길동',
|
||||
// email: 'admin@example.com',
|
||||
// gender: 'male',
|
||||
// memo: ''
|
||||
// },
|
||||
// // Yup에게 검증을 맡기다
|
||||
// resolver: yupResolver(schema),
|
||||
// });
|
||||
|
||||
// // 제출 시 처리 준비
|
||||
// const onsubmit = data => console.log(data);
|
||||
// const onerror = err => console.log(err);
|
||||
|
||||
// return (
|
||||
// <form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label><br/>
|
||||
// <input id="name" type="text"
|
||||
// {...register('name')} />
|
||||
// <div>{errors.name?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="gender">성별:</label><br/>
|
||||
// <label>
|
||||
// <input type="radio" value="male"
|
||||
// {...register('gender')} />남성
|
||||
// </label>
|
||||
// <label>
|
||||
// <input type="radio" value="female"
|
||||
// {...register('gender')} />여성
|
||||
// </label>
|
||||
// <div>{errors.gender?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label><br/>
|
||||
// <input id="email" type="email"
|
||||
// {...register('email')} />
|
||||
// <div>{errors.email?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="memo">비고:</label><br/>
|
||||
// <textarea id="memo"
|
||||
// {...register('memo')} />
|
||||
// <div>{errors.memo?.message}</div>
|
||||
// </div>
|
||||
// <div>
|
||||
// <button type="submit">제출하기</button>
|
||||
// </div>
|
||||
// </form>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-3-10
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
// ng 규칙 추가
|
||||
yup.addMethod(yup.string, 'ng', function() {
|
||||
return this.test('ng',
|
||||
({ label }) => `${label}에 적절하지 않은 단어가 포함되어 있습니다.`,
|
||||
value => {
|
||||
const ngs = ['폭력', '죽음', '그로테스크'];
|
||||
for (const ng of ngs) {
|
||||
if (value.includes(ng)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
// 검증 규칙 준비
|
||||
const schema = yup.object({
|
||||
name: yup
|
||||
.string()
|
||||
.label('이름')
|
||||
.trim().lowercase()
|
||||
// .transform((value, orgValue) => value.normalize('NFKC'))
|
||||
.required('${label}은 필수 입력입니다.')
|
||||
.max(20, '${label}은 ${max}자 이내로 입력하세요.'),
|
||||
gender: yup
|
||||
.string()
|
||||
.label('성별')
|
||||
.required('${label}은 필수 입력입니다.'),
|
||||
email: yup
|
||||
.string()
|
||||
.label('이메일 주소')
|
||||
.required('${label}은 필수 입력입니다.')
|
||||
.email('${label}의 형식이 잘못되었습니다.'),
|
||||
// memo 필드에 ng 규칙 적용
|
||||
memo: yup
|
||||
.string()
|
||||
.label('비고')
|
||||
.required('${label}은 필수 입력입니다.')
|
||||
.min(10, '${label}은 ${min}자 이상으로 입력하세요.')
|
||||
.ng()
|
||||
});
|
||||
/* eslint-enable no-template-curly-in-string */
|
||||
|
||||
export default function FormYup() {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
defaultValues: {
|
||||
name: '홍길동',
|
||||
email: 'admin@example.com',
|
||||
gender: 'male',
|
||||
memo: ''
|
||||
},
|
||||
// Yup에게 검증을 맡기다
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
// 제출 시 처리 준비
|
||||
const onsubmit = data => console.log(data);
|
||||
const onerror = err => console.log(err);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label><br/>
|
||||
<input id="name" type="text"
|
||||
{...register('name')} />
|
||||
<div>{errors.name?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="gender">성별:</label><br/>
|
||||
<label>
|
||||
<input type="radio" value="male"
|
||||
{...register('gender')} />남성
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" value="female"
|
||||
{...register('gender')} />여성
|
||||
</label>
|
||||
<div>{errors.gender?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email">이메일 주소:</label><br/>
|
||||
<input id="email" type="email"
|
||||
{...register('email')} />
|
||||
<div>{errors.email?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="memo">비고:</label><br/>
|
||||
<textarea id="memo"
|
||||
{...register('memo')} />
|
||||
<div>{errors.memo?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">제출하기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
77
modern-react/my-react/src/chap04/FromKorean.js
Normal file
77
modern-react/my-react/src/chap04/FromKorean.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import yup from './yup.kr.js';
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup
|
||||
.string()
|
||||
.label('이름')
|
||||
.required()
|
||||
.max(20),
|
||||
gender: yup
|
||||
.string()
|
||||
.label('성별')
|
||||
.required(),
|
||||
email: yup
|
||||
.string()
|
||||
.label('이메일 주소')
|
||||
.required()
|
||||
.email(),
|
||||
memo: yup
|
||||
.string()
|
||||
.label('비고')
|
||||
.required()
|
||||
.min(10)
|
||||
});
|
||||
|
||||
export default function FormYup() {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
defaultValues: {
|
||||
name: '홍길동',
|
||||
email: 'admin@example.com',
|
||||
gender: 'male',
|
||||
memo: ''
|
||||
},
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
const onsubmit = data => console.log(data);
|
||||
const onerror = err => console.log(err);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label><br/>
|
||||
<input id="name" type="text"
|
||||
{...register('name')} />
|
||||
<div>{errors.name?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="gender">성별:</label><br/>
|
||||
<label>
|
||||
<input type="radio" value="male"
|
||||
{...register('gender')} />남성
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" value="female"
|
||||
{...register('gender')} />여성
|
||||
</label>
|
||||
<div>{errors.gender?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email">이메일 주소:</label><br/>
|
||||
<input id="email" type="email"
|
||||
{...register('email')} />
|
||||
<div>{errors.email?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="memo">비고:</label><br/>
|
||||
<textarea id="memo"
|
||||
{...register('memo')} />
|
||||
<div>{errors.memo?.message}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">비고</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
43
modern-react/my-react/src/chap04/StateForm.js
Normal file
43
modern-react/my-react/src/chap04/StateForm.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function StateForm() {
|
||||
// 폼으로 취급하는 값을 State로 선언
|
||||
const [form, setForm] = useState({
|
||||
name: '홍길동',
|
||||
age: 18
|
||||
});
|
||||
|
||||
// 폼 요소의 변경 사항을 State에 반영
|
||||
const handleForm = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼으로 로그에 메시지 출력하기
|
||||
const show = () => {
|
||||
console.log(`안녕하세요, ${form.name}(${form.age}세) 님!`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
{/* 개별 폼 요소에 State 값 할당 */}
|
||||
<div>
|
||||
<label htmlFor="name">이름: </label>
|
||||
<input id="name" name="name" type="text"
|
||||
onChange={handleForm} value={form.name} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="age">나이:</label>
|
||||
<input id="age" name="age" type="number"
|
||||
onChange={handleForm} value={form.age} />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</div>
|
||||
<p>안녕하세요, {form.name}({form.age}세) 님!</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
33
modern-react/my-react/src/chap04/StateFormUC.js
Normal file
33
modern-react/my-react/src/chap04/StateFormUC.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
export default function StateFormUC() {
|
||||
// 리액트 요소에 대한 참조 준비
|
||||
const name = useRef(null);
|
||||
const age = useRef(null);
|
||||
|
||||
// 요소(참조)를 통해 입력값 준비하기
|
||||
const show = () => {
|
||||
console.log(`안녕하세요, ${name.current.value}(${age.current.value}세) 님!`);
|
||||
};
|
||||
|
||||
// 폼 그리기
|
||||
return (
|
||||
<form>
|
||||
{/* 준비된 레퍼런스를 각 요소에 연결 */}
|
||||
<div>
|
||||
<label htmlFor="name">이름: </label>
|
||||
<input id="name" name="name" type="text"
|
||||
ref={name} defaultValue="홍길동" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="age">나이: </label>
|
||||
<input id="age" name="age" type="number"
|
||||
ref={age} defaultValue="18" />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
60
modern-react/my-react/src/chap04/StateNest.js
Normal file
60
modern-react/my-react/src/chap04/StateNest.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function StateNest() {
|
||||
// 인자 배열을 State로 선언
|
||||
const [form, setForm] = useState({
|
||||
name: '홍길동',
|
||||
address: {
|
||||
city: '태안',
|
||||
do: '충청남도'
|
||||
}
|
||||
});
|
||||
|
||||
// 1단계 요소를 업데이트하는 핸들러
|
||||
const handleForm = e => {
|
||||
setForm({
|
||||
...form,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
// 2단계 요소를 업데이트하는 핸들러
|
||||
const handleFormNest = e => {
|
||||
setForm({
|
||||
...form,
|
||||
address: {
|
||||
...form.address,
|
||||
[e.target.name]: e.target.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// [보내기] 버튼 클릭으로 폼 정보 로그 출력
|
||||
const show = () => {
|
||||
console.log(`${form.name}(${form.address.do}・${form.address.city})`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label>
|
||||
<input id="name" name="name" type="text"
|
||||
onChange={handleForm} value={form.name} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="do">주소(도):</label>
|
||||
<input id="do" name="do" type="text"
|
||||
onChange={handleFormNest} value={form.address.do} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="city">주소(시/군/구):</label>
|
||||
<input id="city" name="city" type="text"
|
||||
onChange={handleFormNest} value={form.address.city} />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
54
modern-react/my-react/src/chap04/StateNestImmer.js
Normal file
54
modern-react/my-react/src/chap04/StateNestImmer.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useImmer } from 'use-immer';
|
||||
|
||||
export default function StateNestImmer() {
|
||||
// 폼으로 취급하는 값을 State로 선언
|
||||
const [form, setForm] = useImmer({
|
||||
name: '홍길동',
|
||||
address: {
|
||||
city: '태안',
|
||||
do: '충청남도'
|
||||
}
|
||||
});
|
||||
|
||||
// 1단계 요소를 업데이트하는 핸들러
|
||||
const handleForm = e => {
|
||||
setForm(form => {
|
||||
form[e.target.name] = e.target.value;
|
||||
});
|
||||
};
|
||||
|
||||
// 2단계 요소를 업데이트하는 핸들러
|
||||
const handleFormNest = e => {
|
||||
setForm(form => {
|
||||
form.address[e.target.name] = e.target.value;
|
||||
});
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
console.log(`${form.name}(${form.address.do}・${form.address.city})`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label>
|
||||
<input id="name" name="name" type="text"
|
||||
onChange={handleForm} value={form.name} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="do">주소(도):</label>
|
||||
<input id="do" name="do" type="text"
|
||||
onChange={handleFormNest} value={form.address.do} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="city">주소(시/군/구):</label>
|
||||
<input id="city" name="city" type="text"
|
||||
onChange={handleFormNest} value={form.address.city} />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
52
modern-react/my-react/src/chap04/StateNestImmer2.js
Normal file
52
modern-react/my-react/src/chap04/StateNestImmer2.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useImmer } from 'use-immer';
|
||||
|
||||
export default function StateNestImmer2() {
|
||||
const [form, setForm] = useImmer({
|
||||
name: '홍길동',
|
||||
address: {
|
||||
city: '태안',
|
||||
do: '충청남도'
|
||||
}
|
||||
});
|
||||
|
||||
const handleNest = e => {
|
||||
// 요소명을 ".으로 분해(요소 이름이 'xxxxxx.xxxxxx'라는 가정 하에)
|
||||
const ns = e.target.name.split('.');
|
||||
setForm(form => {
|
||||
// 계층에 따라 대위임처를 변경한다.
|
||||
if (ns.length === 1) {
|
||||
form[ns[0]] = e.target.value;
|
||||
} else {
|
||||
form[ns[0]][ns[1]] = e.target.value;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
console.log(`${form.name}(${form.address.prefecture}・${form.address.city})`);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label>
|
||||
<input id="name" name="name" type="text"
|
||||
onChange={handleNest} value={form.name} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="do">주소(도):</label>
|
||||
<input id="do" name="address.do" type="text"
|
||||
onChange={handleNest} value={form.address.do} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="city">주소(시/군/구):</label>
|
||||
<input id="city" name="address.city" type="text"
|
||||
onChange={handleNest} value={form.address.city} />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={show}>
|
||||
보내기</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
3
modern-react/my-react/src/chap04/StateTodo.css
Normal file
3
modern-react/my-react/src/chap04/StateTodo.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.done {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
305
modern-react/my-react/src/chap04/StateTodo.js
Normal file
305
modern-react/my-react/src/chap04/StateTodo.js
Normal file
@@ -0,0 +1,305 @@
|
||||
// Code 4-2-7
|
||||
import { useState } from 'react';
|
||||
|
||||
// Todo 항목 id의 최대값(등록할 때마다 증가)
|
||||
let maxId = 0;
|
||||
export default function StateTodo() {
|
||||
// 입력값(title), 할 일 목록(todo)을 State로 관리
|
||||
const [title, setTitle] = useState('');
|
||||
const [todo, setTodo] = useState([]);
|
||||
|
||||
// 텍스트 상자에 입력한 내용을 State에 반영
|
||||
const handleChangeTitle = e => {
|
||||
setTitle(e.target.value);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
// 새 할 일 추가하기
|
||||
setTodo([
|
||||
...todo,
|
||||
{
|
||||
id: ++maxId, // id 값
|
||||
title, // Todo 본체
|
||||
created: new Date(), // 생성 날짜 및 시각
|
||||
isDone: false // 실행 완료?
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
해야 할 일:
|
||||
<input type="text" name="title"
|
||||
value={title} onChange={handleChangeTitle} />
|
||||
</label>
|
||||
<button type="button"
|
||||
onClick={handleClick}>추가하기</button>
|
||||
<hr />
|
||||
{/* 할 일을 목록으로 정리하기 */}
|
||||
<ul>
|
||||
{todo.map(item => (
|
||||
<li key={item.id}>{item.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 4-2-9
|
||||
// import { useState } from 'react';
|
||||
// import './StateTodo.css';
|
||||
|
||||
// let maxId = 0;
|
||||
// export default function StateTodo() {
|
||||
// // 입력값(title), 할 일 목록(todo)을 State로 관리
|
||||
// const [title, setTitle] = useState('');
|
||||
// const [todo, setTodo] = useState([]);
|
||||
|
||||
// // 텍스트 상자에 입력한 내용을 State에 반영
|
||||
// const handleChangeTitle = e => {
|
||||
// setTitle(e.target.value);
|
||||
// };
|
||||
|
||||
// const handleClick = () => {
|
||||
// // 새 할 일 추가하기
|
||||
// setTodo([
|
||||
// ...todo,
|
||||
// {
|
||||
// id: ++maxId, // id 값
|
||||
// title, // Todo 본체
|
||||
// created: new Date(), // 생성 날짜 및 시각
|
||||
// isDone: false // 실행 완료?
|
||||
// }
|
||||
// ]);
|
||||
// };
|
||||
|
||||
// // [완료] 버튼으로 Todo 항목을 완료 상태로 변경
|
||||
// const handleDone = e => {
|
||||
// // todo 배열을 스캔하여 id 값이 같은 것을 검색한다.
|
||||
// setTodo(todo.map(item => {
|
||||
// if (item.id === Number(e.target.dataset.id)) {
|
||||
// return {
|
||||
// ...item,
|
||||
// isDone: true
|
||||
// };
|
||||
// } else {
|
||||
// return item;
|
||||
// }
|
||||
// }));
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <label>
|
||||
// 해야 할 일:
|
||||
// <input type="text" name="title"
|
||||
// value={title} onChange={handleChangeTitle} />
|
||||
// </label>
|
||||
// <button type="button"
|
||||
// onClick={handleClick}>추가하기</button>
|
||||
// {/* 할 일을 목록으로 정리하기 */}
|
||||
|
||||
// <ul>
|
||||
// {todo.map(item => (
|
||||
// <li key={item.id}
|
||||
// className={item.isDone ? 'done' : ''}>
|
||||
// {item.title}
|
||||
// <button type="button"
|
||||
// onClick={handleDone} data-id={item.id}>완료
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-2-11
|
||||
// import { useState } from 'react';
|
||||
// import './StateTodo.css';
|
||||
|
||||
// let maxId = 0;
|
||||
// export default function StateTodo() {
|
||||
// // 입력값(title), 할 일 목록(todo)을 State로 관리
|
||||
// const [title, setTitle] = useState('');
|
||||
// const [todo, setTodo] = useState([]);
|
||||
|
||||
// // 텍스트 상자에 입력한 내용을 State에 반영
|
||||
// const handleChangeTitle = e => {
|
||||
// setTitle(e.target.value);
|
||||
// };
|
||||
|
||||
// const handleClick = () => {
|
||||
// // 새 할 일 추가하기
|
||||
// setTodo([
|
||||
// ...todo,
|
||||
// {
|
||||
// id: ++maxId, // id 값
|
||||
// title, // Todo 본체
|
||||
// created: new Date(), // 생성 날짜 및 시각
|
||||
// isDone: false // 실행 완료?
|
||||
// }
|
||||
// ]);
|
||||
// };
|
||||
|
||||
// // [완료] 버튼으로 Todo 항목을 완료 상태로 변경
|
||||
// const handleDone = e => {
|
||||
// // todo 배열을 스캔하여 id 값이 같은 것을 검색한다.
|
||||
// setTodo(todo.map(item => {
|
||||
// if (item.id === Number(e.target.dataset.id)) {
|
||||
// return {
|
||||
// ...item,
|
||||
// isDone: true
|
||||
// };
|
||||
// } else {
|
||||
// return item;
|
||||
// }
|
||||
// }));
|
||||
// };
|
||||
|
||||
// // [삭제] 버튼으로 해당 Todo 항목을 삭제한다.
|
||||
// const handleRemove = e => {
|
||||
// setTodo(todo.filter(item =>
|
||||
// item.id !== Number(e.target.dataset.id)
|
||||
// ));
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <label>
|
||||
// 해야 할 일:
|
||||
// <input type="text" name="title"
|
||||
// value={title} onChange={handleChangeTitle} />
|
||||
// </label>
|
||||
// <button type="button"
|
||||
// onClick={handleClick}>추가하기</button>
|
||||
// <hr />
|
||||
// {/* 할 일을 목록으로 정리하기 */}
|
||||
|
||||
// <ul>
|
||||
// {todo.map(item => (
|
||||
// <li key={item.id}
|
||||
// className={item.isDone ? 'done' : ''}>
|
||||
// {item.title}
|
||||
// <button type="button"
|
||||
// onClick={handleDone} data-id={item.id}>완료
|
||||
// </button>
|
||||
// <button type="button"
|
||||
// onClick={handleRemove} data-id={item.id}>삭제
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 4-2-12
|
||||
// import { useState } from 'react';
|
||||
// import './StateTodo.css';
|
||||
|
||||
// let maxId = 0;
|
||||
// export default function StateTodo() {
|
||||
// // 다음 정렬 방향 (내림차순인 경우 true)
|
||||
// const [desc, setDesc] = useState(true);
|
||||
// // 입력값(title), 할 일 목록(todo)을 State로 관리
|
||||
// const [title, setTitle] = useState('');
|
||||
// const [todo, setTodo] = useState([]);
|
||||
|
||||
// // 텍스트 상자에 입력한 내용을 State에 반영
|
||||
// const handleChangeTitle = e => {
|
||||
// setTitle(e.target.value);
|
||||
// };
|
||||
|
||||
// const handleClick = () => {
|
||||
// // 새 할 일 추가하기
|
||||
// setTodo([
|
||||
// ...todo,
|
||||
// {
|
||||
// id: ++maxId, // id 값
|
||||
// title, // Todo 본체
|
||||
// created: new Date(), // 생성 날짜 및 시각
|
||||
// isDone: false // 실행 완료?
|
||||
// }
|
||||
// ]);
|
||||
// };
|
||||
|
||||
// // [완료] 버튼으로 Todo 항목을 완료 상태로 변경
|
||||
// const handleDone = e => {
|
||||
// // todo 배열을 스캔하여 id 값이 같은 것을 검색한다.
|
||||
// setTodo(todo.map(item => {
|
||||
// if (item.id === Number(e.target.dataset.id)) {
|
||||
// return {
|
||||
// ...item,
|
||||
// isDone: true
|
||||
// };
|
||||
// } else {
|
||||
// return item;
|
||||
// }
|
||||
// }));
|
||||
// };
|
||||
|
||||
// // [삭제] 버튼으로 해당 Todo 항목을 삭제한다.
|
||||
// const handleRemove = e => {
|
||||
// setTodo(todo.filter(item =>
|
||||
// item.id !== Number(e.target.dataset.id)
|
||||
// ));
|
||||
// };
|
||||
|
||||
// const handleSort = e => {
|
||||
// // 기존 Todo 목록을 복제하여 정렬하기
|
||||
// const sorted = [...todo];
|
||||
// sorted.sort((m, n) => {
|
||||
// // desc 값에 따라 오름차순/내림차순 결정
|
||||
// if (desc) {
|
||||
// return n.created.getTime() - m.created.getTime();
|
||||
// } else {
|
||||
// return m.created.getTime() - n.created.getTime();
|
||||
// }
|
||||
// });
|
||||
// // desc 값 반전
|
||||
// setDesc(d => !d);
|
||||
// // 정렬된 목록 재설정
|
||||
// setTodo(sorted);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <label>
|
||||
// 해야 할 일:
|
||||
// <input type="text" name="title"
|
||||
// value={title} onChange={handleChangeTitle} />
|
||||
// </label>
|
||||
// <button type="button"
|
||||
// onClick={handleClick}>추가하기</button>
|
||||
// {/* desc 값에 따라 캡션 변경 */}
|
||||
// <button type="button"
|
||||
// onClick={handleSort}>
|
||||
// 정렬({desc ? '↑' : '↓'})</button>
|
||||
// <hr />
|
||||
|
||||
// {/* 할 일을 목록으로 정리하기 */}
|
||||
// <ul>
|
||||
// {todo.map(item => (
|
||||
// <li key={item.id}
|
||||
// className={item.isDone ? 'done' : ''}>
|
||||
// {item.title}
|
||||
// <button type="button"
|
||||
// onClick={handleDone} data-id={item.id}>완료
|
||||
// </button>
|
||||
// <button type="button"
|
||||
// onClick={handleRemove} data-id={item.id}>삭제
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
35
modern-react/my-react/src/chap04/yup.kr.js
Normal file
35
modern-react/my-react/src/chap04/yup.kr.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
// 오류 메시지 정보 선언
|
||||
const krLocale = {
|
||||
mixed: {
|
||||
required: param => `${param.label}은/는 필수입니다.`,
|
||||
oneOf: param => `${param.label}은/는 ${param.values} 중 하나여야 합니다.`,
|
||||
},
|
||||
string: {
|
||||
length: param => `${param.label}은/는 ${param.length}글자여야 합니다.`,
|
||||
min: param => `${param.label}은/는 ${param.min}글자 이상이어야 합니다.`,
|
||||
max: param => `${param.label}은/는 ${param.max}글자 이하여야 합니다.`,
|
||||
matches: param => `${param.label}은/는 ${param.regex} 형식과 일치해야 합니다.`,
|
||||
email: param => `${param.label}은/는 이메일 주소 형식이어야 합니다.`,
|
||||
url: param => `${param.label}은/는 URL 형식이어야 합니다.`,
|
||||
},
|
||||
number: {
|
||||
min: param => `${param.label}은/는 ${param.min} 이상이어야 합니다.`,
|
||||
max: param => `${param.label}은/는 ${param.max} 이하여야 합니다.`,
|
||||
lessThan: param => `${param.label}은/는 ${param.less}보다 작아야 합니다.`,
|
||||
moreThan: param => `${param.label}은/는 ${param.more}보다 커야 합니다.`,
|
||||
positive: param => `${param.label}은/는 양수여야 합니다.`,
|
||||
negative: param => `${param.label}은/는 음수여야 합니다.`,
|
||||
integer: param => `${param.label}은/는 정수여야 합니다.`,
|
||||
},
|
||||
date: {
|
||||
min: param => `${param.label}은/는 ${param.min}보다 미래여야 합니다.`,
|
||||
max: param => `${param.label}은/는 ${param.max}보다 이전이어야 합니다.`,
|
||||
},
|
||||
};
|
||||
|
||||
// 메시지 정보 설정
|
||||
yup.setLocale(krLocale);
|
||||
// 설정된 Yup 내보내기
|
||||
export default yup;
|
||||
19
modern-react/my-react/src/chap05/EmotionComp.js
Normal file
19
modern-react/my-react/src/chap05/EmotionComp.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
// 스타일링된 컴포넌트 준비
|
||||
const MyPanel = styled.div`
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
export default function EmotionComp() {
|
||||
return (
|
||||
// 준비된 구성 요소 배치
|
||||
<MyPanel><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</MyPanel>
|
||||
);
|
||||
}
|
||||
67
modern-react/my-react/src/chap05/EmotionJsx.js
Normal file
67
modern-react/my-react/src/chap05/EmotionJsx.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/** @jsxImportSource @emotion/react */
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
export default function EmotionJsx() {
|
||||
const styles = css`
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
return (
|
||||
<div css={styles}><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// /** @jsxImportSource @emotion/react */
|
||||
// import { css } from '@emotion/react';
|
||||
|
||||
// export default function EmotionJsx() {
|
||||
// const styles = css({
|
||||
// width: 300,
|
||||
// padding: 10,
|
||||
// border: '1px solid #000',
|
||||
// borderRadius: 5,
|
||||
// backgroundColor: 'royalblue',
|
||||
// color: 'white',
|
||||
// });
|
||||
|
||||
// const others = css({
|
||||
// height: 150
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <div css={[styles, others]}><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</div>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// /** @jsxImportSource @emotion/react */
|
||||
// import { css } from '@emotion/react';
|
||||
|
||||
// export default function EmotionJsx() {
|
||||
// const styles = css({
|
||||
// width: 300,
|
||||
// padding: 10,
|
||||
// border: '1px solid #000',
|
||||
// borderRadius: 5,
|
||||
// backgroundColor: 'royalblue',
|
||||
// color: 'white',
|
||||
// });
|
||||
|
||||
// const plus = css`
|
||||
// ${styles}
|
||||
// margin: 20px;
|
||||
// `;
|
||||
|
||||
// return (
|
||||
// <div css={plus}><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</div>
|
||||
// );
|
||||
// }
|
||||
30
modern-react/my-react/src/chap05/ErrorEvent.js
Normal file
30
modern-react/my-react/src/chap05/ErrorEvent.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export default function ErrorEvent() {
|
||||
const handleClick = () => {
|
||||
throw new Error('Error is occured in MyApp.');
|
||||
};
|
||||
return (
|
||||
<button type="button" onClick={handleClick}>
|
||||
오류 발사
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import { useErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
// export default function ErrorEvent() {
|
||||
// const { showBoundary } = useErrorBoundary();
|
||||
// const handleClick = () => {
|
||||
// try {
|
||||
// throw new Error('Error is occured in MyApp.');
|
||||
// } catch(e) {
|
||||
// // 핸들러 내에서 발생한 예외를 Error Boundary로 넘긴다.
|
||||
// showBoundary(e);
|
||||
// }
|
||||
// };
|
||||
// return (
|
||||
// <button type="button" onClick={handleClick}>
|
||||
// 오류 발사
|
||||
// </button>
|
||||
// );
|
||||
// }
|
||||
28
modern-react/my-react/src/chap05/ErrorEventRoot.js
Normal file
28
modern-react/my-react/src/chap05/ErrorEventRoot.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import ErrorEvent from './ErrorEvent';
|
||||
|
||||
export default function ErrorEventRoot() {
|
||||
const handleFallback = ({ error, resetErrorBoundary }) => {
|
||||
const handleClick = () => resetErrorBoundary();
|
||||
return (
|
||||
<div>
|
||||
<h4>다음 오류가 발생했다.</h4>
|
||||
<p>{error.message}</p>
|
||||
<button type="button" onClick={handleClick}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const handleReset = () => console.log('Retry!!');
|
||||
return (
|
||||
<>
|
||||
<h3>Error Boundary의 기본</h3>
|
||||
<ErrorBoundary
|
||||
onReset={handleReset}
|
||||
fallbackRender={handleFallback}>
|
||||
<ErrorEvent />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
modern-react/my-react/src/chap05/ErrorFallback.js
Normal file
12
modern-react/my-react/src/chap05/ErrorFallback.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
const handleClick = () => resetErrorBoundary();
|
||||
return (
|
||||
<div>
|
||||
<h4>다음 오류가 발생했습니다.</h4>
|
||||
<p>{error.message}</p>
|
||||
<button type="button" onClick={handleClick}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
modern-react/my-react/src/chap05/ErrorRetryRoot.js
Normal file
71
modern-react/my-react/src/chap05/ErrorRetryRoot.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import ErrorRetryThrow from './ErrorRetryThrow';
|
||||
|
||||
export default function ErrorRetryRoot() {
|
||||
// 오류 발생 시 실행되는 처리
|
||||
const handleFallback = ({ error, resetErrorBoundary }) => {
|
||||
const handleClick = () => resetErrorBoundary();
|
||||
return (
|
||||
<div>
|
||||
<h4>다음 오류가 발생했다.</h4>
|
||||
<p>{error.message}</p>
|
||||
<button type="button" onClick={handleClick}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// 리셋 시 실행되는 처리
|
||||
const handleReset = () => console.log('Retry!!');
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Error Boundary의 기본</h3>
|
||||
<ErrorBoundary
|
||||
onReset={handleReset}
|
||||
fallbackRender={handleFallback}
|
||||
>
|
||||
<ErrorRetryThrow />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 5-3-10
|
||||
// import { ErrorBoundary } from 'react-error-boundary';
|
||||
// import ErrorRetryThrow from './ErrorRetryThrow';
|
||||
// import ErrorFallback from './ErrorFallback';
|
||||
|
||||
// export default function ErrorRetryRoot() {
|
||||
// // 오류 발생 시 실행되는 처리
|
||||
// const handleFallback = ({ error, resetErrorBoundary }) => {
|
||||
// const handleClick = () => resetErrorBoundary();
|
||||
// return (
|
||||
// <div>
|
||||
// <h4>다음 오류가 발생했다.</h4>
|
||||
// <p>{error.message}</p>
|
||||
// <button type="button" onClick={handleClick}>
|
||||
// Retry
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
// // 리셋 시 실행되는 처리
|
||||
// const handleReset = () => console.log('Retry!!');
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <h3>Error Boundary의 기본</h3>
|
||||
// {/* 오류 발생 시 렌더링 콘텐츠를 컴포넌트로 지정 */}
|
||||
// <ErrorBoundary
|
||||
// onReset={handleReset}
|
||||
// fallbackRender={handleFallback}
|
||||
// FallbackComponent={ErrorFallback}
|
||||
// >
|
||||
// <ErrorRetryThrow />
|
||||
// </ErrorBoundary>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
9
modern-react/my-react/src/chap05/ErrorRetryThrow.js
Normal file
9
modern-react/my-react/src/chap05/ErrorRetryThrow.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default function ErrorRetryThrow() {
|
||||
// 60%의 확률로 오류 발생
|
||||
if (Math.random() < 0.6) {
|
||||
throw new Error('Error is occured in MyApp.');
|
||||
}
|
||||
return (
|
||||
<p>잘 실행되었다.</p>
|
||||
);
|
||||
}
|
||||
32
modern-react/my-react/src/chap05/ErrorRoot.js
Normal file
32
modern-react/my-react/src/chap05/ErrorRoot.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import ErrorThrow from './ErrorThrow';
|
||||
|
||||
export default function ErrorRoot() {
|
||||
return (
|
||||
<>
|
||||
<h3>Error Boundary의 기본</h3>
|
||||
<ErrorBoundary fallback={<div>오류가 발생했다.</div>}>
|
||||
<ErrorThrow />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// import { ErrorBoundary } from 'react-error-boundary';
|
||||
// import ErrorThrow from './ErrorThrow';
|
||||
|
||||
// export default function ErrorRoot() {
|
||||
// return (
|
||||
// <>
|
||||
// <h3>Error Boundary의 기본</h3>
|
||||
// <ErrorBoundary
|
||||
// onError={err => alert(err.message)}
|
||||
// fallback={<div>오류가 발생했다.</div>}
|
||||
// >
|
||||
// <ErrorThrow />
|
||||
// </ErrorBoundary>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
7
modern-react/my-react/src/chap05/ErrorThrow.js
Normal file
7
modern-react/my-react/src/chap05/ErrorThrow.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ErrorThrow() {
|
||||
// 무조건 예외 발생
|
||||
throw new Error('Error is occured in MyApp.');
|
||||
return (
|
||||
<p>잘 실행되었다.</p>
|
||||
);
|
||||
}
|
||||
10
modern-react/my-react/src/chap05/HeavyUI.js
Normal file
10
modern-react/my-react/src/chap05/HeavyUI.js
Normal file
@@ -0,0 +1,10 @@
|
||||
function sleep(delay) {
|
||||
let start = Date.now();
|
||||
while (Date.now() - start < delay);
|
||||
}
|
||||
|
||||
// delay 밀리초 지연 발생
|
||||
export default function HeavyUI({ delay }) {
|
||||
sleep(delay);
|
||||
return <p>지연 시간은 {delay}밀리초</p>;
|
||||
}
|
||||
34
modern-react/my-react/src/chap05/LazyBasic.js
Normal file
34
modern-react/my-react/src/chap05/LazyBasic.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
// ms 밀리초의 지연을 발생시키는 sleep 함수
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
// LazyButton 지연 로드
|
||||
const LazyButton = lazy(() => sleep(2000).then(() => import('./LazyButton')));
|
||||
|
||||
export default function LazyBasic() {
|
||||
// LazyButton이 로딩될 때까지 메시지를 표시한다.
|
||||
return (
|
||||
<Suspense fallback={<p>Now Loading...</p>}>
|
||||
<LazyButton />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import { Suspense, lazy } from 'react';
|
||||
// import MyLoading from './MyLoading';
|
||||
|
||||
// // ms 밀리초의 지연을 발생시키는 sleep 함수
|
||||
// const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
// // LazyButton 지연 로드
|
||||
// const LazyButton = lazy(() => import('./LazyButton'));
|
||||
|
||||
// export default function LazyBasic() {
|
||||
// // LazyButton이 로딩될 때까지 메시지를 표시한다.
|
||||
// return (
|
||||
// // 대기 상태에서는 MyLoading 컴포넌트를 표시한다.
|
||||
// <Suspense fallback= {<MyLoading />}>
|
||||
// <LazyButton />
|
||||
// </Suspense>
|
||||
// );
|
||||
// }
|
||||
7
modern-react/my-react/src/chap05/LazyButton.js
Normal file
7
modern-react/my-react/src/chap05/LazyButton.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function LazyButton() {
|
||||
return (
|
||||
<div>
|
||||
<button id="btn">버튼1</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
modern-react/my-react/src/chap05/LazyButton2.js
Normal file
7
modern-react/my-react/src/chap05/LazyButton2.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function LazyButton2() {
|
||||
return (
|
||||
<div>
|
||||
<button id="btn">버튼2</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
modern-react/my-react/src/chap05/LazyMulti.js
Normal file
16
modern-react/my-react/src/chap05/LazyMulti.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// 여러 컴포넌트 지연 로드
|
||||
const LazyButton = lazy(() => sleep(2000).then(() => import('./LazyButton')));
|
||||
const LazyButton2 = lazy(() => sleep(1000).then(() => import('./LazyButton2')));
|
||||
|
||||
export default function LazyMulti() {
|
||||
return (
|
||||
<Suspense fallback={<p>Now Loading...</p>}>
|
||||
<LazyButton />
|
||||
<LazyButton2 />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
5
modern-react/my-react/src/chap05/MyLoading.js
Normal file
5
modern-react/my-react/src/chap05/MyLoading.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function MyLoading() {
|
||||
return (
|
||||
<p>Now Loading...</p>
|
||||
);
|
||||
}
|
||||
13
modern-react/my-react/src/chap05/PortalBasic.css
Normal file
13
modern-react/my-react/src/chap05/PortalBasic.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.dialog {
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
height: 100px;
|
||||
width: 250px;
|
||||
z-index: 99999;
|
||||
display: block;
|
||||
border: 1px solid black;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
box-shadow: 5px;
|
||||
}
|
||||
28
modern-react/my-react/src/chap05/PortalBasic.js
Normal file
28
modern-react/my-react/src/chap05/PortalBasic.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import './PortalBasic.css';
|
||||
|
||||
export default function PortalBasic() {
|
||||
// 다이얼로그 창의 개폐 상태를 나타내는 State(false로 닫힌 상태)
|
||||
const [show, setShow] = useState(false);
|
||||
// 버튼 클릭 시 핸들러(State 켜기/끄기)
|
||||
const handleDialog = () => setShow(s => !s);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<button type="button" onClick={handleDialog}
|
||||
disabled={show}>
|
||||
다이얼로그 표시
|
||||
</button>
|
||||
{show && createPortal(
|
||||
<div className="dialog">
|
||||
<p>Portal에서 생성된 대화상자</p>
|
||||
<button type="button" onClick={handleDialog}>
|
||||
닫기
|
||||
</button>
|
||||
</div>,
|
||||
document.getElementById('dialog')
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
23
modern-react/my-react/src/chap05/ProfilerBasic.js
Normal file
23
modern-react/my-react/src/chap05/ProfilerBasic.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Profiler } from 'react';
|
||||
import HeavyUI from './HeavyUI';
|
||||
|
||||
export default function ProfilerBasic() {
|
||||
// 성능 측정을 위한 함수(onRender 함수)
|
||||
const handleMeasure = (id, phase, actualDuration,
|
||||
baseDuration, startTime, endTime) => {
|
||||
console.log('id: ', id);
|
||||
console.log('phase: ', phase);
|
||||
console.log('actualDuration: ', actualDuration);
|
||||
console.log('baseDuration: ', baseDuration);
|
||||
console.log('startTime: ', startTime);
|
||||
console.log('endTime', endTime);
|
||||
};
|
||||
|
||||
return (
|
||||
<Profiler id="heavy" onRender={handleMeasure}>
|
||||
<HeavyUI delay={1500} />
|
||||
<HeavyUI delay={500} />
|
||||
<HeavyUI delay={2000} />
|
||||
</Profiler>
|
||||
);
|
||||
}
|
||||
5
modern-react/my-react/src/chap05/StyledCommon.css.js
Normal file
5
modern-react/my-react/src/chap05/StyledCommon.css.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
export default css`
|
||||
margin: 20px;
|
||||
`;
|
||||
18
modern-react/my-react/src/chap05/StyledCommon.js
Normal file
18
modern-react/my-react/src/chap05/StyledCommon.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
import PanelBase from './StyledCommon.css';
|
||||
|
||||
const MyPanel = styled.div`
|
||||
${PanelBase}
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
export default function StyledCommon() {
|
||||
return (
|
||||
<MyPanel><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</MyPanel>
|
||||
);
|
||||
}
|
||||
17
modern-react/my-react/src/chap05/StyledComp.js
Normal file
17
modern-react/my-react/src/chap05/StyledComp.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// 표준 <div> 요소를 확장한 MyPanel 컴포넌트를 정의한다.
|
||||
const MyPanel = styled.div`
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
export default function StyledComp() {
|
||||
return (
|
||||
<MyPanel><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</MyPanel>
|
||||
);
|
||||
}
|
||||
20
modern-react/my-react/src/chap05/StyledComp2.js
Normal file
20
modern-react/my-react/src/chap05/StyledComp2.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
// <button> 요소를 생성하는 MyButton 컴포넌트
|
||||
export function MyButton({ className, children }) {
|
||||
return (
|
||||
<button type="button" className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// MyButton에 스타일을 부여한 MyStyledButton을 정의한다.
|
||||
export const MyStyledButton = styled(MyButton)`
|
||||
display: block;
|
||||
background-color: royalblue;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
width: 80px;
|
||||
height: 50px;
|
||||
`;
|
||||
9
modern-react/my-react/src/chap05/StyledGlobal.js
Normal file
9
modern-react/my-react/src/chap05/StyledGlobal.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
export default createGlobalStyle`
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: Yellow;
|
||||
}
|
||||
`;
|
||||
19
modern-react/my-react/src/chap05/StyledProps.js
Normal file
19
modern-react/my-react/src/chap05/StyledProps.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const MyPanel = styled.div`
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
color: white;
|
||||
border-radius: ${ props => (props.theme.radius ? '10px' : '0px') };
|
||||
background-color: ${ props => props.theme.color };
|
||||
`;
|
||||
|
||||
export default function StyledProps({ theme }) {
|
||||
return (
|
||||
<MyPanel theme={{
|
||||
radius: true,
|
||||
color: 'royalblue'
|
||||
}}><b>Styled JSX</b>는 JSX 표현식에 스타일 정의를 삽입하는 형식의 라이브러리입니다.</MyPanel>
|
||||
);
|
||||
}
|
||||
10
modern-react/my-react/src/chap05/SuspenseResult.js
Normal file
10
modern-react/my-react/src/chap05/SuspenseResult.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Suspense } from 'react';
|
||||
import ThrowResult from './ThrowResult';
|
||||
|
||||
export default function SuspenseResult() {
|
||||
return (
|
||||
<Suspense fallback={<p>Now Loading...</p>}>
|
||||
<ThrowResult />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
10
modern-react/my-react/src/chap05/SuspenseSimple.js
Normal file
10
modern-react/my-react/src/chap05/SuspenseSimple.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Suspense } from 'react';
|
||||
import ThrowPromise from './ThrowPromise';
|
||||
|
||||
export default function SuspenseSimple() {
|
||||
return (
|
||||
<Suspense fallback={<p>Now Loading...</p>}>
|
||||
<ThrowPromise />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
25
modern-react/my-react/src/chap05/ThrowPromise.js
Normal file
25
modern-react/my-react/src/chap05/ThrowPromise.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default function ThrowPromise() {
|
||||
throw new Promise((resolve, reject) => { });
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 5-1-8
|
||||
// // Promise가 종료되었는지 여부를 나타내는 플래그 변수
|
||||
// let flag = false;
|
||||
|
||||
// export default function ThrowPromise() {
|
||||
// // Promise가 완료되면 원래의 결과를 표시한다.
|
||||
// if (flag) {
|
||||
// return <p>올바르게 표시되었다.</p>;
|
||||
// }
|
||||
// // 로딩 중이라면 Promise를 던져라
|
||||
// throw new Promise((resolve, reject) => {
|
||||
// // 3000밀리초 후에 해결(resolve)하는 처리
|
||||
// setTimeout(() => {
|
||||
// flag = true;
|
||||
// resolve('Susccess!!');
|
||||
// // reject(new Error('Error is occurred!!'));
|
||||
// }, 3000);
|
||||
// });
|
||||
// }
|
||||
23
modern-react/my-react/src/chap05/ThrowResult.js
Normal file
23
modern-react/my-react/src/chap05/ThrowResult.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import wrapPromise from "./wrapPromise";
|
||||
|
||||
// Promise의 상태를 관리하는 오브젝트를 가져온다.
|
||||
const info = getInfo();
|
||||
// Promise의 상태에 따라 결과를 표시하는 컴포넌트
|
||||
export default function ThrowResult() {
|
||||
const result = info.get();
|
||||
return <p>{result}</p>;
|
||||
}
|
||||
|
||||
// 비동기적으로 데이터를 취득하기 위한 함수
|
||||
function getInfo() {
|
||||
return wrapPromise(new Promise((resolve, reject) => {
|
||||
// 2000밀리초 후 50% 확률로 성공/실패 메시지를 생성한다.
|
||||
setTimeout(() => {
|
||||
if (Math.random() > 0.5) {
|
||||
resolve('Succeeded!!');
|
||||
} else {
|
||||
reject('Error!!');
|
||||
}
|
||||
}, 2000);
|
||||
}));
|
||||
}
|
||||
34
modern-react/my-react/src/chap05/wrapPromise.js
Normal file
34
modern-react/my-react/src/chap05/wrapPromise.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default function wrapPromise(promise) {
|
||||
// Promise 상태 관리(pending, fullfilled, rejected)
|
||||
let status = 'pending';
|
||||
// Promise에서 받은 데이터
|
||||
let data;
|
||||
// 원래의 Promise에 후처리 부여
|
||||
let wrapper = promise.then(
|
||||
// 성공 시 status를 fulfilled(성공), data에 취득한 데이터를 설정한다.
|
||||
result => {
|
||||
status = 'fulfilled';
|
||||
data = result;
|
||||
},
|
||||
// 실패 시 status를 rejected(실패), data에 에러 오브젝트를 설정한다.
|
||||
e => {
|
||||
status = 'rejected';
|
||||
data = e;
|
||||
}
|
||||
);
|
||||
// 반환값은 Promise의 상태에 따라 값을 반환하는 get 메서드를 가진 객체다.
|
||||
return {
|
||||
get() {
|
||||
switch(status) {
|
||||
case 'fulfilled':
|
||||
return data; // 성공 시 실제 데이터를 반환한다.
|
||||
case 'rejected':
|
||||
throw data; // 실패 시 에러 발생
|
||||
case 'pending':
|
||||
throw wrapper; // 완료하기 전에 Promise를 던져라.
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
82
modern-react/my-react/src/chap06/FormMui.js
Normal file
82
modern-react/my-react/src/chap06/FormMui.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Button, FormControl, FormControlLabel, FormHelperText,
|
||||
FormLabel, Radio, RadioGroup, TextField } from '@mui/material';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export default function FormMui() {
|
||||
const defaultValues = {
|
||||
name: '홍길동',
|
||||
email: 'admin@example.com',
|
||||
gender: 'male',
|
||||
memo: ''
|
||||
};
|
||||
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
defaultValues
|
||||
});
|
||||
const onsubmit = data => console.log(data);
|
||||
const onerror = err => console.log(err);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onsubmit, onerror)} noValidate>
|
||||
<div>
|
||||
<TextField label="이름" margin="normal"
|
||||
{...register('name', {
|
||||
required: '이름은 필수 입력 항목입니다.',
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: '이름은 20자 이내로 작성해 주세요'
|
||||
}
|
||||
})}
|
||||
error={'name' in errors}
|
||||
helperText={errors.name?.message} />
|
||||
</div>
|
||||
<div>
|
||||
<FormControl>
|
||||
<FormLabel component="legend">성별:</FormLabel>
|
||||
<RadioGroup name="gender">
|
||||
<FormControlLabel value="male" control={<Radio />} label="남성"
|
||||
{...register('gender', {
|
||||
required: '성별은 필수입니다.',
|
||||
})}
|
||||
/>
|
||||
<FormControlLabel value="female" control={<Radio />} label="여성"
|
||||
{...register('gender', {
|
||||
required: '성별은 필수입니다.',
|
||||
})}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<FormHelperText error={'gender' in errors}>
|
||||
{errors.gender?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div>
|
||||
<TextField type="email" label="이메일 주소" margin="normal"
|
||||
{...register('email', {
|
||||
required: '이메일 주소는 필수 입력 항목입니다.',
|
||||
pattern: {
|
||||
value: /([a-z\d+\-.]+)@([a-z\d-]+(?:\.[a-z]+)*)/i,
|
||||
message: '이메일 주소 형식이 잘못됐습니다.'
|
||||
}
|
||||
})}
|
||||
error={'email' in errors}
|
||||
helperText={errors.email?.message} />
|
||||
</div>
|
||||
<div>
|
||||
<TextField label="비고" margin="normal" multiline
|
||||
{...register('memo', {
|
||||
required: '비고는 필수 입력 항목입니다.',
|
||||
minLength: {
|
||||
value: 10,
|
||||
message: '비고는 10자 이상으로 작성해 주세요.'
|
||||
},
|
||||
})}
|
||||
error={'memo' in errors}
|
||||
helperText={errors.memo?.message} />
|
||||
</div>
|
||||
<div>
|
||||
<Button variant="contained" type="submit">제출하기</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
45
modern-react/my-react/src/chap06/MaterialBasic.js
Normal file
45
modern-react/my-react/src/chap06/MaterialBasic.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Button } from '@mui/material';
|
||||
|
||||
export default function MaterialBasic() {
|
||||
return (
|
||||
<>
|
||||
<Button variant="text">Text</Button>
|
||||
<Button variant="contained">Contained</Button>
|
||||
<Button variant="outlined">Outlined</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import { Button } from '@mui/material';
|
||||
|
||||
// export default function MaterialBasic() {
|
||||
// return (
|
||||
// <>
|
||||
// <Button variant="text" color="secondary">Text</Button>
|
||||
// <Button variant="contained" color="secondary">Contained</Button>
|
||||
// <Button variant="outlined" color="secondary">Outlined</Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Code 6-1-3
|
||||
// /** @jsxImportSource @emotion/react */
|
||||
// import { css } from '@emotion/react';
|
||||
// import { Button } from '@mui/material';
|
||||
|
||||
// export default function MaterialBasic() {
|
||||
// // 텍스트 대/소문자 변환을 비활성화하다.
|
||||
// const font = css`
|
||||
// text-transform: none;
|
||||
// `;
|
||||
// return (
|
||||
// <>
|
||||
// <Button variant="text" css={font}>Text</Button>
|
||||
// <Button variant="contained" css={font}>Contained</Button>
|
||||
// <Button variant="outlined" css={font}>Outlined</Button>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
43
modern-react/my-react/src/chap06/MaterialDrawer.js
Normal file
43
modern-react/my-react/src/chap06/MaterialDrawer.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
import { Home, Mail, Info, AccountTree } from '@mui/icons-material';
|
||||
import { Box, Button, Drawer, List, ListItem, ListItemButton,
|
||||
ListItemText, ListItemIcon } from '@mui/material';
|
||||
|
||||
// 표시용 메뉴 정보 준비
|
||||
const menu = [
|
||||
{ title: '홈', href: 'home.html', icon: Home },
|
||||
{ title: 'Contact Us', href: 'contact.html', icon: Mail },
|
||||
{ title: '회사 소개', href: 'company.html', icon: Info },
|
||||
{ title: '사이트맵', href: 'sitemap.html', icon: AccountTree },
|
||||
];
|
||||
|
||||
export default function MaterialDrawer() {
|
||||
// 드로워 개폐를 위한 플래그
|
||||
const [show, setShow] = useState(false);
|
||||
// 버튼 클릭 시 호출되는 핸들러 (show를 반전)
|
||||
const handleDraw = () => setShow(!show);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={handleDraw}>드로워</Button>
|
||||
<Drawer anchor="left" open={show}>
|
||||
<Box sx={{ height: '100vh' }} onClick={handleDraw}>
|
||||
<List>
|
||||
{/* 미리 준비된 배열을 메뉴로 확장 */}
|
||||
{menu.map(obj => {
|
||||
const Icon = obj.icon;
|
||||
return (
|
||||
<ListItem key={obj.title}>
|
||||
<ListItemButton href={obj.href}>
|
||||
<ListItemIcon><Icon /></ListItemIcon>
|
||||
<ListItemText primary={obj.title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
44
modern-react/my-react/src/chap06/MaterialGrid.js
Normal file
44
modern-react/my-react/src/chap06/MaterialGrid.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button } from '@mui/material';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
|
||||
export default function MaterialGrid() {
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid xs={6}>
|
||||
<Button variant="contained" fullWidth>1</Button>
|
||||
</Grid>
|
||||
<Grid xs={2}>
|
||||
<Button variant="contained" fullWidth>2</Button>
|
||||
</Grid>
|
||||
<Grid xs={3}>
|
||||
<Button variant="contained" fullWidth>3</Button>
|
||||
</Grid>
|
||||
<Grid xs={12}>
|
||||
<Button variant="contained" fullWidth>4</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// import { Button } from '@mui/material';
|
||||
// import Grid from '@mui/material/Unstable_Grid2';
|
||||
|
||||
// export default function MaterialGrid() {
|
||||
// return (
|
||||
// <Grid container spacing={2}>
|
||||
// <Grid xs={12} sm={9} md={6}>
|
||||
// <Button variant="contained" fullWidth>1</Button>
|
||||
// </Grid>
|
||||
// <Grid xs={12} sm={3} md={2}>
|
||||
// <Button variant="contained" fullWidth>2</Button>
|
||||
// </Grid>
|
||||
// <Grid xs={12} sm={4} md={3}>
|
||||
// <Button variant="contained" fullWidth>3</Button>
|
||||
// </Grid>
|
||||
// <Grid xs={12}>
|
||||
// <Button variant="contained" fullWidth>4</Button>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// );
|
||||
// }
|
||||
93
modern-react/my-react/src/chap06/MaterialMode.js
Normal file
93
modern-react/my-react/src/chap06/MaterialMode.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { amber, grey } from '@mui/material/colors';
|
||||
import { CssBaseline, Button, useMediaQuery } from '@mui/material';
|
||||
|
||||
export default function MaterialMode() {
|
||||
// const mode = useMediaQuery('(prefers-color-scheme: dark)') ?
|
||||
// 'dark' : 'light';
|
||||
|
||||
// 현재 모드를 관리하는 State
|
||||
const [mode, setMode] = useState('light');
|
||||
// State 값 mode를 light⇔dark으로 반전
|
||||
const toggleMode = () => setMode(prev =>
|
||||
prev === 'light' ? 'dark' : 'light'
|
||||
);
|
||||
// 테마 정의
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode,
|
||||
// mode 값에 따라 테마 전환
|
||||
...(mode === 'light'
|
||||
// 라이트 모드에서 사용하는 팔레트
|
||||
? {
|
||||
primary: amber,
|
||||
}
|
||||
// 다크 모드에서 사용하는 팔레트
|
||||
: {
|
||||
primary: {
|
||||
main: grey[500],
|
||||
contrastText: '#fff'
|
||||
},
|
||||
background: {
|
||||
default: grey[900],
|
||||
paper: grey[900],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Button variant="contained" onClick={toggleMode}>
|
||||
Mode {mode}
|
||||
</Button>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// import { useState } from 'react';
|
||||
// import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
// import { amber, grey } from '@mui/material/colors';
|
||||
// import { CssBaseline, Button, useMediaQuery } from '@mui/material';
|
||||
|
||||
// export default function MaterialMode() {
|
||||
// const mode = useMediaQuery('(prefers-color-scheme: dark)') ?
|
||||
// 'dark' : 'light';
|
||||
|
||||
// // 테마 정의
|
||||
// const theme = createTheme({
|
||||
// palette: {
|
||||
// mode,
|
||||
// // mode 값에 따라 테마 전환
|
||||
// ...(mode === 'light'
|
||||
// // 라이트 모드에서 사용하는 팔레트
|
||||
// ? {
|
||||
// primary: amber,
|
||||
// }
|
||||
// // 다크 모드에서 사용하는 팔레트
|
||||
// : {
|
||||
// primary: {
|
||||
// main: grey[500],
|
||||
// contrastText: '#fff'
|
||||
// },
|
||||
// background: {
|
||||
// default: grey[900],
|
||||
// paper: grey[900],
|
||||
// },
|
||||
// }),
|
||||
// },
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <ThemeProvider theme={theme}>
|
||||
// <CssBaseline />
|
||||
// <Button variant="contained">
|
||||
// Mode {mode}
|
||||
// </Button>
|
||||
// </ThemeProvider>
|
||||
// );
|
||||
// }
|
||||
138
modern-react/my-react/src/chap06/MyButton.js
Normal file
138
modern-react/my-react/src/chap06/MyButton.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import '../stories/button.css';
|
||||
|
||||
export default function MyButton ({
|
||||
primary = false,
|
||||
backgroundColor = null,
|
||||
size = 'medium',
|
||||
label = 'Button',
|
||||
...props
|
||||
}) {
|
||||
// primary 속성에 따라 스타일 클래스 결정
|
||||
const mode = primary ?
|
||||
'storybook-button--primary' : 'storybook-button--secondary';
|
||||
return (
|
||||
// Props를 기반으로 button 요소를 조립
|
||||
<button
|
||||
type="button"
|
||||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
style={backgroundColor && { backgroundColor }}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Code 6-2-6
|
||||
// import PropTypes from 'prop-types';
|
||||
// import '../stories/button.css';
|
||||
|
||||
// /**
|
||||
// * 속성 설정에 따라 다양한 버튼 생성
|
||||
// */
|
||||
// export default function MyButton ({
|
||||
// primary = false,
|
||||
// backgroundColor = null,
|
||||
// size = 'medium',
|
||||
// label = 'Button',
|
||||
// ...props
|
||||
// }) {
|
||||
// // primary 속성에 따라 스타일 클래스 결정
|
||||
// const mode = primary ?
|
||||
// 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
// return (
|
||||
// // Props를 기반으로 button 요소를 조립
|
||||
// <button
|
||||
// type="button"
|
||||
// className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
// style={backgroundColor && { backgroundColor }}
|
||||
// {...props}
|
||||
// >
|
||||
// {label}
|
||||
// </button>
|
||||
// );
|
||||
// };
|
||||
|
||||
// // Props의 타입 정보 선언
|
||||
// /**
|
||||
// * Primary 색상 활성화 여부
|
||||
// */
|
||||
// MyButton.propTypes = {
|
||||
// primary: PropTypes.bool,
|
||||
// /**
|
||||
// * 배경색
|
||||
// */
|
||||
// backgroundColor: PropTypes.string,
|
||||
// /**
|
||||
// * 버튼 크기
|
||||
// */
|
||||
// size: PropTypes.oneOf(['small', 'medium', 'large']),
|
||||
// /**
|
||||
// * 버튼 캡션
|
||||
// */
|
||||
// label: PropTypes.string.isRequired,
|
||||
// /**
|
||||
// * 클릭 핸들러
|
||||
// */
|
||||
// onClick: PropTypes.func,
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// Code 6-2-10
|
||||
// import PropTypes from 'prop-types';
|
||||
// import '../stories/button.css';
|
||||
|
||||
// /**
|
||||
// * 속성 설정에 따라 다양한 버튼 생성
|
||||
// */
|
||||
// export default function MyButton ({
|
||||
// primary = false,
|
||||
// backgroundColor = null,
|
||||
// size = 'medium',
|
||||
// label = 'Button',
|
||||
// handleClick,
|
||||
// ...props
|
||||
// }) {
|
||||
// // primary 속성에 따라 스타일 클래스 결정
|
||||
// const mode = primary ?
|
||||
// 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
// return (
|
||||
// // Props를 기반으로 button 요소를 조립
|
||||
// <button
|
||||
// type="button"
|
||||
// className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
// style={backgroundColor && { backgroundColor }}
|
||||
// onClick={handleClick}
|
||||
// {...props}
|
||||
// >
|
||||
// {label}
|
||||
// </button>
|
||||
// );
|
||||
// };
|
||||
|
||||
// // Props의 타입 정보 선언
|
||||
// MyButton.propTypes = {
|
||||
// /**
|
||||
// * Primary 색상 활성화 여부
|
||||
// */
|
||||
// primary: PropTypes.bool,
|
||||
// /**
|
||||
// * 배경색
|
||||
// */
|
||||
// backgroundColor: PropTypes.string,
|
||||
// /**
|
||||
// * 버튼 크기
|
||||
// */
|
||||
// size: PropTypes.oneOf(['small', 'medium', 'large']),
|
||||
// /**
|
||||
// * 버튼 캡션
|
||||
// */
|
||||
// label: PropTypes.string.isRequired,
|
||||
// /**
|
||||
// * 클릭 핸들러
|
||||
// */
|
||||
// handleClick: PropTypes.func,
|
||||
// };
|
||||
35
modern-react/my-react/src/chap06/MyButton.mdx
Normal file
35
modern-react/my-react/src/chap06/MyButton.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Meta,
|
||||
Title,
|
||||
Description,
|
||||
Primary,
|
||||
Controls,
|
||||
Story,
|
||||
Canvas,
|
||||
} from "@storybook/blocks";
|
||||
import * as MyButtonStories from "./MyButton.stories";
|
||||
|
||||
{/* 스토리 및 문서 연결 */}
|
||||
|
||||
<Meta of={MyButtonStories} />
|
||||
{/* <Meta of={MyButtonStories} name="API Ref" /> */}
|
||||
|
||||
<Title />
|
||||
|
||||
<Description />
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
**※표의 외관을 변경하려면 표의 Control 열을 조작하세요**
|
||||
|
||||
## 구체적인 예시
|
||||
|
||||
backgroundColor 속성으로 버튼의 배경색을 변경할 수도 있다.
|
||||
|
||||
<Canvas of={MyButtonStories.Yellow} />
|
||||
|
||||
---
|
||||
|
||||
_Copyright WINGS Project, 2023-_
|
||||
1112
modern-react/my-react/src/chap06/MyButton.stories.js
Normal file
1112
modern-react/my-react/src/chap06/MyButton.stories.js
Normal file
File diff suppressed because it is too large
Load Diff
35
modern-react/my-react/src/chap06/QueryBasic.js
Normal file
35
modern-react/my-react/src/chap06/QueryBasic.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
// delay 초 동안 처리를 일시 정지하는 sleep 함수
|
||||
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// 날씨 정보를 얻기 위한 함수
|
||||
const fetchWeather = async () => {
|
||||
// 처리 지연을 위한 더미 휴지 처리
|
||||
await sleep(2000);
|
||||
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Seoul&lang=ko&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`);
|
||||
if (res.ok) { return res.json(); }
|
||||
throw new Error(res.statusText);
|
||||
};
|
||||
|
||||
export default function QuerBasic() {
|
||||
// fetchWeather 함수로 데이터 가져오기
|
||||
const { data, isLoading, isError, error } = useQuery('weather', fetchWeather);
|
||||
// 로딩 중일 경우 로딩 메시지 표시
|
||||
if (isLoading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
// 통신 오류 발생 시 오류 메시지 표시
|
||||
if (isError) {
|
||||
return <p>Error: {error.message}</p>;
|
||||
}
|
||||
// 로딩 중이거나 오류가 아닌 경우 결과 표시
|
||||
return (
|
||||
<figure>
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${data?.weather?.[0]?.icon}.png`}
|
||||
alt={data?.weather?.[0]?.main} />
|
||||
<figcaption>{data?.weather?.[0]?.description}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
53
modern-react/my-react/src/chap06/QueryPre.js
Normal file
53
modern-react/my-react/src/chap06/QueryPre.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// delay 초 동안 처리를 일시 정지하는 sleep 함수
|
||||
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// 날씨 정보를 얻기 위한 함수
|
||||
const fetchWeather = async () => {
|
||||
// 처리 지연을 위한 더미 휴지 처리
|
||||
await sleep(2000);
|
||||
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Seoul&lang=ko&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`);
|
||||
if (res.ok) { return res.json(); }
|
||||
// 오류 발생 시 해당 내용을 슬로우
|
||||
throw new Error(res.statusText);
|
||||
};
|
||||
|
||||
export default function QueryPre({ id }) {
|
||||
// 날씨 정보(info), loading(로딩 중인가?), error(오류 정보) 준비 error(오류 정보) 준비
|
||||
const [data, setData] = useState(null);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 컴포넌트 실행 시 날씨 정보 획득
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchWeather()
|
||||
// 성공 시 정보 업데이트
|
||||
.then(result => setData(result))
|
||||
// 실패 시 error 업데이트
|
||||
.catch(err => setError(err.message))
|
||||
// 성공 여부와 상관없이 isLoading 업데이트
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// 로딩 중이라면 로딩 메시지 표시
|
||||
if (isLoading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
// 통신 오류 발생 시 오류 메시지 표시
|
||||
if (error) {
|
||||
return <p>Error: {error}</p>;
|
||||
}
|
||||
|
||||
// 로딩 중이거나 오류가 아닌 경우 결과 표시
|
||||
return (
|
||||
<figure>
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${data?.weather?.[0]?.icon}.png`}
|
||||
alt={data?.weather?.[0]?.main} />
|
||||
<figcaption>{data?.weather?.[0]?.description}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
27
modern-react/my-react/src/chap06/QuerySuspense.js
Normal file
27
modern-react/my-react/src/chap06/QuerySuspense.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
// delay 밀리초 동안 처리를 일시 정지하는 sleep 함수
|
||||
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
const fetchWeather = async () => {
|
||||
// 더미 지연
|
||||
await sleep(2000);
|
||||
// const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Tokyo&lang=ja&appid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`);
|
||||
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=Seoul&lang=kr&appid=6fd0c26b5a2a9ad110324cc29669eb7c`);
|
||||
if (res.ok) { return res.json(); }
|
||||
throw new Error(res.statusText);
|
||||
};
|
||||
|
||||
export default function QuerySuspense() {
|
||||
const { data } = useQuery('weather', fetchWeather);
|
||||
// const { data } = useQuery('weather', fetchWeather, { suspense: true });
|
||||
|
||||
return (
|
||||
<figure>
|
||||
<img
|
||||
src={`https://openweathermap.org/img/wn/${data?.weather?.[0]?.icon}.png`}
|
||||
alt={data?.weather?.[0]?.main} />
|
||||
<figcaption>{data?.weather?.[0]?.description}</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
44
modern-react/my-react/src/chap06/theme.js
Normal file
44
modern-react/my-react/src/chap06/theme.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
import { green, orange } from "@mui/material/colors";
|
||||
|
||||
const theme = createTheme({
|
||||
// 앱에서 사용하는 컬러링 설정
|
||||
palette: {
|
||||
primary: {
|
||||
main: orange[500],
|
||||
},
|
||||
secondary: {
|
||||
main: green[500],
|
||||
}
|
||||
},
|
||||
spacing: 10,
|
||||
});
|
||||
|
||||
export default theme;
|
||||
|
||||
|
||||
|
||||
// import { createTheme } from "@mui/material";
|
||||
// import { green, orange } from "@mui/material/colors";
|
||||
|
||||
// const theme = createTheme({
|
||||
// // 앱에서 사용하는 컬러링 설정
|
||||
// palette: {
|
||||
// primary: {
|
||||
// main: orange[500],
|
||||
// },
|
||||
// secondary: {
|
||||
// main: green[500],
|
||||
// }
|
||||
// },
|
||||
// spacing: 10,
|
||||
// components: {
|
||||
// MuiButton: {
|
||||
// defaultProps: {
|
||||
// variant: 'contained',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// export default theme;
|
||||
70
modern-react/my-react/src/chap07/HookCallbackRef.js
Normal file
70
modern-react/my-react/src/chap07/HookCallbackRef.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function HookCallbackRef() {
|
||||
const [show, setShow] = useState(false);
|
||||
// 버튼 클릭으로 표시/숨기기 반전
|
||||
const handleClick = () => setShow(!show);
|
||||
// [주소]란 참조
|
||||
const address = useRef(null);
|
||||
// [주소] 항목이 비어있지 않으면 포커스 이동
|
||||
useEffect(() => {
|
||||
if (address.current) {
|
||||
address.current.focus();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="name">이름:</label>
|
||||
<input id="name" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email">이메일 주소:</label>
|
||||
<input id="email" type="text" />
|
||||
<button onClick={handleClick}>확장 표시</button>
|
||||
</div>
|
||||
{/* State(show) 값에 따라 [주소] 란을 표시 */}
|
||||
{show &&
|
||||
<div>
|
||||
<label htmlFor="address">주소:</label>
|
||||
<input id="address" type="text" ref={address} />
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Code 7-2-12
|
||||
// import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// export default function HookCallbackRef() {
|
||||
// const [show, setShow] = useState(false);
|
||||
// const handleClick = () => setShow(!show);
|
||||
|
||||
// // 콜백 Ref 준비
|
||||
// const callbackRef = elem => elem?.focus();
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <div>
|
||||
// <label htmlFor="name">이름:</label>
|
||||
// <input id="name" type="text" />
|
||||
// </div>
|
||||
// <div>
|
||||
// <label htmlFor="email">이메일 주소:</label>
|
||||
// <input id="email" type="text" />
|
||||
// <button onClick={handleClick}>확장 표시</button>
|
||||
// </div>
|
||||
// {/* State(show) 값에 따라 [주소] 란을 표시 */}
|
||||
// {show &&
|
||||
// <div>
|
||||
// <label htmlFor="address">주소:</label>
|
||||
// <input id="address" type="text" ref={callbackRef} />
|
||||
// </div>
|
||||
// }
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user