Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
1-1) React Concept
1-2) 개발 환경 체크
1-3) React 라이브러리
Angular vs React vs Vue
View 를 다루는 라이브러리
Only Rendering & Update
NOT included another functionality (ex. http client, ...)
Component Based Development
독립적인 코드 블럭 (HTML + CSS + JavaScript)
작업의 단위
Virtual DOM
이제는 DOM 을 직접 다루지 않음.
JSX
NOT Templates
transpile to JS (Babel, TypeScript)
CSR & SSR
<!-- HTMLElement -->
<img src="이미지 주소"/>
<button class="클래스 이름">버튼</button>
<!-- 내가 만든 컴포넌트 -->
<내가지은이름1 name="Mark" />
<내가지은이름 prop={false}>내용</내가지은이름>
<!--
- src, class, name, props 밖에서 넣어주는 데이터
- 문서(HTML), 스타일(CSS), 동작(JS) 를 합쳐서 내가 만든 일종의 태그
-->
nvm install 14.16.1
nvm use 14.16.1
nvm alias default 14.16.1
리액트의 핵심 모듈 2개로 리액트가 하는 일 알아보기
// 1. 리액트 컴포넌트 => HTMLElement 연결하기
import ReactDOM from 'react-dom';
// 2. 리액트 컴포넌트 만들기
import React from 'react';
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example'),
);
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
리액트 컴포넌트를 만들 때 사용하는 API 모음
CDN 을 통한 리액트 라이브러리 사용
mkdir what-is-react
cd what-is-react
npm init -y
npx serve
<!-- ex1.html : CDN 을 통해 React, ReactDOM 가져오기 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
></script>
<script type="text/javascript">
// Global 에 React 와 ReactDOM 객체가 생성
console.log(React);
console.log(ReactDOM);
</script>
</body>
</html>
HTML 로 문서 구조를 잡고,
CSS 로 스타일을 입히고,
JavaScript 로 DOM 을 조작합니다.
<!-- ex2.html : 고전 프론트엔드 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<style>
* {margin: 0;padding: 0;border: 0;}
#root p {color: white;font-size: 20px;background-color: green;text-align: center;width: 200px;}
#btn_plus {background-color: red;border: 2px solid #000000;font-size: 15px;width: 200px;}
</style>
</head>
<body>
<div id="root"></div>
<button id="btn_plus">+</button>
<script type="text/javascript">
const root = document.querySelector("#root");
const btn_plus = document.querySelector("#btn_plus");
let i = 0;
root.innerHTML = `<p>init : 0</p>`;
btn_plus.addEventListener("click", () => {
root.innerHTML = `<p>init : ${++i}</p>`;
});
</script>
</body>
</html>
컴포넌트를 정의하고,
실제 DOM 에 컴포넌트를 그려준다.
<!-- ex3.html : 컴포넌트를 만들고, 실제 DOM 에 그린다. -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<button id="btn_plus">+</button>
<script type="text/javascript">
// react 라이브러리가 하는 일
const component = {
message: "init",
count: 0,
render() {
return `<p>${this.message} : ${this.count}</p>`;
}
};
// react-dom 라이브러리가 하는 일
function render(dom, component) {
// 컴포넌트를 render 하고, DOM 에 그려준다.
root.innerHTML = component.render();
}
render(document.querySelector("#root"), component);
document.querySelector("#btn_plus").addEventListener("click", () => {
// 외부에서 컴포넌트의 값을 변경하는 행위
component.message = "update";
component.count = component.count + 1;
render(document.querySelector("#root"), component);
});
</script>
</body>
</html>
컴포넌트를 정의하고,
실제 DOM 에 컴포넌트를 그려준다.
<!-- ex4.html : React 로 컴포넌트를 만들고, 실제 DOM 에 그린다. -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<button id="btn_plus">+</button>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script type="text/javascript">
const Component = props => {
return React.createElement(
"p",
null,
`${props.message} ${props.count}`
);
};
let i = 0;
ReactDOM.render(
React.createElement(Component, { message: "init", count: i }, null),
document.querySelector("#root")
);
document.querySelector("#btn_plus").addEventListener("click", () => {
i++;
ReactDOM.render(
React.createElement(Component, { message: "update", count: i }, null),
document.querySelector("#root")
);
});
</script>
</body>
</html>
2-1) React.createElement 로 컴포넌트 만들기 2-2) JSX
2-3) React Component 만드는 법 2-4) Props 와 State
2-5) Event Handling 2-6) Component Lifecycle
순수 JavaScript (그렇다면 순수하지 않은 것은??)
<!-- ex5.html : React.createElement 로 컴포넌트를 만들기 -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script type="text/javascript">
// React.createElement(
// type, // 태그 이름 문자열 | React 컴포넌트 | React.Fragment
// [props], // 리액트 컴포넌트에 넣어주는 데이터 객체
// [...children] // 자식으로 넣어주는 요소들
// );
// 1. 태그 이름 문자열 type
// ReactDOM.render(
// React.createElement('h1', null, `type 이 "태그 이름 문자열" 입니다.`),
// document.querySelector('#root'),
// );
// 2. React 컴포넌트 type
// const Component = props => {
// return React.createElement('p', null, `type 이 "React 컴포넌트" 입니다.`);
// };
// ReactDOM.render(
// React.createElement(
// Component,
// null,
// null
// ),
// document.querySelector("#root")
// );
// 3. React Fragment type
// ReactDOM.render(
// React.createElement(
// React.Fragment,
// null,
// `type 이 "React Fragment" 입니다.`
// ),
// document.querySelector("#root")
// );
// 4. props 를 통해 데이터를 주입
// const Component = props => {
// return React.createElement(
// 'p',
// null,
// `message 는 "${props.message}" 입니다.`,
// );
// };
// ReactDOM.render(
// React.createElement(
// Component,
// { message: '이것은 메세지 입니다.' },
// null,
// ),
// document.querySelector('#root'),
// );
// 5. props 에 들어가는 children
// const Component = props => {
// return React.createElement(
// 'p',
// null,
// `message 는 "${props.message}" 입니다.`,
// `props.children 은 "${props.children}" 입니다.`,
// );
// };
// ReactDOM.render(
// React.createElement(
// Component,
// { message: '이것은 메세지 입니다.' },
// '이것은 children 입니다.',
// ),
// document.querySelector('#root'),
// );
// 6. 리액트 엘리먼트에 style 추가
// ReactDOM.render(
// React.createElement(
// 'h1',
// { style: { color: 'red' } },
// `type 이 "태그 이름 문자열" 입니다.`,
// ),
// document.querySelector('#root'),
// );
// 7. 복잡한 컴포넌트
// ReactDOM.render(
// React.createElement(
// 'div',
// { style: { backgroundColor: 'red', width: 100, height: 100 } },
// React.createElement(
// 'div',
// { style: { backgroundColor: 'green', width: 50, height: 50 } },
// null,
// ),
// React.createElement(
// 'div',
// { style: { backgroundColor: 'yellow', width: 50, height: 50 } },
// null,
// ),
// ),
// document.querySelector('#root'),
// );
</script>
</body>
</html>
JSX 문법으로 작성된 코드는 순수한 JavaScript 로 컴파일 하여 사용한다.
누가 해주나요?? => babel
JSX 문법 => React.createElement
<!-- ex6.html : React.createElement => JSX -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// React.createElement(
// type, // 태그 이름 문자열 | React 컴포넌트 | React.Fragment
// [props], // 리액트 컴포넌트에 넣어주는 데이터 객체
// [...children] // 자식으로 넣어주는 요소들
// );
// 1. 태그 이름 문자열 type
// ReactDOM.render(
// React.createElement('h1', null, `type 이 "태그 이름 문자열" 입니다.`),
// document.querySelector('#root'),
// );
// ReactDOM.render(
// <h1>type 이 "태그 이름 문자열" 입니다.</h1>,
// document.querySelector('#root'),
// );
// 2. React 컴포넌트 type
// const Component = props => {
// return React.createElement('p', null, `나는 컴포넌트입니다.`);
// };
// ReactDOM.render(
// React.createElement(
// Component,
// null,
// `type 이 "React 컴포넌트" 입니다.`
// ),
// document.querySelector("#root")
// );
// const Component = props => {
// return <p>type 이 "React 컴포넌트" 입니다.</p>;
// };
// ReactDOM.render(<Component />, document.querySelector('#root'));
// 3. React Fragment type
// ReactDOM.render(
// React.createElement(
// React.Fragment,
// null,
// `type 이 "React Fragment" 입니다.`
// ),
// document.querySelector("#root")
// );
// ReactDOM.render(
// <>`type 이 "React Fragment" 입니다.</>,
// document.querySelector('#root'),
// );
// 4. props 를 통해 데이터를 주입
// const Component = props => {
// return React.createElement(
// 'p',
// null,
// `message 는 "${props.message}" 입니다.`,
// );
// };
// ReactDOM.render(
// React.createElement(
// Component,
// { message: '이것은 메세지 입니다.' },
// null,
// ),
// document.querySelector('#root'),
// );
// const Component = props => {
// return <p>message 는 "{props.message}" 입니다.</p>;
// };
// ReactDOM.render(
// <Component message="이것은 메세지 입니다." />,
// document.querySelector('#root'),
// );
// 5. props 에 들어가는 children
// const Component = props => {
// return React.createElement(
// 'p',
// null,
// `message 는 "${props.message}" 입니다.`,
// `props.children 은 "${props.children}" 입니다.`,
// );
// };
// ReactDOM.render(
// React.createElement(
// Component,
// { message: '이것은 메세지 입니다.' },
// '이것은 children 입니다.',
// ),
// document.querySelector('#root'),
// );
// const Component = props => {
// return (
// <p>
// message 는 "{props.message}" 입니다. props.children 은 "
// {props.children}" 입니다.
// </p>
// );
// };
// ReactDOM.render(
// <Component message="이것은 메세지 입니다.">
// 이것은 children 입니다.
// </Component>,
// document.querySelector('#root'),
// );
// 6. 리액트 엘리먼트에 style 추가
// ReactDOM.render(
// React.createElement(
// 'h1',
// { style: { color: 'red' } },
// `type 이 "태그 이름 문자열" 입니다.`,
// ),
// document.querySelector('#root'),
// );
// ReactDOM.render(
// <h1 style={{ color: 'red' }}>type 이 "태그 이름 문자열" 입니다.</h1>,
// document.querySelector('#root'),
// );
// 7. 복잡한 컴포넌트
// ReactDOM.render(
// React.createElement(
// 'div',
// { style: { backgroundColor: 'red', width: 100, height: 100 } },
// React.createElement(
// 'div',
// { style: { backgroundColor: 'green', width: 50, height: 50 } },
// null,
// ),
// React.createElement(
// 'div',
// { style: { backgroundColor: 'yellow', width: 50, height: 50 } },
// null,
// ),
// ),
// document.querySelector('#root'),
// );
// ReactDOM.render(
// <div style={{ backgroundColor: 'red', width: 100, height: 100 }}>
// <div style={{ backgroundColor: 'green', width: 50, height: 50 }} />
// <div style={{ backgroundColor: 'yellow', width: 50, height: 50 }} />
// </div>,
// document.querySelector('#root'),
// );
</script>
</body>
</html>
<!-- ex7.html : JSX 문법 -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// 1. 최상위 요소가 하나여야 합니다.
// 2. 최상위 요소 리턴하는 경우, ( ) 로 감싸야 합니다.
// const Comp1 = props => {
// return (
// <h1>제목</h1>
// <h2>부제목</h2>
// );
// }
// const Comp2 = props => {
// return (
// <div>
// <h1>제목</h1>
// <h2>부제목</h2>
// </div>
// );
// };
// 3. 자식들을 바로 랜더링하고 싶으면, <>자식들</> 를 사용합니다. => Fragment
// const Comp3 = props => {
// return (
// <>
// <h1>제목</h1>
// <h2>부제목</h2>
// </>
// );
// };
// 4. 자바스크립트 표현식을 사용하려면, {표현식} 를 이용합니다.
// const Comp4 = props => {
// return (
// <div>
// <h1>제목</h1>
// <h2>{props.children}</h2>
// </div>
// );
// };
// 5. if 문은 사용할 수 없습니다.
// 삼항 연산자 혹은 && 를 사용합니다.
// const Comp5 = props => {
// return (
// <div>
// <h1>제목</h1>
// <h2>{props.children}</h2>
// {props.isShow ? '있다' : '없다.'}
// {props.isShow && '있을 때만 나온다'}
// </div>
// );
// };
// 6. style 을 이용해 인라인 스타일링이 가능합니다.
// const Comp6 = props => {
// return (
// <div>
// <h1
// style={{
// color: 'red',
// }}
// >
// 제목
// </h1>
// <h2>부제목</h2>
// </div>
// );
// };
// 7. class 대신 className 을 사용해 class 를 적용할 수 있습니다.
// const Comp7 = props => {
// return (
// <div>
// <h1 className="title">제목</h1>
// <h2>부제목</h2>
// </div>
// );
// };
// 8. 자식요소가 있으면, 꼭 닫아야 하고, 자식요소가 없으면 열면서 닫아야 합니다.
// <p>어쩌구</p>
// <br />
// const Comp7 = props => {
// return (
// <div>
// <h1 className="title">제목</h1>
// <br />
// <h2>부제목</h2>
// </div>
// );
// };
</script>
</body>
</html>
컴포넌트 내부에 상태가 있다면 ?
class
컴포넌트 내부에 상태가 없다면 ?
라이프사이클을 사용해야 한다면 ?
class
라이프사이클에 관계 없다면 ?
function
class
function
import React from 'react';
// 정의
class ClassComponent extends React.Component {
render() {
return (<div>Hello</div>);
}
}
// 사용
<ClassComponent />
import React from 'react';
// 정의 1
function FunctionComponent() {
return <div>Hello</div>;
}
// 정의 2
const FunctionComponent = () => <div>Hello</div>;
// 사용
<FunctionComponent />
Props 는 컴포넌트 외부에서 컴포넌트에게 주는 데이터입니다.
State 는 컴포넌트 내부에서 변경할 수 있는 데이터입니다.
둘 다 변경이 발생하면, 랜더가 다시 일어날 수 있습니다.
Props 와 State 를 바탕으로 컴포넌트를 그립니다.
그리고 Props 와 State 가 변경되면, 컴포넌트를 다시 그립니다.
컴포넌트를 그리는 방법을 기술하는 함수가 랜더 함수 입니다.
<!-- ex8-1.html : 함수로 리액트 컴포넌트 만들기 -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
function Component(props) {
return (
<div>
<h1>{props.message} 이것은 함수로 만든 컴포넌트 입니다.</h1>
</div>
);
}
ReactDOM.render(
<Component message="안녕하세요!!!" />,
document.querySelector('#root'),
);
</script>
</body>
</html>
props 설정
function Component(props) {
return (
<div>
<h1>{props.message} 이것은 함수로 만든 컴포넌트 입니다.</h1>
</div>
);
}
props 사용
<!-- ex8-2.html : 클래스로 리액트 컴포넌트 만들기 -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class Component extends React.Component {
render() {
return (
<div>
<h1>
{this.props.message} 이것은 클래스를 상속하여 만든 컴포넌트
입니다.
</h1>
</div>
);
}
}
ReactDOM.render(
<Component message="안녕하세요!!!" />,
document.querySelector('#root'),
);
</script>
</body>
</html>
props 설정
class Component extends React.Component {
render() {
return (
<div>{this.props.p}</div>
);
}
}
props 사용
<!-- ex9.html : defaultProps 설정 -->
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class Component extends React.Component {
static defaultProps = {
message: '안녕하세요!!!',
};
render() {
return (
<div>
{this.props.message} 이것은 클래스를 상속하여 만든 컴포넌트
입니다.
</div>
);
}
}
// Component.defaultProps = {
// message: '안녕하세요!!!',
// };
ReactDOM.render(<Component />, document.querySelector('#root'));
</script>
</body>
</html>
state 초기값 설정
class Component extends React.Component {
state = {
s: '스테이트'
};
render() {
return (
<div>{this.state.s}</div>
);
}
}
state 사용
class Component extends React.Component {
constructor(props) {
super(props);
this.state = {s: '스테이트'};
}
render() {
return (
<div>{this.state.s}</div>
);
}
}
state 값 업데이트
class Component extends React.Component {
state = {
s: '스테이트'
};
render() {
return (
<div onClick={() => {
this.setState({s: '새 스테이트'});
}}>{this.state.s}</div>
);
}
}
class Comp extends React.Component {
render() {
return (
<div>
<button onClick={() => {
console.log('clicked');
}}>클릭</button>
</div>
);
}
}
<!-- ex10.html : 이벤트를 이용하여 state 바꾸기 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<style>
* { margin: 0; padding: 0; border: 0; }
</style>
</head>
<body>
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
class Component extends React.Component {
state = { message: 'init', count: 0 };
render() {
return (
<>
<p
style={{
color: 'white',
fontSize: 20,
backgroundColor: 'green',
textAlign: 'center',
width: 200,
}}
>
{this.state.message} {this.state.count}
</p>
<button
style={{
backgroundColor: 'red',
border: '2px solid #000000',
fontSize: 15,
width: 200,
}}
onClick={() => {
this.setState({
message: 'update',
count: this.state.count + 1,
});
}}
>
+
</button>
</>
);
}
}
ReactDOM.render(<Component />, document.querySelector('#root'));
</script>
</body>
</html>
Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.
constructor
componentWillMount
render (최초 랜더)
componentDidMount
class App extends React.Component {
_interval;
constructor(props) {
console.log('App constructor');
super(props);
this.state = {
age: 37,
};
}
componentWillMount() {
console.log('App componentWillMount');
}
componentDidMount() {
console.log('App componentDidMount');
this._interval = window.setInterval(() => {
this.setState({
age: this.state.age + 1,
});
}, 1000);
}
componentWillUnmount() {
console.log('App componentWillUnmount');
clearInterval(this._interval);
}
render() {
console.log('App render');
return (
<div>
<h2>
Hello {this.props.name} - {this.state.age}
</h2>
</div>
);
}
}
constructor
componentWillMount
render
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
componentWillReceiveProps(nextProps) {
console.log(
`App componentWillReceiveProps : ${JSON.stringify(
this.props
)} => ${JSON.stringify(nextProps)}`
);
}
shouldComponentUpdate(nextProps, nextState) {
console.log(
`App shouldComponentUpdate : ${JSON.stringify(
this.props
)} => ${JSON.stringify(nextProps)}, ${JSON.stringify(
this.state
)} => ${JSON.stringify(nextState)}`
);
return true;
}
componentWillUpdate(nextProps, nextState) {
console.log(
`App componentWillUpdate : ${JSON.stringify(
this.props
)} => ${JSON.stringify(nextProps)}, ${JSON.stringify(
this.state
)} => ${JSON.stringify(nextState)}`
);
}
componentDidUpdate(prevProps, prevState) {
console.log(
`App componentDidUpdate : ${JSON.stringify(
prevProps
)} => ${JSON.stringify(this.props)}, ${JSON.stringify(
prevState
)} => ${JSON.stringify(this.state)}`
);
}
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
props 를 새로 지정했을 때 바로 호출됩니다.
여기는 state 의 변경에 반응하지 않습니다.
여기서 props 의 값에 따라 state 를 변경해야 한다면,
setState 를 이용해 state 를 변경합니다.
그러면 다음 이벤트로 각각 가는것이 아니라 한번에 변경됩니다.
props 만 변경되어도
state 만 변경되어도
props & state 둘다 변경되어도
newProps 와 new State 를 인자로 해서 호출
return type 이 boolean 입니다.
true 면 render
false 면 render 가 호출되지 않습니다.
이 함수를 구현하지 않으면, 디폴트는 true
컴포넌트가 재 랜더링 되기 직전에 불립니다.
여기선 setState 같은 것을 쓰면 아니됩니다.
컴포넌트가 재 랜더링을 마치면 불립니다.
componentWillUnmpunt
class App extends React.Component {
_interval;
constructor(props) {
console.log('App constructor');
super(props);
this.state = {
age: 37,
};
}
componentDidMount() {
console.log('App componentDidMount');
this._interval = window.setInterval(() => {
this.setState({
age: this.state.age + 1,
});
}, 1000);
}
componentWillUnmount() {
console.log('App componentWillUnmount');
clearInterval(this._interval);
}
render() {
console.log('App render');
return (
<div>{this.state.age < 50 && <Button />}</div>
);
}
}
componentWillUnmount
class Button extends React.Component {
componentWillUnmount() {
console.log('Button componentWillUnmount');
}
render() {
return <>hello</>;
}
}
constructor
componentWillMount => getDerivedStateFromProps
render
componentDidMount
componentWillReceiveProps => getDerivedStateFromProps
shouldComponentUpdate
render
componentWillUpdate => getSnapshotBeforeUpdate
(dom 에 적용)
componentDidUpdate
componentWillUnmount
constructor
static getDerivedStateFromProps
render (최초 랜더)
componentDidMount
import React from 'react';
class App extends React.Component {
state = {
age: 0,
};
static getDerivedStateFromProps(nextProps, prevState) {
console.log(nextProps, prevState);
if (prevState.age !== nextProps.age) {
return { age: nextProps.age };
}
return null;
}
render() {
console.log('App render');
return <div>{this.state.age}</div>;
}
}
export default App;
getDerivedStateFromProps
static getDerivedStateFromProps (props 변경)
shouldComponentUpdate (state 변경)
render
getSnapshotBeforeUpdate
(dom 에 적용)
componentDidUpdate
import React from "react";
import "./App.css";
let i = 0;
export default class App extends React.Component {
state = { list: [] };
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.list.length === this.state.list.length) return null;
const list = document.querySelector("#list");
return list.scrollHeight - list.scrollTop;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot === null) return;
const list = document.querySelector("#list");
list.scrollTop = list.scrollHeight - snapshot;
}
componentDidMount() {
setInterval(() => {
this.setState({
list: [...this.state.list, i++],
});
}, 1000);
}
render() {
return (
<div id="list" style={{ height: 100, overflow: "scroll" }}>
{this.state.list.map((i) => (
<div>{i}</div>
))}
</div>
);
}
}
getSnapshotBeforeUpdate
componentWillUnmount
componentDidCatch
import React from 'react';
class Button extends React.Component {
render() {
test();
return <div>hello</div>;
}
}
class App extends React.Component {
state = {
hasError: false,
};
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
// logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
return <div>에러 화면</div>;
}
return (
<div>
<Button />
</div>
);
}
}
export default App;
componentDidCatch
3-1) Create React App 3-2) ESLint
3-3) Prettier 3-4) husky
3-5) lint-staged 3-6) Create React App 시작 코드 이해하기
3-7) 리액트 컴포넌트 디버깅
https://create-react-app.dev
npx create-react-app tic-tac-toe
npm 5.2.0 이상부터 함께 설치된 커맨드라인 명령어
이런 곳에 있습니다.
npx create-react-app 프로젝트이름
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
"react": "^16.13.1"
"react-dom": "^16.13.1"
"react-scripts": "3.4.1"
"@testing-library/jest-dom": "^4.2.4"
"@testing-library/react": "^9.5.0"
"@testing-library/user-event": "^7.2.1"
npm install serve -g
serve -s build
eject 를 이용하면, cra 로 만든 프로젝트에서 cra 를 제거합니다.
이는 돌이킬 수 없기 때문에 결정하기 전에 신중해야 합니다.
보통 cra 내에서 해결이 안되는 설정을 추가해야 할 때 합니다.
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@babel/core": "7.9.0",
"@svgr/webpack": "4.3.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"babel-eslint": "10.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "8.1.0",
"babel-plugin-named-asset-import": "^0.3.6",
"babel-preset-react-app": "^9.1.2",
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "3.4.2",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "3.0.3",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.1",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "^1.6.1",
"file-loader": "4.3.0",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "4.0.0-beta.11",
"identity-obj-proxy": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"mini-css-extract-plugin": "0.9.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.6.4",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"react": "^16.13.1",
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
"semver": "6.3.0",
"style-loader": "0.23.1",
"terser-webpack-plugin": "2.3.5",
"ts-pnp": "1.1.6",
"url-loader": "2.3.0",
"webpack": "4.42.0",
"webpack-dev-server": "3.10.3",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1"
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
"setupFiles": [
"react-app-polyfill/jsdom"
],
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jest-environment-jsdom-fourteen",
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$"
],
"modulePaths": [],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"babel": {
"presets": [
"react-app"
]
}
}
webpack
파일 확장자에 맞는 loader 에게 위임
babel-loader
js
jsx
css
css-loader
최종 배포용 파일
babel config
어떤 문법을 번역할건지 설정
The pluggable linting utility for JavaScript and JSX
mkdir eslint-test
cd eslint-test
npm init -y
npm install eslint -D
npx eslint --init
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"semi": [
"error",
"always"
]
}
}
rules 을 추가합니다.
// index.js
console.log("hello")
{
"name": "tic-tac-toe",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app",
"rules": {
"semi": [
"error",
"always"
]
}
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
.eslintrc.json
An opinionated code formatter
mkdir prettier-test
cd prettier-test
npm init -y
npm i prettier -D
// index.js
console.log('hello')
// index.js
console.log("hello")
* replace *
Prettier 에서 불필요하거나, Prettier 와 충돌할 수 있는 모든 규칙을 끕니다.
이 구성은 규칙을 끄기만 하기 때문에 다른 설정과 함께 사용하는 것이 좋습니다.
{
...
"eslintConfig": {
"extends": [
"react-app",
"prettier"
]
},
...
}
Git hooks made easy
mkdir husky-test
cd husky-test
npm init -y
git init
npm i huskey -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "npm test"
}
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8"
}
}
git add -A
git commit -m "husky-test"
Run linters on git staged files
npm i lint-staged -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.js": [
"git add"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8",
"lint-staged": "^9.4.1"
}
}
npm i eslint prettier -D
{
"name": "husky-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"husky": "^3.0.8",
"lint-staged": "^9.4.1"
},
"eslintConfig": {}
}
개발 모드
4-1) React 의 라우팅 이해하기 4-2) Dynamic 라우팅
4-3) Switch 와 NotFound 4-4) JSX 링크로 라우팅 이동하기
4-5) JS 로 라우팅 이동하기 4-6) Redirect
react-router-dom
/
/profile
/about
Client (Browser)
Server
/
/profile
/about
Client (Browser)
브라우저에서 최초에 '/' 경로로 요청을 하면,
React Web App 을 내려줍니다.
내려받은 React App 에서 '/' 경로에 맞는 컴포넌트를 보여줍니다.
React App 에서 다른 페이지로 이동하는 동작을 수행하면,
새로운 경로에 맞는 컴포넌트를 보여줍니다.
npm i react-router-dom
cra 에 기본 내장된 패키지가 아닙니다.
react-router-dom 은 Facebook 의 공식 패키지는 아닙니다.
가장 대표적인 라우팅 패키지입니다.
'/' => Home 컴포넌트
'/profile' => Profile 컴포넌트
'/about' => About 컴포넌트
// src/pages/Home.jsx
export default function Home() {
return <div>Home 페이지 입니다.</div>;
}
// src/pages/Profile.jsx
export default function Profile() {
return <div>Profile 페이지 입니다.</div>;
}
// src/pages/About.jsx
export default function About() {
return <div>About 페이지 입니다.</div>;
}
// src/App.js
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
function App() {
return (
<BrowserRouter>
<Route path="/" component={Home} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
export default App;
- Route 컴포넌트에 경로(path) 와 컴포넌트(component) 를 설정하여 나열해줍니다.
- BrowserRouter 로 Route 들을 감싸줍니다.
- 브라우저에서 요청한 경로에 Route 의 path 가 들어있으면 해당 component 를 보여줍니다.
<Route path="/" exact component={Home} />
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
function App() {
return (
<BrowserRouter>
<Route path="/" exact component={Home} />
<Route path="/profile" exact component={Profile} />
<Route path="/profile/:id" component={Profile} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
export default App;
<Route path="/profile/:id" component={Profile} />
export default function Profile(props) {
const id = props.match.params.id;
console.log(id, typeof id);
return (
<div>
<h2>Profile 페이지입니다.</h2>
{id && <p>id 는 {id} 입니다.</p>}
</div>
);
}
props.match.params.id
typeof id => 'string'
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
function App() {
return (
<BrowserRouter>
<Route path="/" exact component={Home} />
<Route path="/profile" exact component={Profile} />
<Route path="/profile/:id" component={Profile} />
<Route path="/about" component={About} />
</BrowserRouter>
);
}
export default App;
<Route path="/about" component={About} />
// src/pages/About.jsx
export default function About(props) {
const searchParams = props.location.search;
console.log(searchParams);
return (
<div>
<h2>About 페이지 입니다.</h2>
</div>
);
}
props.location.search
// src/pages/About.jsx
export default function About(props) {
const searchParams = new URLSearchParams(props.location.search);
const name = searchParams.get('name');
console.log(name);
return (
<div>
<h2>About 페이지 입니다.</h2>
{name && <p>name 은 {name} 입니다.</p>}
</div>
);
}
new URLSearchParams(props.location.search);
// src/pages/About.jsx
import queryString from 'query-string';
export default function About(props) {
const query = queryString.parse(props.location.search);
const { name } = query;
console.log(name);
return (
<div>
<h2>About 페이지 입니다.</h2>
{name && <p>name 은 {name} 입니다.</p>}
</div>
);
}
const query = queryString.parse(props.location.search);
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
import About from "./pages/About";
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</BrowserRouter>
);
}
export default App;
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
export default App;
export default function NotFound() {
return <div>페이지를 찾을 수 없습니다.</div>;
}
<Link to="/">Home</Link>
<a href="/">Home</a>
앱을 새로고침하면서 경로를 이동합니다.
import { Link } from 'react-router-dom';
브라우저의 주소를 바꾸고,
맞는 Route 로 화면을 변경합니다.
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
import NotFound from './pages/NotFound';
import Links from './components/Links';
function App() {
return (
<BrowserRouter>
<Links />
<Switch>
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
export default App;
import { Link } from 'react-router-dom';
export default function Links() {
return (
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/profile">Profile</Link>
</li>
<li>
<Link to="/profile/1">Profile/1</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/about?name=mark">About?name=mark</Link>
</li>
</ul>
);
}
import { NavLink } from 'react-router-dom';
activeClassName, activeStyle 처럼 active 상태에 대한 스타일 지정이 가능합니다.
Route 의 path 처럼 동작하기 때문에 exact 가 있습니다.
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
import NotFound from './pages/NotFound';
import Links from './components/Links';
import NavLinks from './components/NavLinks';
function App() {
return (
<BrowserRouter>
<Links />
<NavLinks />
<Switch>
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
export default App;
import { NavLink } from "react-router-dom";
const activeStyle = { color: "green" };
export default function NavLinks() {
return (
<ul>
<li>
<NavLink to="/" exact activeStyle={activeStyle}>
Home
</NavLink>
</li>
<li>
<NavLink to="/profile" exact activeStyle={activeStyle}>
Profile
</NavLink>
</li>
<li>
<NavLink to="/profile/1" activeStyle={activeStyle}>
Profile/1
</NavLink>
</li>
<li>
<NavLink
to="/about"
activeStyle={activeStyle}
isActive={(match, location) =>
match !== null && location.search === ""
}
>
About
</NavLink>
</li>
<li>
<NavLink
to="/about?name=mark"
activeStyle={activeStyle}
isActive={(match, location) =>
match !== null && location.search === "?name=mark"
}
>
About?name=mark
</NavLink>
</li>
</ul>
);
}
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
import NotFound from './pages/NotFound';
import Links from './components/Links';
import NavLinks from './components/NavLinks';
import Login from './pages/Login';
function App() {
return (
<BrowserRouter>
<Links />
<NavLinks />
<Switch>
<Route path="/login" component={Login} />
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
export default App;
props.history.push("/");
// src/pages/Login.jsx
export default function Login(props) {
console.log(props);
function login() {
setTimeout(() => {
props.history.push('/');
}, 1000);
}
return (
<div>
<h2>Login 페이지 입니다.</h2>
<button onClick={login}>로그인하기</button>
</div>
);
}
// src/pages/Login.jsx
import LoginButton from '../components/LoginButton';
export default function Login() {
return (
<div>
<h2>Login 페이지 입니다.</h2>
<LoginButton />
</div>
);
}
// src/components/LoginButton.jsx
export default function LoginButton(props) {
console.log(props);
function login() {
setTimeout(() => {
props.history.push('/');
}, 1000);
}
return <button onClick={login}>로그인하기</button>;
}
import { withRouter } from 'react-router-dom';
export default withRouter(function LoginButton(props) {
console.log(props);
function login() {
setTimeout(() => {
props.history.push('/');
}, 1000);
}
return <button onClick={login}>로그인하기</button>;
});
export default withRouter(LoginButton);
import { Redirect } from 'react-router-dom';
// jsx
<Redirect to="/" />
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import Home from './pages/Home';
import Profile from './pages/Profile';
import About from './pages/About';
import NotFound from './pages/NotFound';
import Links from './components/Links';
import NavLinks from './components/NavLinks';
import Login from './pages/Login';
const isLogin = true;
function App() {
return (
<BrowserRouter>
<Links />
<NavLinks />
<Switch>
<Route
path="/login"
render={() => (isLogin ? <Redirect to="/" /> : <Login />)}
/>
<Route path="/profile/:id" component={Profile} />
<Route path="/profile" component={Profile} />
<Route path="/about" component={About} />
<Route path="/" exact component={Home} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
);
}
export default App;
5-1) Style Loaders 5-2) CSS, SASS
5-3) CSS module, SASS module 5-4) Styled-components
5-5) React Shadow 5-6) Ant Design
파일 확장자에 맞는 loader 에게 위임
babel-loader
.js
.jsx
.css
style-loader
css-loader
최종 배포용 파일
babel config
어떤 문법을 번역할건지 설정
npx create-react-app style-loaders-example
cd style-loaders-example
npm run eject
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex, // /\.css$/
exclude: cssModuleRegex, // /\.module\.css$/
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
import './App.css';
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex, // /\.module\.css$/
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
import styles from './App.module.css';
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex, // /\.(scss|sass)$/
exclude: sassModuleRegex, // /\.module\.(scss|sass)$/
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
import './App.scss';
import './App.sass';
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex, // /\.module\.(scss|sass)$/
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader'
),
},
import styles from './App.module.scss';
import styles from './App.module.sass';
// App.js
import './App.css';
<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>
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
}
.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: #09d3ac;
}
.App
.App-header
.App-logo
.App-link
.App {
text-align: center;
}
.App .logo {
height: 40vmin;
}
.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: #09d3ac;
}
.App
.App .header
.App .logo
.App .link
<div className="App">
<header className="header">
<img src={logo} className="logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
.App {
text-align: center;
.logo {
height: 40vmin;
}
.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;
}
.link {
color: #09d3ac;
}
}
.App
.App .header
.App .logo
.App .link
<div className="App">
<header className="header">
<img src={logo} className="logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
npm i sass
import styles from './App.module.css';
console.log(styles);
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
}
.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: #09d3ac;
}
import styles from './App.module.scss';
console.log(styles);
.App {
text-align: center;
.logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.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;
}
.link {
color: #61dafb;
}
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
import logo from "./logo.svg";
import styles from "./App.module.css";
const App = () => {
console.log(styles);
return (
<div className={styles["App"]}>
<header className={styles["App-header"]}>
<img src={logo} className={styles["App-logo"]} alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className={styles["App-link"]}
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
};
export default App;
- Button.module.css
- Button.jsx
import styles from './Button.module.css';
const Button = props => <button className={styles.button} {...props} />;
export default Button;
.button {
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 20px;
}
import styles from './Button.module.css';
export default class Button extends React.Component {
state = {
loading: false,
};
startLoading = () => {
console.log('start');
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
render() {
const { loading } = this.state;
return (
<button
className={
loading ? `${styles.button} ${styles.loading}` : styles.button
}
{...this.props}
onClick={this.startLoading}
/>
);
}
}
.button {
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 20px;
}
.loading {
border: 2px solid grey;
color: grey;
}
npm i classnames
import classNames from 'classnames';
console.log(classNames('foo', 'bar')); // "foo bar"
console.log(classNames('foo', 'bar', 'baz')); // "foo bar baz"
console.log(classNames({ foo: true }, { bar: true })); // "foo bar"
console.log(classNames({ foo: true }, { bar: false })); // "foo"
console.log(classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, '')); // "bar 1"
console.log(classNames(styles.button, styles.loading)); // Button_button__2Ce79 Button_loading__XEngF
import styles from './Button.module.css';
import classNames from 'classnames';
export default class Button extends React.Component {
state = {
loading: false,
};
startLoading = () => {
console.log('start');
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
render() {
const { loading } = this.state;
return (
<button
className={
loading ? classNames(styles.button, styles.loading) : styles.button
}
{...this.props}
onClick={this.startLoading}
/>
);
}
}
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
console.log(cx('button', 'loading')); // Button_button__2Ce79 Button_loading__XEngF
console.log(cx('button', { loading: false })); // Button_button__2Ce79
import styles from './Button.module.css';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
export default class Button extends React.Component {
state = {
loading: false,
};
startLoading = () => {
console.log('start');
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
render() {
const { loading } = this.state;
return (
<button
className={cx('button', { loading })}
{...this.props}
onClick={this.startLoading}
/>
);
}
}
npx create-react-app styled-components-example
cd styled-components-example
npm i styled-components
code .
npm start
import logo from './logo.svg';
import './App.css';
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
<StyledButton>버튼</StyledButton>
</p>
</header>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button``;
export default StyledButton;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled, { css } from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
${props =>
props.primary &&
css`
background: palevioletred;
color: white;
`};
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
<StyledButton primary>Primary 버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled, { css } from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
`;
const PrimaryStyledButton = styled(StyledButton)`
background: palevioletred;
color: white;
`;
export default PrimaryStyledButton;
import PrimaryStyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<PrimaryStyledButton>버튼</PrimaryStyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 1em;
display: inline-block;
text-decoration: none;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton as="a" href="/">
a 태그 버튼
</StyledButton>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 1em;
display: inline-block;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
const UppercaseButton = props => (
<button {...props} children={props.children.toUpperCase()} />
);
function App() {
return (
<div className="App">
<p>
<StyledButton as={UppercaseButton}>button</StyledButton>
<StyledButton>button</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
function MyButton({ className, children }) {
return <button className={className}>MyButton {children}</button>;
}
const StyledButton = styled(MyButton)`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 1em;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>button</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled('button')`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
font-size: 1em;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>button</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid ${props => props.color || 'palevioletred'};
color: ${props => props.color || 'palevioletred'};
margin: 0 1em;
padding: 0.25em 1em;
font-size: 1em;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>button</StyledButton>
<StyledButton color="red">red button</StyledButton>
<StyledButton color="green">green button</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
:hover {
border: 2px solid red;
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
::before {
content: '@';
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
&:hover {
border: 2px solid red;
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
& ~ & {
border: 2px solid red;
}
& + & {
border: 2px solid green;
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>버튼</StyledButton>
<StyledButton>버튼</StyledButton>
<StyledButton>버튼</StyledButton>
<button>버튼</button>
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
&.orange {
border: 2px solid orange;
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton className="orange">버튼</StyledButton>
</p>
<p className="orange">
<StyledButton>버튼</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
.orange {
color: orange;
}
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>
<a className="orange">버튼</a>
</StyledButton>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
border: 1px solid palevioletred;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
button {
color: palevioletred;
}
`;
function App() {
return (
<div className="App">
<p>
<GlobalStyle />
<StyledButton>버튼</StyledButton>
<button>버튼</button>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledButton = styled.button`
border: 1px solid palevioletred;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
button${StyledButton} {
color: palevioletred;
}
`;
function App() {
return (
<div className="App">
<p>
<GlobalStyle />
<StyledButton>버튼</StyledButton>
<button>버튼</button>
</p>
</div>
);
}
export default App;
import styled from 'styled-components';
const StyledA = styled.a.attrs(props => ({
href: props.href || 'https://www.fastcampus.co.kr',
color: props.color || 'palevioletred',
target: '_BLANK',
}))`
color: ${props => props.color};
`;
export default StyledA;
import StyledA from './components/StyledA';
function App() {
return (
<div className="App">
<p>
<StyledA>링크</StyledA>
<StyledA color="red">링크</StyledA>
</p>
</div>
);
}
export default App;
import styled, { keyframes } from 'styled-components';
const slide = keyframes`
from {
margin-top: 0em;
}
to {
margin-top: 1em;
}
`;
const StyledButton = styled.button`
display: inline-block;
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
animation: ${slide} 0.3s ease-in;
`;
export default StyledButton;
import StyledButton from './components/StyledButton';
function App() {
return (
<div className="App">
<p>
<StyledButton>Slide Button</StyledButton>
</p>
</div>
);
}
export default App;
npx create-react-app react-shadow-example
cd react-shadow-example
npm i react-shadow
code .
npm start
/* index.css */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
p {
color: red;
}
.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);
}
}
import logo from "./logo.svg";
import root from "react-shadow";
const styles = `...`;
function App() {
return (
<root.div>
<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>
<style type="text/css">{styles}</style>
</root.div>
);
}
export default App;
npx create-react-app antd-example
cd antd-example
npm i antd
import "./App.css";
import { DatePicker } from "antd";
function App() {
return (
<div className="App">
<DatePicker />
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import App from "./App";
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import 'antd/dist/antd.css'; // <= 전역 스타일 추가 in index.js
import { DatePicker } from 'antd'; // <= 리액트 컴포넌트 in App.js
import "./App.css";
import { DatePicker } from "antd";
import "antd/es/date-picker/style/css";
function App() {
return (
<div className="App">
<DatePicker />
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom';
// import "antd/dist/antd.css";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import DatePicker from 'antd/es/date-picker';
import 'antd/es/date-picker/style/css';
{
...
"babel": {
"presets": [
"react-app"
],
"plugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
]
]
},
...
}
npm run eject
npm install babel-plugin-import --save-dev
import "./App.css";
import { DatePicker } from "antd";
// import "antd/es/date-picker/style/css";
function App() {
return (
<div className="App">
<DatePicker />
</div>
);
}
export default App;
npm install @ant-design/icons
import { Button } from 'antd';
import { HeartOutlined } from "@ant-design/icons";
export default class LoadingButton extends React.Component {
state = {
loading: false,
};
startLoading = () => {
console.log('start');
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
};
render() {
const { loading } = this.state;
return (
<Button
type="primary"
size="large"
icon={<HeartOutlined />}
loading={loading}
onClick={this.startLoading}
style={{
width: 50,
}}
/>
);
}
}
import LoadingButton from './components/LoadingButton';
import { TwitterOutlined } from "@ant-design/icons";
function App() {
return (
<div className="App">
<p>
<LoadingButton />
</p>
<p>
저는 <TwitterOutlined /> 를 잘 안해요!
</p>
</div>
);
}
export default App;
import { Row, Col } from 'antd';
const colStyle = () => ({
height: 50,
backgroundColor: 'red',
opacity: Math.round(Math.random() * 10) / 10,
});
function App() {
return (
<div className="App">
<Row>
<Col span={12} style={colStyle()} />
<Col span={12} style={colStyle()} />
</Row>
<Row>
<Col span={8} style={colStyle()} />
<Col span={8} style={colStyle()} />
<Col span={8} style={colStyle()} />
</Row>
<Row>
<Col span={6} style={colStyle()} />
<Col span={6} style={colStyle()} />
<Col span={6} style={colStyle()} />
<Col span={6} style={colStyle()} />
</Row>
</div>
);
}
export default App;
import { Row, Col } from 'antd';
function MyCol({ span }) {
return (
<Col span={span}>
<div style={{ height: 50, backgroundColor: 'red', opacity: 0.7 }} />
</Col>
);
}
export default function App() {
return (
<div className="App">
<Row gutter={16}>
<MyCol span={12} />
<MyCol span={12} />
</Row>
<Row gutter={16}>
<MyCol span={8} />
<MyCol span={8} />
<MyCol span={8} />
</Row>
<Row gutter={16}>
<MyCol span={6} />
<MyCol span={6} />
<MyCol span={6} />
<MyCol span={6} />
</Row>
</div>
);
}
import { Row, Col } from 'antd';
function MyCol({ span, offset }) {
return (
<Col span={span} offset={offset}>
<div style={{ height: 50, backgroundColor: 'red', opacity: 0.7 }} />
</Col>
);
}
export default function App() {
return (
<div className="App">
<Row gutter={16}>
<MyCol span={12} offset={12} />
</Row>
<Row gutter={16}>
<MyCol span={8} />
<MyCol span={8} offset={8} />
</Row>
<Row gutter={16}>
<MyCol span={6} />
<MyCol span={6} offset={3} />
<MyCol span={6} offset={3} />
</Row>
</div>
);
}
import { Row, Col } from 'antd';
function MyCol({ span, offset }) {
const opacity = Math.round(Math.random() * 10) / 10;
return (
<Col span={span} offset={offset}>
<div style={{ height: 50, backgroundColor: 'red', opacity }} />
</Col>
);
}
export default function App() {
return (
<div className="App">
<Row
style={{
height: 300,
}}
justify="start"
align="top"
>
<MyCol span={4} />
<MyCol span={4} />
<MyCol span={4} />
<MyCol span={4} />
</Row>
</div>
);
}
"start" | "center" | "end" | "space-between" | "space-around"
"top" | "middle" | "bottom"
import { Layout } from 'antd';
const { Header, Sider, Content, Footer } = Layout;
export default function App() {
return (
<div className="App">
<Layout>
<Header>Header</Header>
<Layout>
<Sider>Sider</Sider>
<Content>Content</Content>
</Layout>
<Footer>Footer</Footer>
</Layout>
</div>
);
}
6-1) High Order Component
6-2) Controlled Component 와 Uncontrolled Component
6-3) http 요청하기
Higher Order Component
advanced technique in React for reusing component logic.
not part of the React API
a pattern that emerges from React’s compositional nature.
HOC = function(컴포넌트) { return 새로운 컴포넌트; }
import React from "react";
import { withRouter } from "react-router-dom";
const LoginButton = props => {
console.log(props);
function login() {
setTimeout(() => {
props.history.push("/");
}, 1000);
}
return <button onClick={login}>로그인하기</button>;
};
export default withRouter(LoginButton);
보통 with 가 붙은 함수가 HOC 인 경우가 많다.
export default withRouter(LoginButton);
Use HOCs For Cross-Cutting Concerns
Don’t Mutate the Original Component. Use Composition.
Pass Unrelated Props Through to the Wrapped Component
Maximizing Composability
Wrap the Display Name for Easy Debugging
Don’t Use HOCs Inside the render Method
Static Methods Must Be Copied Over
Refs Aren’t Passed Through (feat. React.forwardRef)
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);
// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
npx create-react-app controlled-uncontrolled-example
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
return (
<div>
<input />
</div>
);
}
}
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
const { value } = this.state;
return (
<div>
<input value={value} onChange={this._change} />
</div>
);
}
_change = e => {
// console.log(e.target.value);
this.setState({ value: e.target.value });
};
}
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
const { value } = this.state;
return (
<div>
<input value={value} onChange={this._change} />
<button onClick={this._click}>전송</button>
</div>
);
}
_change = e => {
// console.log(e.target.value);
this.setState({ value: e.target.value });
};
_click = () => {
console.log('최종 결과', this.state.value);
};
}
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
const { value } = this.state;
return (
<div>
<input value={value} onChange={this._change} />
<button onClick={this._click}>전송</button>
</div>
);
}
_change = e => {
// console.log(e.target.value);
this.setState({ value: e.target.value });
};
_click = () => {
console.log('최종 결과', this.state.value);
};
}
import React from 'react';
export default class Uncontrolled extends React.Component {
_input = React.createRef();
render() {
return (
<div>
<input ret={this._input} />
</div>
);
}
}
import React from 'react';
export default class Uncontrolled extends React.Component {
_input = React.createRef();
render() {
return (
<div>
<input ref={this._input} />
<button onClick={this._click}>전송</button>
</div>
);
}
_click = () => {
console.log('최종 결과', this._input.current.value);
};
}
import React from 'react';
const Uncontrolled = () => {
const inputRef = React.createRef();
function click() {
console.log('최종 결과', inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={click}>전송</button>
</div>
);
};
export default Uncontrolled;
// 책 목록보기
axios.get(
'https://api.marktube.tv/v1/book',
{ headers: `Bearer ${token}` },
);
// 책 추가하기
axios.post(
'https://api.marktube.tv/v1/book',
{
title,
message,
author,
url,
},
{ headers: `Bearer ${token}` },
);
// 책 상세보기
axios.get(
`https://api.marktube.tv/v1/book/${book.id}`,
{ headers: `Bearer ${token}` },
);
// 책 수정하기
axios.patch(
`https://api.marktube.tv/v1/book/${book.id}`,
{
title,
message,
author,
url,
},
{ headers: `Bearer ${token}` },
);
// 책 삭제하기
axios.delete(
`https://api.marktube.tv/v1/book/${book.id}`,
{ headers: `Bearer ${token}` },
);
1) Basic Hooks 2) Custom Hooks
3) Additional Hooks 4) React Router Hooks
5) 컴포넌트 간 통신 6) Context API
useState
useEffect
useContext (Context API 에서 다룹니다.)
npx create-react-app react-hooks-example
import React from 'react';
export default class Example1 extends React.Component {
state = {
count: 0,
};
render() {
const { count } = this.state;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={this.click}>Click me</button>
</div>
);
}
click = () => {
this.setState({ count: this.state.count + 1 });
};
}
import React, { useState } from 'react';
const Example2 = () => {
const [count, setCount] = useState(0);
function click() {
setCount(count + 1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={click}>Click me</button>
</div>
);
};
export default Example2;
const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);
import React, { useState } from 'react';
const Example3 = () => {
const [state, setState] = useState({ count: 0 });
function click() {
setState({ count: state.count + 1 });
}
return (
<div>
<p>You clicked {state.count} times</p>
<button onClick={click}>Click me</button>
</div>
);
};
export default Example3;
const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);
컴포넌트 사이에서 상태와 관련된 로직을 재사용하기 어렵습니다.
컨테이너 방식 말고, 상태와 관련된 로직
복잡한 컴포넌트들은 이해하기 어렵습니다.
Class 는 사람과 기계를 혼동시킵니다.
컴파일 단계에서 코드를 최적화하기 어렵게 만든다.
this.state 는 로직에서 레퍼런스를 공유하기 때문에 문제가 발생할 수 있다.
좋은 것일까 ?
import React from 'react';
export default class Example4 extends React.Component {
state = { count: 0 };
componentDidMount() {
console.log('componentDidMount', this.state.count);
}
componentDidUpdate() {
console.log('componentDidUpdate', this.state.count);
}
render() {
const { count } = this.state;
return (
<div>
<p>You clicked {count} times</p>
<button onClick={this.click}>Click me</button>
</div>
);
}
click = () => {
this.setState({ count: this.state.count + 1 });
};
}
import React, { useState, useEffect } from 'react';
const Example5 = () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('componentDidMount & componentDidUpdate', count);
});
function click() {
setCount(count + 1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={click}>Click me</button>
</div>
);
};
export default Example5;
import React from 'react';
export default class Example6 extends React.Component {
state = {
time: new Date(),
};
_timer = null;
componentDidMount() {
this._timer = setInterval(() => {
this.setState({ time: new Date() });
}, 1000);
}
componentWillUnmount() {
clearInterval(this._timer);
}
render() {
const { time } = this.state;
return <div>{time.toISOString()}</div>;
}
}
import React, { useState, useEffect } from 'react';
const Example7 = () => {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <div>{time.toISOString()}</div>;
};
export default Example7;
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
function click() {
setCount(count + 1);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={click}>Click me</button>
</div>
);
}
from 리액트
"컴포넌트야,
state가 0 일 때의 UI를 보여줘."
from 컴포넌트
from 리액트
"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."
from 브라우저
"좋아, 화면에 그려줄게."
리액트
좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.
() => { document.title = 'You clicked 0 times' } 를 실행.
from 컴포넌트
"이봐 리액트, 내 상태를 1 로 변경해줘."
from 리액트
"상태가 1 일때의 UI를 줘."
from 컴포넌트
"여기 랜더링 결과물로 <p>You clicked 1 times</p> 가 있어."
"그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마."
() => { document.title = 'You clicked 1 times' }.
from 리액트
"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."
from 브라우저
"좋아, 화면에 그려줄게."
리액트
좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.
() => { document.title = 'You clicked 1 times' } 를 실행.
useSomething
// hooks/useWindowWidth.js
import { useState, useEffect } from 'react';
export default function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => {
window.removeEventLister('resize', onResize);
};
}, []);
return width;
}
// hocs/withHasMounted.js
import React from 'react';
export default function withHasMounted(Component) {
class WrapperComponent extends React.Component {
state = {
hasMounted: false,
};
componentDidMount() {
this.setState({
hasMounted: true,
});
}
render() {
const { hasMounted } = this.state;
return <Component {...this.props} hasMounted={hasMounted} />;
}
}
WrapperComponent.displayName = `withHasMounted(${Component.name})`;
return WrapperComponent;
}
// hooks/useHasMounted.js
import { useState, useEffect } from 'react';
export default function useHasMounted() {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
useReducer
useCallback, useMemo
useRef, useImperativeHandle,
useLayoutEffect
useDebugValue
다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우
다음 state가 이전 state에 의존적인 경우
Redux 를 안다면 쉽게 사용 가능
import React, { useReducer, useEffect } from 'react';
const Example8 = ({ count }) => {
const [state, dispatch] = useReducer(reducer, { count });
useEffect(() => {
setTimeout(() => {
dispatch({ type: 'PLUS' });
}, 2000);
}, []);
function click() {
dispatch({ type: 'PLUS' });
}
return (
<div>
<p>You clicked {state.count} times</p>
<button onClick={click}>Click me</button>
</div>
);
};
export default Example8;
const reducer = (state, action) => {
if (action.type === 'PLUS') {
return {
count: state.count + 1,
};
}
return state;
};
import React, { useState } from 'react';
function sum(persons) {
console.log('sum...');
return persons.map(person => person.age).reduce((l, r) => l + r, 0);
}
const Example9 = () => {
const [value, setValue] = useState('');
const [persons] = useState([{ name: 'Mark', age: 38 }, { name: 'Hanna', age: 27 }]);
function change(e) {
setValue(e.target.value);
}
const count = sum(persons);
return (
<div>
<input value={value} onChange={change} />
<p>{count}</p>
</div>
);
};
export default Example9;
import React, { useState, useMemo } from 'react';
function sum(persons) {
console.log('sum...');
return persons.map(person => person.age).reduce((l, r) => l + r, 0);
}
const Example9 = () => {
const [value, setValue] = useState('');
const [persons] = useState([{ name: 'Mark', age: 38 }, { name: 'Hanna', age: 27 }]);
function change(e) {
setValue(e.target.value);
}
const count = useMemo(() => sum(persons), [persons]);
return (
<div>
<input value={value} onChange={change} />
<p>{count}</p>
</div>
);
};
export default Example9;
import React, { useState } from 'react';
const Example10 = () => {
const [value, setValue] = useState('');
const [persons, setPersons] = useState([
{ id: 0, name: 'Mark', age: 38 },
{ id: 1, name: 'Hanna', age: 27 },
]);
function change(e) {
setValue(e.target.value);
}
function click(id) {
setPersons(
persons => persons.map(person =>
person.id === id
? { ...person, age: person.age + 1 }
: { ...person },
)
);
}
return (
<div>
<input value={value} onChange={change} />
{persons.map(person => (
<Person {...person} key={person.id} click={click} />
))}
</div>
);
};
export default Example10;
const Person = React.memo(({ id, name, age, click }) => {
console.log('Person...');
function onClick() {
click(id);
}
return (
<div>
{name}, {age} <button onClick={onClick}>+</button>
</div>
);
});
import React, { useState } from 'react';
const Example10 = () => {
const [value, setValue] = useState('');
const [persons, setPersons] = useState([
{ id: 0, name: 'Mark', age: 38 },
{ id: 1, name: 'Hanna', age: 27 },
]);
function change(e) {
setValue(e.target.value);
}
const click = useCallback(id => {
setPersons(persons => {
return persons.map(person =>
person.id === id
? {
...person,
age: person.age + 1,
}
: {
...person,
},
);
});
}, []);
return (
<div>
<input value={value} onChange={change} />
{persons.map(person => (
<Person {...person} key={person.id} click={click} />
))}
</div>
);
};
export default Example10;
const Person = React.memo(({ id, name, age, click }) => {
console.log('Person...');
function onClick() {
click(id);
}
return (
<div>
{name}, {age} <button onClick={onClick}>+</button>
</div>
);
});
import React, { useRef, useEffect, useState } from 'react';
const Example11 = () => {
const [count, setCount] = useState(0);
const inputCreateRef = React.createRef();
const inputUseRef = useRef();
console.log(inputCreateRef.current);
console.log(inputUseRef.current);
useEffect(() => {
setTimeout(() => {
setCount(count => count + 1);
}, 1000);
});
return (
<div>
<p>{count}</p>
<input ref={inputCreateRef} />
<input ref={inputUseRef} />
</div>
);
};
export default Example11;
npx create-react-app component-communication
<A /> 컴포넌트에서 button 에 onClick 이벤트를 만들고,
button 을 클릭하면, <A /> 의 state 를 변경하여, <B /> 로 내려주는 props 를 변경
<B /> 의 props 가 변경되면, <C /> 의 props 에 전달
<C /> 의 props 가 변경되면, <D /> 의 props 로 전달
<D /> 의 props 가 변경되면, <E /> 의 props 로 전달
// A 컴포넌트
<div>
<B />
<button>클릭</button>
</div>
// B 컴포넌트
<div>
<C />
</div>
// C 컴포넌트
<div>
<D />
</div>
// D 컴포넌트
<div>
<E />
</div>
// E 컴포넌트
<div>
{props.value}
</div>
import React from "react";
class A extends React.Component {
state = {
value: "아직 안바뀜"
};
render() {
console.log("A render");
return (
<div>
<B {...this.state} />
<button onClick={this._click}>E 의 값을 바꾸기</button>
</div>
);
}
_click = () => {
this.setState({
value: "E 의 값을 변경"
});
};
}
export default A;
const B = props => (
<div>
<p>여긴 B</p>
<C {...props} />
</div>
);
const C = props => (
<div>
<p>여긴 C</p>
<D {...props} />
</div>
);
const D = props => (
<div>
<p>여긴 D</p>
<E {...props} />
</div>
);
const E = props => (
<div>
<p>여긴 E</p>
<h3>{props.value}</h3>
</div>
);
<A /> 에 함수를 만들고, 그 함수 안에 state 를 변경하도록 구현, 그 변경으로 인해 p 안의 내용을 변경.
만들어진 함수를 props 에 넣어서, <B /> 로 전달
<B /> 의 props 의 함수를 <C /> 의 props 로 전달
<C /> 의 props 의 함수를 <D /> 의 props 로 전달
<D /> 의 Props 의 함수를 <E /> 의 props 로 전달, <E /> 에서 클릭하면 props 로 받은 함수를 실행
// A 컴포넌트
<div>
<B />
<p>{state.value}</p>
</div>
// B 컴포넌트
<div>
<C />
</div>
// C 컴포넌트
<div>
<D />
</div>
// D 컴포넌트
<div>
<E />
</div>
// E 컴포넌트
<div>
<button>클릭</button>
</div>
import React from "react";
class A extends React.Component {
state = {
value: "아직 안바뀜"
};
render() {
console.log("A render");
return (
<div>
<h3>{this.state.value}</h3>
<B change={this.change} />
</div>
);
}
change = () => {
this.setState({
value: "A 의 값을 변경"
});
};
}
export default A;
const B = props => (
<div>
<p>여긴 B</p>
<C {...props} />
</div>
);
const C = props => (
<div>
<p>여긴 C</p>
<D {...props} />
</div>
);
const D = props => (
<div>
<p>여긴 D</p>
<E {...props} />
</div>
);
const E = props => {
function click() {
props.change();
}
return (
<div>
<p>여긴 E</p>
<button onClick={click}>클릭</button>
</div>
);
};
npx create-react-app react-context-example
데이터를 Set 하는 놈
가장 상위 컴포넌트 => 프로바이더
데이터를 Get 하는 놈
모든 하위 컴포넌트에서 접근 가능
컨슈머로 하는 방법
클래스 컴포넌트의 this.context 로 하는 방법
펑셔널 컴포넌트의 useContext 로 하는 방법
일단 컨텍스트를 생성한다.
컨텍스트.프로바이더 를 사용한다.
value 를 사용
import React from 'react';
const PersonContext = React.createContext();
export default PersonContext;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import PersonContext from './contexts/PersonContext';
const persons = [
{ id: 0, name: 'Mark', age: 38 },
{ id: 1, name: 'Hanna', age: 27 },
];
ReactDOM.render(
<PersonContext.Provider value={persons}>
<App />
</PersonContext.Provider>,
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
컨텍스트를 가져온다.
컨텍스트.컨슈머를 사용한다.
value 를 사용
import React from 'react';
import PersonContext from '../contexts/PersonContext';
const Example1 = () => (
<PersonContext.Consumer>
{value => <ul>{JSON.stringify(value)}</ul>}
</PersonContext.Consumer>
);
export default Example1;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';
export default function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Example1 />
</header>
</div>
);
}
static contextType 에 컨텍스트를 설정한다.
this.context => value 이다.
import React from 'react';
import PersonContext from '../contexts/PersonContext';
export default class Example2 extends React.Component {
static contextType = PersonContext;
render() {
return <ul>{JSON.stringify(this.context)}</ul>;
}
}
// Example2.contextType = PersonContext;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';
import Example2 from './components/Example2';
export default function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Example1 />
<Example2 />
</header>
</div>
);
}
useContext 로 컨텍스트를 인자로 호출한다.
useContext 의 리턴이 value 이다.
import React, { useContext } from 'react';
import PersonContext from '../contexts/PersonContext';
const Example3 = () => {
const value = useContext(PersonContext);
return <ul>{JSON.stringify(value)}</ul>;
};
export default Example3;
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Example1 from './components/Example1';
import Example2 from './components/Example2';
import Example3 from './components/Example3';
export default function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Example1 />
<Example2 />
<Example3 />
</header>
</div>
);
}
1) JavaScript Unit Test && Jest 사용하기
2) React Component Test
3) testing-library/react 활용하기
사람을 믿으시겠습니까 ? 테스트 코드를 믿으시겠습니까 ?
실제로는 사람이 아니라, 사람의 감 입니다.
코드는 거짓말을 하지 않습니다.
통합테스트에 비해 빠르고 쉽습니다.
통합테스트를 진행하기 전에 문제를 찾아낼 수 있습니다.
그렇다고, 통합테스트가 성공하리란 보장은 없습니다.
테스트 코드가 살아있는(동작을 설명하는) 명세가 됩니다.
테스트를 읽고 어떻게 동작하는지도 예측 가능합니다.
소프트웨어 장인이 되려면 TDD 해야죠..
선 코딩 후, (몰아서) 단위테스트가 아니라
리액트의 영향이 크겠지만 가장 핫한 테스트 도구
👩🏻💻 Easy Setup
🏃🏽 Instant Feedback
고친 파일만 빠르게 테스트 다시 해주는 기능 등
📸 Snapshot Testing
컴포넌트 테스트에 중요한 역할을 하는 스냅샷
mkdir jest-example
cd jest-example
npm init -y
npm i jest -D
{
"name": "jest-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^24.9.0"
}
}
test('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
describe('expect test', () => {
it('37 to equal 37', () => {
const received = 37;
const expected = 37;
expect(received).toBe(expected);
});
it('{age: 37} to equal {age: 37}', () => {
const received = {
age: 37,
};
const expected = {
age: 37,
};
expect(received).toBe(expected);
});
it('{age: 37} to equal {age: 37}', () => {
const received = {
age: 37,
};
const expected = {
age: 37,
};
expect(received).toEqual(expected);
});
});
describe('.to~ test', () => {
it('.toBe', () => {
expect(37).toBe(37);
});
it('.toHaveLength', () => {
expect('hello').toHaveLength(5);
});
it('.toHaveProperty', () => {
expect({ name: 'Mark' }).toHaveProperty('name');
expect({ name: 'Mark' }).toHaveProperty('name', 'Mark');
});
it('.toBeDefined', () => {
expect({ name: 'Mark' }.name).toBeDefined();
});
it('.toBeFalsy', () => {
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(NaN).toBeFalsy();
});
it('.toBeGreaterThan', () => {
expect(10).toBeGreaterThan(9);
});
});
describe('.to~ test', () => {
it('.toBeGreaterThanOrEqual', () => {
expect(10).toBeGreaterThanOrEqual(10);
});
it('.toBeInstanceOf', () => {
class Foo {}
expect(new Foo()).toBeInstanceOf(Foo);
});
it('.toBeNull', () => {
expect(null).toBeNull();
});
it('.toBeTruthy', () => {
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect('hello').toBeTruthy();
expect({}).toBeTruthy();
});
it('.toBeUndefined', () => {
expect({ name: 'Mark' }.age).toBeUndefined();
});
it('.toBeNaN', () => {
expect(NaN).toBeNaN();
});
});
describe('.not.to~ test', () => {
it('.not.toBe', () => {
expect(37).not.toBe(36);
});
it('.not.toBeFalsy', () => {
expect(true).not.toBeFalsy();
expect(1).not.toBeFalsy();
expect('hello').not.toBeFalsy();
expect({}).not.toBeFalsy();
});
it('.not.toBeGreaterThan', () => {
expect(10).not.toBeGreaterThan(10);
});
});
describe('use async test', () => {
it('setTimeout without done', () => {
setTimeout(() => {
expect(37).toBe(36);
}, 1000);
});
it('setTimeout with done', done => {
setTimeout(() => {
expect(37).toBe(36);
done();
}, 1000);
});
});
describe('use async test', () => {
it('promise then', () => {
function p() {
return new Promise(resolve => {
setTimeout(() => {
resolve(37);
}, 1000);
});
}
return p().then(data => expect(data).toBe(37));
});
it('promise catch', () => {
function p() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, 1000);
});
}
return p().catch(e => expect(e).toBeInstanceOf(Error));
});
});
describe('use async test', () => {
it('promise .resolves', () => {
function p() {
return new Promise(resolve => {
setTimeout(() => {
resolve(37);
}, 1000);
});
}
return expect(p()).resolves.toBe(37);
});
it('promise .rejects', () => {
function p() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, 1000);
});
}
return expect(p()).rejects.toBeInstanceOf(Error);
});
});
describe('use async test', () => {
it('async-await', async () => {
function p() {
return new Promise(resolve => {
setTimeout(() => {
resolve(37);
}, 1000);
});
}
const data = await p();
return expect(data).toBe(37);
});
});
describe('use async test', () => {
it('async-await, catch', async () => {
function p() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('error'));
}, 1000);
});
}
try {
await p();
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
});
npx create-react-app react-component-test
cd react-component-test
npm test
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
__tests__ 폴더 안의 .js 파일
.test.js 로 끝나는 파일
.spec.js 로 끝나는 파일
// src/components/Button.test.js
import React from "react";
import Button from "./Button";
import { render } from "@testing-library/react";
describe("Button 컴포넌트 (@testing-library/react)", () => {
it("컴포넌트가 정상적으로 생성된다.", async () => {
render(<Button />);
});
});
// src/components/Button.jsx
import React from "react";
const Button = () => <></>;
export default Button;
describe("Button 컴포넌트", () => {
// ...
it(`"button" 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다.`, () => {
const { getByText } = render(<Button />);
const buttonElement = getByText("button");
expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
});
});
// src/components/Button.jsx
import React from "react";
const Button = () => <button>button</button>;
export default Button;
describe("Button 컴포넌트 (@testing-library/react)", () => {
// ...
it(`버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다." 라고 쓰여진다.`, () => {
const { getByText } = render(<Button />);
const button = getByText("button");
fireEvent.click(button);
const p = getByText("버튼이 방금 눌렸다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
});
// src/components/Button.jsx
import React from "react";
const Button = () => (
<>
<button>button</button>
<p>버튼이 방금 눌렸다.</p>
</>
);
export default Button;
describe("Button 컴포넌트 (@testing-library/react)", () => {
// ...
it(`버튼을 클릭하기 전에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.`, () => {
const { getByText } = render(<Button />);
const p = getByText("버튼이 눌리지 않았다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
});
// src/components/Button.jsx
import React, { useState } from "react";
const Button = () => {
const [message, setMessage] = useState("버튼이 눌리지 않았다.");
function click() {
setMessage("버튼이 방금 눌렸다.");
}
return (
<>
<button onClick={click}>button</button>
<p>{message}</p>
</>
);
};
export default Button;
jest.useFakeTimers();
describe("Button 컴포넌트 (@testing-library/react)", () => {
// ...
it(`버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.`, async () => {
const { getByText } = render(<Button />);
const button = getByText("button");
fireEvent.click(button);
jest.advanceTimersByTime(5000);
const p = getByText("버튼이 눌리지 않았다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
});
jest.useFakeTimers();
describe("Button 컴포넌트 (@testing-library/react)", () => {
// ...
it(`버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.`, async () => {
const { getByText } = render(<Button />);
const button = getByText("button");
fireEvent.click(button);
act(() => {
jest.advanceTimersByTime(5000);
});
const p = getByText("버튼이 눌리지 않았다.");
expect(p).not.toBeNull();
expect(p).toBeInstanceOf(HTMLParagraphElement);
});
});
// src/components/Button.jsx
import React, { useState } from "react";
const Button = () => {
const [message, setMessage] = useState("버튼이 눌리지 않았다.");
function click() {
setMessage("버튼이 방금 눌렸다.");
setTimeout(() => {
setMessage("버튼이 눌리지 않았다.");
}, 5000);
}
return (
<>
<button onClick={click}>button</button>
<p>{message}</p>
</>
);
};
export default Button;
// src/components/Button.jsx
import React, { useState, useEffect, useRef } from "react";
const Button = () => {
const [message, setMessage] = useState("버튼이 눌리지 않았다.");
const timer = useRef(null);
function click() {
if (timer.current !== null) clearTimeout(timer);
setMessage("버튼이 방금 눌렸다.");
timer.current = setTimeout(() => {
setMessage("버튼이 눌리지 않았다.");
}, 5000);
}
useEffect(() => {
return () => {
if (timer.current !== null) clearTimeout(timer.current);
};
}, []);
return (
<>
<button onClick={click}>button</button>
<p>{message}</p>
</>
);
};
export default Button;
jest.useFakeTimers();
describe("Button 컴포넌트 (@testing-library/react)", () => {
// ...
it(`버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.`, () => {
const { getByText } = render(<Button />);
const button = getByText("button");
fireEvent.click(button);
expect(button.disabled).toBeTruthy();
act(() => {
jest.advanceTimersByTime(5000);
});
expect(button.disabled).toBeFalsy();
});
});
// src/components/Button.jsx
import React, { useState, useEffect, useRef } from "react";
const Button = () => {
const [message, setMessage] = useState("버튼이 눌리지 않았다.");
const timer = useRef(null);
function click() {
if (timer.current !== null) clearTimeout(timer);
setMessage("버튼이 방금 눌렸다.");
timer.current = setTimeout(() => {
setMessage("버튼이 눌리지 않았다.");
}, 5000);
}
useEffect(() => {
return () => {
if (timer.current !== null) clearTimeout(timer.current);
};
}, []);
return (
<>
<button onClick={click} disabled={message === "버튼이 방금 눌렸다."}>
button
</button>
<p>{message}</p>
</>
);
};
export default Button;
describe("Button 컴포넌트", () => {
// ...
t(`버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.`, () => {
const { getByText } = render(<Button />);
const button = getByText("button");
fireEvent.click(button);
expect(button).toBeDisabled();
act(() => {
jest.advanceTimersByTime(5000);
});
expect(button).not.toBeDisabled();
});
});
1) Optimizing Performance
2) React.createPortal
3) React.forwardRef
필요할 때만 랜더한다.
랜더 전후의 일치 여부를 판단하는 규칙
서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
개발자가 key prop 을 통해,
여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setInterval(() => {
this.setState({
count: this.state.count + 1
});
}, 1000);
}
render() {
if (this.state.count % 2 === 0) {
return (
<div>
<Foo />
</div>
);
}
return (
<span>
<Foo />
</span>
);
}
}
class Foo extends React.Component {
componentDidMount() {
console.log("Foo componentDidMount");
}
componentWillUnmount() {
console.log("Foo componentWillUnmount");
}
render() {
return <p>Foo</p>;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setInterval(() => {
this.setState({
count: this.state.count + 1
});
}, 1000);
}
render() {
if (this.state.count % 2 === 0) {
return <div className="before" title="stuff" />;
}
return <div className="after" title="stuff" />;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setInterval(() => {
this.setState({
count: this.state.count + 1
});
}, 1000);
}
render() {
if (this.state.count % 2 === 0) {
return <div style={{ color: "red", fontWeight: "bold" }} />;
}
return <div style={{ color: "green", fontWeight: "bold" }} />;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setInterval(() => {
this.setState({
count: this.state.count + 1
});
}, 1000);
}
render() {
if (this.state.count % 2 === 0) {
return <Foo name="Mark" />;
}
return <Foo name="Anna" />;
}
}
class Foo extends React.Component {
state = {};
componentDidMount() {
console.log("Foo componentDidMount");
}
componentWillUnmount() {
console.log("Foo componentWillUnmount");
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("Foo getDerivedStateFromProps", nextProps, prevState);
return {};
}
render() {
console.log("Foo render");
return <p>Foo</p>;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setTimeout(() => {
this.setState({
count: this.state.count + 1
});
}, 3000);
}
render() {
if (this.state.count % 2 === 0) {
return (
<ul>
<Foo>first</Foo>
<Foo>second</Foo>
</ul>
);
}
return (
<ul>
<Foo>first</Foo>
<Foo>second</Foo>
<Foo>third</Foo>
</ul>
);
}
}
class Foo extends React.Component {
state = {};
componentDidMount() {
console.log("Foo componentDidMount", this.props.children);
}
componentWillUnmount() {
console.log("Foo componentWillUnmount");
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("Foo getDerivedStateFromProps", nextProps, prevState);
return {};
}
render() {
console.log("Foo render", this.props.children);
return <p>{this.props.children}</p>;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setTimeout(() => {
this.setState({
count: this.state.count + 1
});
}, 3000);
}
render() {
if (this.state.count % 2 === 0) {
return (
<ul>
<Foo>second</Foo>
<Foo>third</Foo>
</ul>
);
}
return (
<ul>
<Foo>first</Foo>
<Foo>second</Foo>
<Foo>third</Foo>
</ul>
);
}
}
class Foo extends React.Component {
state = {};
componentDidMount() {
console.log("Foo componentDidMount", this.props.children);
}
componentWillUnmount() {
console.log("Foo componentWillUnmount");
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("Foo getDerivedStateFromProps", nextProps, prevState);
return {};
}
render() {
console.log("Foo render", this.props.children);
return <p>{this.props.children}</p>;
}
}
class App extends React.Component {
state = {
count: 0
};
componentDidMount() {
setTimeout(() => {
this.setState({
count: this.state.count + 1
});
}, 3000);
}
render() {
if (this.state.count % 2 === 0) {
return (
<ul>
<Foo key="2">second</Foo>
<Foo key="3">third</Foo>
</ul>
);
}
return (
<ul>
<Foo key="1">first</Foo>
<Foo key="2">second</Foo>
<Foo key="3">third</Foo>
</ul>
);
}
}
class Foo extends React.Component {
state = {};
componentDidMount() {
console.log("Foo componentDidMount", this.props.children);
}
componentWillUnmount() {
console.log("Foo componentWillUnmount");
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("Foo getDerivedStateFromProps", nextProps, prevState);
return {};
}
render() {
console.log("Foo render", this.props.children);
return <p>{this.props.children}</p>;
}
}
class App extends React.Component {
state = {
text: "",
persons: [
{ id: 1, name: "Mark", age: 37 },
{ id: 2, name: "Anna", age: 26 },
]
};
render() {
console.log("App render");
const { text, persons } = this.state;
return (
<div>
<input type="text" value={text} onChange={this._change} />
<button onClick={this._click}>click</button>
<ul>
{persons.map(p => (
<Person {...p} key={p.id} />
))}
</ul>
</div>
);
}
_change = e => {
this.setState({
...this.state,
text: e.target.value
});
};
_click = () => {
console.log(this.state.text);
};
}
class Person extends React.Component {
render() {
console.log("Person render");
const { name, age } = this.props;
return (
<ul>
{name} / {age}
</ul>
);
}
}
class App extends React.Component {
state = {
text: "",
persons: [
{ id: 1, name: "Mark", age: 37 },
{ id: 2, name: "Anna", age: 26 },
]
};
render() {
console.log("App render");
const { text, persons } = this.state;
return (
<div>
<input type="text" value={text} onChange={this._change} />
<button onClick={this._click}>click</button>
<ul>
{persons.map(p => (
<Person {...p} key={p.id} />
))}
</ul>
</div>
);
}
_change = e => {
this.setState({
...this.state,
text: e.target.value
});
};
_click = () => {
console.log(this.state.text);
};
}
class Person extends React.Component {
shouldComponentUpdate(previousProps) {
for (const key in this.props) {
if (previousProps[key] !== this.props[key]) {
return true;
}
}
return false;
}
render() {
console.log("Person render");
const { name, age } = this.props;
return (
<ul>
{name} / {age}
</ul>
);
}
}
class App extends React.Component {
state = {
text: "",
persons: [
{ id: 1, name: "Mark", age: 37 },
{ id: 2, name: "Anna", age: 26 },
]
};
render() {
console.log("App render");
const { text, persons } = this.state;
return (
<div>
<input type="text" value={text} onChange={this._change} />
<button onClick={this._click}>click</button>
<ul>
{persons.map(p => (
<Person {...p} key={p.id} />
))}
</ul>
</div>
);
}
_change = e => {
this.setState({
...this.state,
text: e.target.value
});
};
_click = () => {
console.log(this.state.text);
};
}
class Person extends React.PureComponent {
render() {
console.log("Person render");
const { name, age } = this.props;
return (
<ul>
{name} / {age}
</ul>
);
}
}
class App extends React.Component {
state = {
text: "",
persons: [
{ id: 1, name: "Mark", age: 37 },
{ id: 2, name: "Anna", age: 26 },
]
};
render() {
console.log("App render");
const { text, persons } = this.state;
return (
<div>
<input type="text" value={text} onChange={this._change} />
<button onClick={this._click}>click</button>
<ul>
{persons.map(p => (
<Person {...p} key={p.id} onClick={() => {}} />
))}
</ul>
</div>
);
}
_change = e => {
this.setState({
...this.state,
text: e.target.value
});
};
_click = () => {
console.log(this.state.text);
};
}
class Person extends React.PureComponent {
render() {
console.log("Person render");
const { name, age } = this.props;
return (
<ul>
{name} / {age}
</ul>
);
}
}
class App extends React.Component {
state = {
text: "",
persons: [
{ id: 1, name: "Mark", age: 37 },
{ id: 2, name: "Anna", age: 26 },
]
};
render() {
console.log("App render");
const { text, persons } = this.state;
return (
<div>
<input type="text" value={text} onChange={this._change} />
<button onClick={this._click}>click</button>
<ul>
{persons.map(p => (
<Person {...p} key={p.id} onClick={() => {}} />
))}
</ul>
</div>
);
}
_change = e => {
this.setState({
...this.state,
text: e.target.value
});
};
_click = () => {
console.log(this.state.text);
};
}
const Person = React.memo(props => {
console.log("Person render");
const { name, age } = props;
return (
<ul>
{name} / {age}
</ul>
);
});
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal"></div>
</body>
</html>
body {...}
#modal {
position: absolute;
top: 0;
left: 0;
}
import ReactDOM from 'react-dom';
const Modal = ({ children }) =>
ReactDOM.createPortal(children, document.querySelector('#modal'));
export default Modal;
const Home = () => {
const [visible, setVisible] = useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
return (
<div>
<h1>Home</h1>
<button onClick={click}>open</button>
<NaviContainer />
<BooksContainer />
{visible && (
<Modal>
<div
style={{
width: '100vw',
height: '100vh',
background: 'rgba(0, 0, 0, 0.5)',
}}
onClick={hide}
>
Hello
</div>
</Modal>
)}
</div>
);
};
1) SPA 프로젝트 배포 이해하기
2) serve 패키지로 React Wep App 배포하기
3) AWS S3 에 React Wep App 배포하기
4) NginX 로 React Wep App 배포하기
5) node.js express 로 React Wep App 배포하기
6) 서버사이드 렌더링 이해하기
SPA Deploy
git clone https://github.com/xid-mark/tic-tac-toe.git
cd tic-tac-toe
npm ci
npm run build
npm install serve -g
serve -s build
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::react-camp/*"
]
}
]
}
sudo apt-get update
sudo apt-get upgrade
wget http://nginx.org/keys/nginx_signing.key
sudo apt-key add nginx_signing.key
sudo rm -rf nginx_signing.key
sudo nano /etc/apt/sources.list
```
deb http://nginx.org/packages/mainline/ubuntu/ trusty nginx
deb-src http://nginx.org/packages/mainline/ubuntu/ trusty nginx
```
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install nginx
nginx -v
server {
listen 80;
server_name localhost;
root /home/ubuntu/tic-tac-toe/build;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
npm i express
const express = require('express');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
// app.get('/', function(req, res) {
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(9000);
1) 프로젝트 개요
2) 기본 디자인 설정하기
3) 상품 리스트 보여주기
4) 상품 선택과 삭제를 주문에서 보여주기
Context API 와 Hooks 를 이용한 전역 상태 관리
npx create-react-app prototype-shop
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<link
href="https://fonts.googleapis.com/css?family=Roboto&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
/* reset */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* apply a natural box layout model to all elements, but allowing views to change */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
button:focus {
outline: 0;
}
/* prototypes */
:root {
--primary: #021d49;
--primary-alpha: rgba(2, 29, 73, 0.5);
--secondary: rgb(255, 102, 97);
--secondary-alpha: rgba(0, 255, 126, 0.5);
--gray: #f6f6f6;
--gray-alpha: rgba(246, 246, 246, 0.9);
--black-color: #222223;
--red-color: #ff4d4f;
}
::selection {
background: var(--secondary);
}
::-moz-selection {
background: var(--secondary);
}
body,
button {
font-family: "Roboto", sans-serif;
font-size: 1.4rem;
background: var(--gray);
color: var(--primary);
}
.container {
margin-left: auto;
margin-right: auto;
padding-left: 0.4rem;
padding-right: 0.4rem;
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 1000px;
}
@media (min-width: 768px) {
.container {
display: grid;
grid-template-columns: 0.7fr 0.3fr;
grid-template-rows: auto 1fr auto;
}
}
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
a:active,
a:hover {
outline-width: 0;
}
a {
color: var(--primary);
outline: none;
text-decoration: none;
}
a:focus,
a:hover,
a:active,
a.active {
color: var(--secondary);
text-decoration: underline;
}
header {
grid-column: span 2;
padding: 1vw;
text-align: center;
background-color: white;
}
header .btn__area {
margin-top: 40px;
margin-bottom: 40px;
}
header a {
display: inline-block;
}
header button {
display: inline-block;
font-weight: 700;
border-radius: 4px;
cursor: pointer;
transition: color 0.3s ease 0s, border-color 0.3s ease 0s,
background-color 0.3s ease 0s;
text-decoration: none;
border: 2px solid rgb(255, 102, 97);
background-color: rgb(255, 102, 97);
font-size: 1.2rem;
line-height: 22px;
padding: 15px 24px;
color: rgb(255, 255, 255) !important;
line-height: 22px;
}
.header__container {
margin-top: 50px;
margin-left: auto;
margin-right: auto;
padding-left: 0.4rem;
padding-right: 0.4rem;
display: flex;
flex-direction: column;
max-width: 1000px;
}
header .title {
font-size: 3rem;
line-height: 4rem;
font-weight: bold;
}
header .subtitle {
margin-top: 24px;
font-size: 22px;
line-height: 28px;
margin: 16px 0px 0px;
color: rgb(71, 71, 71);
font-weight: 500;
vertical-align: baseline;
}
.logo {
width: 25vw;
min-width: 160px;
max-width: 380px;
}
aside {
flex: 2;
padding: 0.4rem;
}
main {
padding: 0.4rem;
}
footer {
grid-column: span 2;
padding: 30px;
padding-left: 0px;
text-align: right;
font-size: 0.8em;
vertical-align: middle;
}
h1 {
margin-bottom: 1em;
font-size: 1.3em;
font-weight: bold;
}
hr {
border: 0;
height: 0;
border-top: 1px solid var(--gray);
margin-bottom: 20px;
}
.payment {
max-width: 400px;
margin: 0 auto;
}
.payment-logo {
padding-top: 6vh;
width: 100%;
}
.float--right {
float: right !important;
padding: 0.8rem;
}
.float--left {
float: left !important;
padding: 0.8rem;
}
.prototypes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
grid-gap: 20px;
}
.prototype {
display: grid;
grid-template-columns: 100px 1fr;
grid-gap: 10px;
border: 0.01rem solid #fff;
background: #fff;
display: flex;
display: -ms-flexbox;
-ms-flex-direction: column;
flex-direction: column;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.prototype .prototype__body p {
padding: 0.8rem;
font-size: 1rem;
line-height: 1.4;
}
.prototype .prototype__footer {
padding: 0.8rem;
}
.prototype .prototype-header:last-child,
.prototype .prototype-body:last-child,
.prototype .prototype-footer:last-child {
padding-bottom: 0.8rem;
}
.prototype .prototype-image {
padding-top: 0.8rem;
}
.prototype .prototype-image:first-child {
padding-top: 0;
}
.prototype__artwork:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.prototype {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.prototype .prototype__body .prototype__title {
padding: 0.8rem;
padding-bottom: 0;
font-size: 1rem;
line-height: 1.4;
font-size: 1.2rem;
color: var(--secondary);
}
.prototype .prototype__body .prototype__price {
padding-top: 0.1rem;
width: 100%;
color: var(--primary-alpha);
}
.prototype .prototype__body .prototype__desc {
font-size: 0.8rem;
padding-bottom: 0.8rem;
}
.prototype__artwork {
width: 100%;
}
.prototype__edit {
width: 100%;
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.prototype__edit:hover {
cursor: pointer;
opacity: 0.4;
}
/* Button */
.btn {
-webkit-appearance: none;
-moz-appearance: none;
background: #fff;
border: 0.05rem solid var(--secondary);
border-radius: 6px;
color: var(--secondary);
cursor: pointer;
display: inline-block;
line-height: 1rem;
outline: none;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
font-size: 0.9rem;
height: 2rem;
padding: 0.45rem 0.6rem;
}
.btn:focus,
.btn:hover {
background: var(--gray);
border-color: var(--primary);
text-decoration: none;
}
.btn[disabled],
.btn:disabled,
.btn.disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}
.btn.btn--primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn.btn--primary:focus,
.btn.btn--primary:hover {
background: var(--secondary);
border-color: var(--secondary);
color: var(--primary);
}
.btn.btn--primary:active,
.btn.btn--primary.active {
background: var(--primary);
border-color: var(--primary);
color: var(--secondary);
}
.btn.btn--secondary {
background: var(--secondary);
border-color: var(--secondary);
color: var(--primary);
}
.btn.btn--secondary:focus,
.btn.btn--secondary:hover {
background: var(--primary);
border-color: var(--primary);
color: var(--gray);
}
.btn.btn--secondary:active,
.btn.btn--secondary.active {
background: var(--primary);
border-color: var(--primary);
color: var(--secondary);
}
.btn.btn--link {
background: transparent;
border-color: transparent;
color: var(--primary);
}
.btn.btn--link:focus,
.btn.btn--link:hover,
.btn.btn--link:active,
.btn.btn--link.active {
color: var(--secondary);
}
.btn.btn--clear {
background: transparent;
border: 0;
color: currentColor;
height: 0.8rem;
line-height: 0.8rem;
margin-left: 0.2rem;
margin-right: -2px;
opacity: 1;
padding: 0;
text-decoration: none;
width: 0.8rem;
}
.btn.btn--clear:hover {
color: #00ffb9;
opacity: 0.95;
}
.btn.btn--clear::before {
content: "\2715";
}
.btn .icon {
vertical-align: -10%;
}
/* Icon */
.icon {
box-sizing: border-box;
display: inline-block;
font-size: inherit;
font-style: normal;
height: 1em;
position: relative;
text-indent: -9999px;
vertical-align: middle;
width: 1em;
}
.icon::before,
.icon::after {
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.icon--forward::before {
border: 1px solid currentColor;
border-bottom: 0;
border-right: 0;
content: "";
height: 0.62em;
width: 0.62em;
}
.icon--forward::after {
background: currentColor;
content: "";
height: 13px;
width: 1px;
}
.icon--forward::after {
left: 46%;
}
.icon--forward::before {
transform: translate(-50%, -50%) rotate(135deg);
}
.icon--plus::before,
.icon--cross::before {
background: currentColor;
content: "";
height: 1px;
width: 100%;
}
.icon--plus::after,
.icon--cross::after {
background: currentColor;
content: "";
height: 100%;
width: 1px;
}
.icon--cross::before {
width: 100%;
}
.icon--cross::after {
height: 100%;
}
.icon--cross::before,
.icon--cross::after {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon--delete::before {
border: 1px solid currentColor;
border-bottom-left-radius: 0.1rem;
border-bottom-right-radius: 0.1rem;
border-top: 0;
content: "";
height: 0.65em;
top: 60%;
width: 0.75em;
}
.icon--delete::after {
background: currentColor;
box-shadow: -0.25em 0.2em, 0.25em 0.2em;
content: "";
height: 1px;
top: 0.05rem;
width: 0.55em;
}
/* Orders */
.order {
border: 0.05rem solid var(--gray);
border-radius: 6px;
display: flex;
display: -ms-flexbox;
-ms-flex-direction: column;
flex-direction: column;
background: #fff;
}
.order .total {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding: 0.8rem;
font-size: 1.1rem;
}
.order .order-nav {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.order .body {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
padding: 0.6rem 0.8rem;
padding-bottom: 0;
}
/* item */
.item {
align-content: space-between;
align-items: center;
display: flex;
display: -ms-flexbox;
-ms-flex-align: center;
-ms-flex-line-pack: justify;
}
.item .action {
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.item .action .price {
-webkit-appearance: none;
-moz-appearance: none;
background: #fff;
display: inline-block;
line-height: 1rem;
outline: none;
text-align: center;
text-decoration: none;
transition: all 0.2s ease;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
vertical-align: middle;
white-space: nowrap;
font-size: 0.9rem;
height: 2rem;
padding: 0.45rem 0.6rem;
background: transparent;
border-color: transparent;
color: var(--primary);
cursor: default;
opacity: 0.5;
pointer-events: none;
}
.item video {
margin: 0.4rem 0.4rem 0.4rem 0.2rem;
border-radius: 4px;
max-width: 30px;
}
.item .content {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}
.item .title {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-margin-before: 0px;
line-height: 1rem;
font-size: 0.9rem;
}
.item .sold-out {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-margin-before: 0px;
line-height: 1rem;
font-size: 0.9rem;
color: var(--red-color);
}
.item.item-centered .title,
.item.item-centered .item-subtitle {
margin-bottom: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* empty */
.empty {
background-color: #fff;
border: 0.05rem solid #fff;
color: var(--primary-alpha);
border-radius: 6px;
padding: 3.2rem 1.6rem;
text-align: center;
}
.empty .title,
.empty .subtitle {
margin: 0.8rem auto;
}
.empty .subtitle {
font-size: 0.9rem;
}
// App.js
import Footer from "./components/Footer";
import Header from "./components/Header";
import Orders from "./components/Orders";
import Prototypes from "./components/Prototypes";
function App() {
return (
<>
<Header />
<div className="container">
<Prototypes />
<Orders />
<Footer />
</div>
</>
);
}
export default App;
// components/Header.jsx
export default function Header() {
return (
<header>
<div className="header__container">
<div className="title">Awesome Prototypes in Shop</div>
<div className="subtitle">
Check out what other designers have created using ProtoPie—download
these examples to learn exactly how they made their interactions.
</div>
<div className="btn__area">
<a href="https://www.protopie.io" target="_BLANK" rel="noreferrer">
<button>Try ProtoPie Yourself</button>
</a>
</div>
</div>
</header>
);
}
// components/Footer.jsx
export default function Footer() {
return (
<footer>
<p>© 2021 Mark Lee. All rights reserved.</p>
</footer>
);
}
// components/Prototypes.jsx
export default function Prototypes() {
return (
<main>
<div className="prototypes">
상품 리스트
</div>
</main>
);
}
// components/Orders.jsx
export default function Orders() {
return (
<aside>
<div className="empty">
<div className="title">You don't have any orders</div>
<div className="subtitle">Click on a + to add an order</div>
</div>
</aside>
);
}
// components/Prototypes.jsx
const prototypes = [
{
id: "pp-01",
title: "Kids-story",
artist: "Thomas Buisson",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Kids-story_1.mp4",
price: 10,
pieUrl: "https://cloud.protopie.io/p/8a6461ad85",
},
{
id: "pp-02",
title: "mockyapp",
artist: "Ahmed Amr",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/mockyapp.mp4",
price: 20,
pieUrl: "https://cloud.protopie.io/p/27631ac9d5",
},
{
id: "pp-03",
title: "macOS Folder Concept",
artist: "Dominik Kandravý",
desc: "Folder concept prototype by Dominik Kandravý.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/macOS_Folder_Concept_-_Folder_concept.mp4",
price: 30,
pieUrl: "https://cloud.protopie.io/p/acde5ccdf9",
},
{
id: "pp-04",
title: "Translator",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Translator.mp4",
price: 40,
pieUrl: "https://cloud.protopie.io/p/b91edba11d",
},
{
id: "pp-05",
title: "In-car voice control",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/In-car_voice_control.mp4",
price: 50,
pieUrl: "https://cloud.protopie.io/p/6ec7e70d1a",
},
{
id: "pp-06",
title: "The Adventures of Proto",
artist: "Richard Oldfield",
desc: `Made exclusively for Protopie Playoff 2021
Shout up if you get stuck!
For the full experience. View in the Protopie App.
#PieDay #PlayOff #ProtoPie`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/The_Adventures_of_Proto.mp4",
price: 60,
pieUrl: "https://cloud.protopie.io/p/95ee13709f",
},
{
id: "pp-07",
title: "Sunglasses shop app",
artist: "Mustafa Alabdullah",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/sunglasses_shop_app.mp4",
price: 70,
pieUrl: "https://cloud.protopie.io/p/6f336cac8c",
},
{
id: "pp-08",
title: "Alwritey—Minimalist Text Editor",
artist: "Fredo Tan",
desc: `This minimalist text editor prototype was made with ProtoPie by Fredo Tan.
---
Inspired by Writty, a simple writing app by Carlos Yllobre. Try out Writty at https://writtyapp.com.
---
ProtoPie is an interactive prototyping tool for all digital products.
---
Learn more about ProtoPie at https://protopie.io.`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/minimalist-text-editor.mp4",
price: 80,
pieUrl: "https://cloud.protopie.io/p/946f88f8d3",
},
{
id: "pp-09",
title: "Voice search for TV",
artist: "Tony Kim",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/TV.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/60ee64cda0",
},
{
id: "pp-10",
title: "Finance App Visual Interaction 2.0",
artist: "Arpit Agrawal",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Credit_Card_App.mp4",
price: 90,
pieUrl:
"https://cloud.protopie.io/p/09ce2fdf84/21?ui=true&mockup=true&touchHint=true&scaleToFit=true&cursorType=touch",
},
{
id: "pp-11",
title: "Whack-a-mole",
artist: "Changmo Kang",
desc: "This prototype was made with ProtoPie, the interactive prototyping tool for all digital products.",
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Whack_a_mole.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/ab796f897e",
},
{
id: "pp-12",
title: "Voice Note",
artist: "Haerin Song",
desc: `Made by Haerin Song
(Soda Design)`,
thumbnail:
"https://prototype-shop.s3.ap-northeast-2.amazonaws.com/thumbnails/Voice_note_with_sound_wave.mp4",
price: 90,
pieUrl: "https://cloud.protopie.io/p/7a0d6567d2",
},
];
export default function Prototypes() {
return (
<main>
<div className="prototypes">
상품 리스트
</div>
</main>
);
}
// components/Prototypes.jsx
const prototypes = [...];
export default function Prototypes() {
return (
<main>
<div className="prototypes">
{prototypes.map((prototype) => {
const { id, thumbnail, title, price, desc, pieUrl } = prototype;
return (
<div className="prototype" key={id}>
<a href={pieUrl} target="_BLANK" rel="noreferrer">
<div
style={{
padding: "25px 0 33px 0",
}}
>
<video
autoPlay
loop
playsInline
className="prototype__artwork prototype__edit"
style={{
objectFit: "contain",
}}
src={thumbnail}
/>
</div>
</a>
<div className="prototype__body">
<div className="prototype__title">
<div className="btn btn--primary float--right">
<i className="icon icon--plus" />
</div>
{title}
</div>
<p className="prototype__price">$ {price}</p>
<p className="prototype__desc">{desc}</p>
</div>
</div>
);
})}
</div>
</main>
);
}
// contexts/AppStateContext.jsx
import React from "react";
const AppStateContext = React.createContext();
export default AppStateContext;
// contexts/AppStateProvider.jsx
import { useCallback, useState } from "react";
import AppStateContext from "../contexts/AppStateContext";
const AppStateProvider = ({ children }) => {
const [prototypes] = useState([...]);
const [orders, setOrders] = useState([]);
const addToOrder = useCallback((id) => {}, []);
const remove = useCallback((id) => {}, []);
const removeAll = useCallback(() => {}, []);
return (
<AppStateContext.Provider
value={{ prototypes, orders, addToOrder, remove, removeAll }}
>
{children}
</AppStateContext.Provider>
);
};
export default AppStateProvider;
// App.js
import Footer from "./components/Footer";
import Header from "./components/Header";
import Orders from "./components/Orders";
import Prototypes from "./components/Prototypes";
import AppStateProvider from "./providers/AppStateProvider";
function App() {
return (
<AppStateProvider>
<Header />
<div className="container">
<Prototypes />
<Orders />
<Footer />
</div>
</AppStateProvider>
);
}
export default App;
// hooks/usePrototypes.js
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";
export default function usePrototypes() {
const { prototypes } = useContext(AppStateContext);
return prototypes;
}
// hooks/useOrders.js
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";
export default function usePrototypes() {
const { orders } = useContext(AppStateContext);
return orders;
}
// hooks/useActions.js
import { useContext } from "react";
import AppStateContext from "../contexts/AppStateContext";
export default function useActions() {
const { addToOrder, remove, removeAll } = useContext(AppStateContext);
return { addToOrder, remove, removeAll };
}
// components/Prototypes.jsx
import usePrototypes from "../hooks/usePrototypes";
import useActions from "../hooks/useActions";
export default function Prototypes() {
const prototypes = usePrototypes();
const { addToOrder } = useActions();
return (
<main>
<div className="prototypes">
{prototypes.map((prototype) => {
const { id, thumbnail, title, price, desc, pieUrl } = prototype;
const click = () => {
addToOrder(id);
};
return (
<div className="prototype" key={id}>
<a href={pieUrl} target="_BLANK" rel="noreferrer">
<div
style={{
padding: "25px 0 33px 0",
}}
>
<video
autoPlay
loop
playsInline
className="prototype__artwork prototype__edit"
style={{
objectFit: "contain",
}}
src={thumbnail}
/>
</div>
</a>
<div className="prototype__body">
<div className="prototype__title">
<div
className="btn btn--primary float--right"
onClick={click}
>
<i className="icon icon--plus" />
</div>
{title}
</div>
<p className="prototype__price">$ {price}</p>
<p className="prototype__desc">{desc}</p>
</div>
</div>
);
})}
</div>
</main>
);
}
// providers/AppStateProvider.jsx
const addToOrder = useCallback((id) => {
setOrders((orders) => {
const finded = orders.find((order) => order.id === id);
if (finded === undefined) {
return [...orders, { id, quantity: 1 }];
} else {
return orders.map((order) => {
if (order.id === id) {
return {
id,
quantity: order.quantity + 1,
};
} else {
return order;
}
});
}
});
}, []);
// components/Orders.jsx
import { useMemo } from "react";
import useOrders from "../hooks/useOrders";
import usePrototypes from "../hooks/usePrototypes";
import useActions from "../hooks/useActions";
export default function Orders() {
const orders = useOrders();
const prototypes = usePrototypes();
const { remove, removeAll } = useActions();
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
if (orders.length === 0) {
return (
<aside>
<div className="empty">
<div className="title">You don't have any orders</div>
<div className="subtitle">Click on a + to add an order</div>
</div>
</aside>
);
}
return (
<aside>
<div className="order">
<div className="body">
{orders.map((order) => {
const { id } = order;
const prototype = prototypes.find((p) => p.id === id);
return (
<div className="item" key={id}>
<div className="img">
<video src={prototype.thumbnail} />
</div>
<div className="content">
<p className="title">
{prototype.title} x {order.quantity}
</p>
</div>
<div className="action">
<div className="price">
$ {prototype.price * order.quantity}
</div>
<button className="btn btn--link" onClick={() => remove(id)}>
<i className="icon icon--cross" />
</button>
</div>
</div>
);
})}
</div>
<div className="total">
<hr />
<div className="item">
<div className="content">Total</div>
<div className="action">
<div className="price">$ {totalPrice}</div>
</div>
<button className="btn btn--link" onClick={removeAll}>
<i className="icon icon--delete" />
</button>
</div>
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
>
Checkout
</button>
</div>
</div>
</aside>
);
}
// components/Orders.jsx
import { useMemo } from "react";
import useOrders from "../hooks/useOrders";
import usePrototypes from "../hooks/usePrototypes";
import useActions from "../hooks/useActions";
export default function Orders() {
const orders = useOrders();
const prototypes = usePrototypes();
const { remove, removeAll } = useActions();
const totalPrice = useMemo(() => {
return orders
.map((order) => {
const { id, quantity } = order;
const prototype = prototypes.find((p) => p.id === id);
return prototype.price * quantity;
})
.reduce((l, r) => l + r, 0);
}, [orders, prototypes]);
if (orders.length === 0) {
return (
<aside>
<div className="empty">
<div className="title">You don't have any orders</div>
<div className="subtitle">Click on a + to add an order</div>
</div>
</aside>
);
}
return (
<aside>
<div className="order">
<div className="body">
{orders.map((order) => {
const { id } = order;
const prototype = prototypes.find((p) => p.id === id);
const click = () => {
remove(id);
};
return (
<div className="item" key={id}>
<div className="img">
<video src={prototype.thumbnail} />
</div>
<div className="content">
<p className="title">
{prototype.title} x {order.quantity}
</p>
</div>
<div className="action">
<div className="price">
$ {prototype.price * order.quantity}
</div>
<button className="btn btn--link" onClick={click}>
<i className="icon icon--cross" />
</button>
</div>
</div>
);
})}
</div>
<div className="total">
<hr />
<div className="item">
<div className="content">Total</div>
<div className="action">
<div className="price">$ {totalPrice}</div>
</div>
<button className="btn btn--link" onClick={removeAll}>
<i className="icon icon--delete" />
</button>
</div>
<button
className="btn btn--secondary"
style={{ width: "100%", marginTop: 10 }}
>
Checkout
</button>
</div>
</div>
</aside>
);
}
// providers/AppStateProvider.jsx
const remove = useCallback((id) => {
setOrders((orders) => {
return orders.filter((order) => order.id !== id);
});
}, []);
const removeAll = useCallback(() => {
setOrders([]);
}, []);