[modern-react] 리액트 스터디 파일 추가

This commit is contained in:
2025-09-30 23:55:13 +09:00
parent 31bcb2efe1
commit 75ec02d506
546 changed files with 141345 additions and 0 deletions

View 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);
}
}

View 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;

View 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();
});

View 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;

View File

@@ -0,0 +1,5 @@
.foo {
color: White;
background-color: Blue;
padding: 3px;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
}

View 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>
</>
);
}

View 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} />
);
}

View 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>
);
}

View 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} />
);
}

View File

@@ -0,0 +1,7 @@
export default function EventObj() {
// 클릭 시 이벤트 오브젝트를 로그에 출력
const handleClick = e => console.log(e);
return (
<button onClick={handleClick}>클릭</button>
);
}

View 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>
</>
);
}

View File

@@ -0,0 +1,6 @@
.box {
height: 100px;
width: 100px;
border: 1px solid #000;
overflow: scroll;
}

View 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>
// );
// }

View File

@@ -0,0 +1,9 @@
#main {
position:absolute;
margin:50px;
top:20px;
left:20px;
height: 150px;
width: 500px;
border: solid 1px #000;
}

View 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>
);
}

View 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
}

View 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>
// );
// }

View 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>
);
}

View 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>
// </>
// );
// }

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
// );
// }

View 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;

View 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;
}

View 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>
);
}

View 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>
// </>
// );
// }

View File

@@ -0,0 +1,5 @@
.cnt {
margin-right: 5px;
width: 50px;
font-size: xx-large;
}

View 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>
);
}

View 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} />
</>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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;

View File

@@ -0,0 +1,3 @@
.invalid {
background-color: #f00;
}

View 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>
// );
// }

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,3 @@
.done {
text-decoration: line-through;
}

View 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>
// );
// }

View 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;

View 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>
);
}

View 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>
// );
// }

View 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>
// );
// }

View 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>
</>
);
}

View 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>
);
}

View 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>
// </>
// );
// }

View 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>
);
}

View 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>
// </>
// );
// }

View File

@@ -0,0 +1,7 @@
export default function ErrorThrow() {
// 무조건 예외 발생
throw new Error('Error is occured in MyApp.');
return (
<p> 실행되었다.</p>
);
}

View 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>;
}

View 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>
// );
// }

View File

@@ -0,0 +1,7 @@
export default function LazyButton() {
return (
<div>
<button id="btn">버튼1</button>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function LazyButton2() {
return (
<div>
<button id="btn">버튼2</button>
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,5 @@
export default function MyLoading() {
return (
<p>Now Loading...</p>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,5 @@
import { css } from 'styled-components';
export default css`
margin: 20px;
`;

View 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>
);
}

View 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>
);
}

View 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;
`;

View File

@@ -0,0 +1,9 @@
import { createGlobalStyle } from 'styled-components';
export default createGlobalStyle`
body {
margin: 0;
padding: 0;
background-color: Yellow;
}
`;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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);
// });
// }

View 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);
}));
}

View 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;
}
}
};
}

View 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>
);
}

View 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>
// </>
// );
// }

View 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>
</>
);
}

View 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>
// );
// }

View 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>
// );
// }

View 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,
// };

View 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-_

File diff suppressed because it is too large Load Diff

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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