React 2021

  • https://slides.com/woongjae/react2021
  • https://github.com/xid-mark/what-is-react
  • https://github.com/xid-mark/tic-tac-toe
  • https://github.com/xid-mark/react2021

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

1. React Getting Started

1-1) React Concept

1-2) 개발 환경 체크

1-3) React 라이브러리

React Concept

Keyword

  • 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

Component ??

<!-- HTMLElement -->

<img src="이미지 주소"/>
  
<button class="클래스 이름">버튼</button>

<!-- 내가 만든 컴포넌트 -->

<내가지은이름1 name="Mark" />

<내가지은이름 prop={false}>내용</내가지은이름>

<!--

- src, class, name, props 밖에서 넣어주는 데이터
- 문서(HTML), 스타일(CSS), 동작(JS) 를 합쳐서 내가 만든 일종의 태그

-->

Component Based Development - Version

Component Based Development - Links

Component Based Development - Title

Component Based Development - Content

Component Based Development - Card

Component Based Development - Card

Component Based Development - Card

Component Tree => DOM Tree

Why Virtual DOM ?

  • DOM 을 직접 제어하는 경우

    • 바뀐 부분만 정확히 바꿔야 한다.

  • DOM 을 직접 제어하지 않는 경우

    • 가상의 돔 트리를 사용해서,

    • 이전 상태와 이후 상태를 비교하여,

    • 바뀐 부분을 찾아내서 자동으로 바꾼다.

Virtual DOM - diff 로 변경

React Client Side Rendering

React Server Side Rendering

CSR vs SSR

  • CSR

    • JS 가 전부 다운로드 되어 리액트 애플리케이션이 정상 실행되기 전까지는
      화면이 보이지 않음.

    • JS 가 전부 다운로드 되어 리액트 애플리케이션이 정상 실행된 후,
      화면이 보이면서 유저가 인터렉션 가능

  • SSR

    • JS 가 전부 다운로드 되지 않아도,
      일단 화면은 보이지만 유저가 사용 할 수 없음.

    • JS 가 전부 다운로드 되어 리액트 애플리케이션이 정상 실행된 후,
      유저가 사용 가능

개발 환경 체크

  • Node.js

    • ​Installer
    • nvm
  • Browser (Chrome)

  • Git

  • VSCode

nvm install 14.16.1

nvm use 14.16.1

nvm alias default 14.16.1

React 라이브러리

리액트가 하는 일

리액트의 핵심 모듈 2개로 리액트가 하는 일 알아보기

// 1. 리액트 컴포넌트 => HTMLElement 연결하기
import ReactDOM from 'react-dom';

// 2. 리액트 컴포넌트 만들기
import React from 'react';

< HTMLElement >

{ React 컴포넌트 } - JS, JSX

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')
);

< HTMLElement >

{ React 컴포넌트 } - JS, JSX

"만들어진 리액트 컴포넌트"

실제 HTMLElement 에 연결할 때

ReactDOM 라이브러리를

이용합니다.

{ React 컴포넌트 } 만들기

리액트 컴포넌트를 만들 때 사용하는 API 모음

Use React, ReactDOM Library with CDN

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>

React 프론트엔드

컴포넌트를 정의하고,

실제 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. React Component

 

2-1) React.createElement 로 컴포넌트 만들기    2-2) JSX

2-3) React Component 만드는 법    2-4) Props 와 State

2-5) Event Handling    2-6) Component Lifecycle

React.createElement 로

컴포넌트 만들기

React.createElement

순수 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

JSX

JSX 문법으로 작성된 코드는 순수한 JavaScript 로 컴파일 하여 사용한다.

누가 해주나요?? => babel

JSX

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>

왜 JSX 을 쓰나요??

  • React.createElement VS JSX
    • 가독성 완승
  • babel 과 같은 컴파일 과정에서 문법적 오류를 인지하기 쉬움

JSX 문법

  • 최상위 요소가 하나여야 합니다.
  • 최상위 요소 리턴하는 경우, ( ) 로 감싸야 합니다.
  • 자식들을 바로 랜더링하고 싶으면, <>자식들</> 를 사용합니다. => Fragment
  • 자바스크립트 표현식을 사용하려면, {표현식} 를 이용합니다.
  • if 문은 사용할 수 없습니다.
    • 삼항 연산자 혹은 && 를 사용합니다.
  • style 을 이용해 인라인 스타일링이 가능합니다.
  • class 대신 className 을 사용해 class 를 적용할 수 있습니다.
  • 자식요소가 있으면, 꼭 닫아야 하고, 자식요소가 없으면 열면서 닫아야 합니다.
    • <p>어쩌구</p>
    • <br />

JSX 문법

<!-- 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>

React Component 만드는 법

Hooks 이전

  • 컴포넌트 내부에 상태가 있다면 ?

    • class

  • 컴포넌트 내부에 상태가 없다면 ?

    • ​라이프사이클을 사용해야 한다면 ?

      • class

    • ​라이프사이클에 관계 없다면 ?

      • ​function

Hooks 이후

  • class

  • function

Class 컴포넌트

import React from 'react';

// 정의
class ClassComponent extends React.Component {
  render() {
    return (<div>Hello</div>);
  }
}

// 사용
<ClassComponent />

Function 컴포넌트

import React from 'react';

// 정의 1
function FunctionComponent() {  
  return <div>Hello</div>;
}

// 정의 2
const FunctionComponent = () => <div>Hello</div>;

// 사용
<FunctionComponent />

Props 와 State

Props 와 State

Props 는 컴포넌트 외부에서 컴포넌트에게 주는 데이터입니다.

State 는 컴포넌트 내부에서 변경할 수 있는 데이터입니다.

둘 다 변경이 발생하면, 랜더가 다시 일어날 수 있습니다.

Component

Props

State

Render 함수

Props 와 State 를 바탕으로 컴포넌트를 그립니다.

그리고 Props 와 State 가 변경되면, 컴포넌트를 다시 그립니다.

컴포넌트를 그리는 방법을 기술하는 함수가 랜더 함수 입니다.

Component

Props

State

Component

Props

State

Props

State

function 컴포넌트(props) { retutn JSX; }

<!-- 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>

<Component p="프롭스" />

props 설정

function Component(props) {
  return (
    <div>
      <h1>{props.message} 이것은 함수로 만든 컴포넌트 입니다.</h1>
    </div>
  );
}

props.p 로 접근

props 사용

class 컴포넌트 extends React.Component

<!-- 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>

<Component p="프롭스" />

props 설정

class Component extends React.Component {
  render() {
    return (
      <div>{this.props.p}</div>
    );
  }
}

this.props.p 로 접근

props 사용

defaultProps

<!-- 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 = {}; constructor

state 초기값 설정

class Component extends React.Component {
  state = {
    s: '스테이트'
  };
  render() {
    return (
      <div>{this.state.s}</div>
    );
  }
}

this.state.s 로 접근

state 사용

class Component extends React.Component {
  constructor(props) {
    super(props);
    this.state = {s: '스테이트'};
  }
  render() {
    return (
      <div>{this.state.s}</div>
    );
  }
}

this.setState({s: '새 스테이트'});

state 값 업데이트

class Component extends React.Component {
  state = {
    s: '스테이트'
  };
  render() {
    return (
      <div onClick={() => {
        this.setState({s: '새 스테이트'});
      }}>{this.state.s}</div>
    );
  }
}

Event Handling

Event Handling

  • HTML DOM 에 클릭하면 이벤트가 발생하고, 발생하면 그에 맞는 변경이 일어나도록 해야합니다.
  • JSX 에 이벤트를 설정할 수 있습니다.
class Comp extends React.Component {
  render() {
    return (
      <div>
        <button onClick={() => {
          console.log('clicked');
        }}>클릭</button>
      </div>
    );
  }
}

Event Handling

  • camelCase 로만 사용할 수 있습니다.
    • onClick, onMouseEnter
  • 이벤트에 연결된 자바스크립트 코드는 함수입니다.
    • 이벤트={함수} 와 같이 씁니다.
  • 실제 DOM 요소들에만 사용 가능합니다.
    • 리액트 컴포넌트에 사용하면, 그냥 props 로 전달합니다.
<!-- 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>

Component Lifecycle

리액트 컴포넌트는 탄생부터 죽음까지

여러지점에서 개발자가 작업이 가능하도록

메서드를 오버라이딩 할 수 있게 해준다.

Declarative 디클레러티브

Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.

Component 생성 및 마운트 (< v16.3)

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

Component props, state 변경 (< v16.3)

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

componentWillReceiveProps

  • props 를 새로 지정했을 때 바로 호출됩니다.

  • 여기는 state 의 변경에 반응하지 않습니다.

    • 여기서 props 의 값에 따라 state 를 변경해야 한다면,

      • setState 를 이용해 state 를 변경합니다.

      • 그러면 다음 이벤트로 각각 가는것이 아니라 한번에 변경됩니다.

shouldComponentUpdate

  • props 만 변경되어도

  • state 만 변경되어도

  • props & state 둘다 변경되어도

  • newProps 와 new State 를 인자로 해서 호출

  • return type 이 boolean 입니다.

    • true 면 render

    • false 면 render 가 호출되지 않습니다.

    • 이 함수를 구현하지 않으면, 디폴트는 true

componentWillUpdate

  • 컴포넌트가 재 랜더링 되기 직전에 불립니다.

  • 여기선 setState 같은 것을 쓰면 아니됩니다.

componentDidUpdate

  • 컴포넌트가 재 랜더링을 마치면 불립니다.

Component 언마운트 (< v16.3)

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

Component 라이프사이클 변경 (v16.3)

constructor

componentWillMount => getDerivedStateFromProps

render

componentDidMount

 

componentWillReceiveProps => getDerivedStateFromProps

shouldComponentUpdate

render

componentWillUpdate => getSnapshotBeforeUpdate

(dom 에  적용)

componentDidUpdate

 

componentWillUnmount

Component 생성 및 마운트 (v16.3)

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

Component props, state 변경 (v16.3)

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

Component 언마운트 (v16.3)

componentWillUnmount

Component 에러 캐치

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. Creating React Project

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) 리액트 컴포넌트 디버깅

Create React App

https://create-react-app.dev

npx create-react-app tic-tac-toe

npx

npm 5.2.0 이상부터 함께 설치된 커맨드라인 명령어

이런 곳에 있습니다.

왜 npx 가 필요했을까요??

  • 프로젝트의 로컬에 설치된 패키지의 실행 커맨드를 사용하려면,
    • package.jsonnpm scripts 에 명령어를 추가하여 사용해야 했다.
    • npx 로 바로 실행 가능
  • 전역으로 실행하고 싶은 패키지가 있을 경우,
    • npm i -g 를 이용하여, 전역에 꼭 설치해서 사용해야 가능했다.
    • npx 로 최신 버전의 패키지를 받아 바로 실행 가능 

npx create-react-app tic-tac-toe

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"
    ]
  }
}

package.json

  • 리액트 핵심 모듈

    • "react": "^16.13.1"

    • "react-dom": "^16.13.1"

  • cra 를 사용하는데 필요한 모듈

    • "react-scripts": "3.4.1"

  • ​Test 를 도와주는 유틸 모듈

    • "@testing-library/jest-dom": "^4.2.4"

    • "@testing-library/react": "^9.5.0"

    • "@testing-library/user-event": "^7.2.1"

  • npm start

    • react-scripts start
    • Starting the development server...
  • npm run build

    • react-scripts build
    • Creating an optimized production build...​
  • ​npm test

    • ​react-scripts test
    • Jest 를 통해 test code 를 실행합니다.
  • ​npm run eject

    • ​react-scripts eject

개발 용 서버를 띄웁니다.

소스 코드가 수정되었을 때,

다시 컴파일 하고 웹페이지를 새로고침 합니다.

Project 폴더 바로 아래

build 라는 폴더가 만들어지고,

그 안에 Production 배포를 위한

파일들이 생성됩니다.

  • serve 라는 패키지를 전역으로 설치합니다.

  • serve 명령어를 -s 옵션으로 build 폴더를 지정하여 실행합니다.

    • ​-s 옵션은 어떤 라우팅으로 요청해도 index.html 을 응답하도록 합니다.
npm install serve -g
serve -s build
  • __tests__ 폴더 안의 .js 파일
  • .test.js 로 끝나는 파일
  • .spec.js 로 끝나는 파일

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"
    ]
  }
}
  • react-scripts 는 사라집니다.
  • 드러내지 않고 cra 에 의해 사용되던
    각종 패키지가 package.json 에 나타납니다.
  • Jest, Babel, ESLint 설정이 추가됩니다.
  • 각종 설정 파일이 config 폴더에 생성됩니다.

webpack

파일 확장자에 맞는 loader 에게 위임

babel-loader

js

jsx

css

.

.

.

css-loader

최종 배포용 파일

babel config

어떤 문법을 번역할건지 설정

ESLint

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

Prettier

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 *

Command + Shift + P

>Format Document

"editor.formatOnSave": true

Prettier 에서 불필요하거나, Prettier 와 충돌할 수 있는 모든 규칙을 끕니다.
이 구성은 규칙을 끄기만 하기 때문에 다른 설정과 함께 사용하는 것이 좋습니다.

{
  ...
  "eslintConfig": {
    "extends": [
      "react-app",
      "prettier"
    ]
  },
  ...
}

husky

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"

lint-staged

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": {}
}

Create React App 시작 코드 이해하기

React Developer Tools

개발 모드

4. React Router

 

4-1) React 의 라우팅 이해하기    4-2) Dynamic 라우팅

4-3) Switch 와 NotFound    4-4) JSX 링크로 라우팅 이동하기

4-5) JS 로 라우팅 이동하기    4-6) Redirect

React 의 라우팅 이해하기

react-router-dom

/

/profile

/about

Client (Browser)

Server

Single Page Application

/

/profile

/about

Client (Browser)

SPA 라우팅 과정

  1. 브라우저에서 최초에 '/' 경로로 요청을 하면,

  2. React Web App 을 내려줍니다.

  3. 내려받은 React App 에서 '/' 경로에 맞는 컴포넌트를 보여줍니다.

  4. React App 에서 다른 페이지로 이동하는 동작을 수행하면,

  5. 새로운 경로에 맞는 컴포넌트를 보여줍니다.

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

   /

   /profile

   /about

// 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 를 보여줍니다.

/

  • /

/profile

  • /
  • /profile

/about

  • /
  • /about
<Route path="/" exact component={Home} />

Dynamic 라우팅

/profile/1

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

/profile/1

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'

/about?name=mark

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

/about?name=mark

// 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

/about?name=mark

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

npm i query-string -S

/about?name=mark

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

Switch 와 NotFound

Switch

  • 여러 Route 중 순서대로 먼저 맞는 하나만 보여줍니다.

  • exact 를 뺄 수 있는 로직을 만들 수 있습니다.

  • 가장 마지막에 어디 path 에도 맞지 않으면 보여지는 컴포넌트를 설정해서,
    "Not Found" 페이지를 만들 수 있습니다.

Switch

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;

Switch

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;

NotFound

export default function NotFound() {
  return <div>페이지를 찾을 수 없습니다.</div>;
}

JSX 링크로 라우팅 이동하기

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

JS 로 라우팅 이동하기

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("/");

/login

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

<Route component={컴포넌트} />

  • history
  • location
  • match

withRouter()

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

withRouter()

export default withRouter(LoginButton);

Redirect

<Redirect />

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. React Component Styling

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

Style Loaders

webpack

파일 확장자에 맞는 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,
            },

CSS (webpack.config.js)

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,
              }),
            },

CSS Module (webpack.config.js)

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

Sass (webpack.config.js)

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'
              ),
            },

Sass Module (webpack.config.js)

import styles from './App.module.scss';
import styles from './App.module.sass';

CSS, 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.css

.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.css

.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.scss

.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

CSS module

SASS module

import styles from './App.module.css';

console.log(styles);

[filename]\_[classname]\_\_[hash]

.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);

[filename]\_[classname]\_\_[hash]

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

styles[클래스]

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;

components

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

Button.jsx

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;
}
  • 조건부 className
  • 여러 className
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 classNames from 'classnames';

Button.jsx

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 classNames from 'classnames/bind';

Button.jsx

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

Styled Components

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;

styled.<태그>`스타일`

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;

styled.<태그>`스타일`

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;

${props => css`스타일`}

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;

styled(StyledButton)

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;

as="태그"

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;

as={컴포넌트}

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;

styled(컴포넌트)

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;

styled('태그') = styled.태그

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;

${props => props.color || ''}

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;

:hover {스타일}

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;

::before {스타일}

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;

&:hover {스타일}

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;

createGlobalStyle`스타일`

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;

createGlobalStyle`스타일`

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;

styled.태그.attr(props => ({속성들}))

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;

keyframes`키프레임`

import StyledButton from './components/StyledButton';

function App() {
  return (
    <div className="App">
      <p>
        <StyledButton>Slide Button</StyledButton>
      </p>
    </div>
  );
}

export default App;

React Shadow

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;

Ant Design

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

modularized 1

{
  ...
  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      [
        "import",
        {
          "libraryName": "antd",
          "libraryDirectory": "es",
          "style": "css"
        }
      ]
    ]
  },
  ...
}
npm run eject
npm install babel-plugin-import --save-dev

modularized 2

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;

modularized 2

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 { Button } from 'antd';

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 { Icon } from 'antd';

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

<Col span={24 중에 어느정도 차지할 지 정수} />

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

<Row gutter={16 + 8n 의 정수} />

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

<Col offset={24 중 건너띄고 싶은 정수} />

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

import { Row, Col } from 'antd';

<Row type="flex" justify="좌우정렬" align="위아래정렬" />

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

import { Layout } from 'antd';

6. 리액트 실전 활용법

6-1) High Order Component

6-2) Controlled Component 와 Uncontrolled Component

6-3) http 요청하기

HOC

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 새로운 컴포넌트; }

HOC

<컴포넌트> 를 인자로 받아

<새로운 컴포넌트> 를 리턴하는

함수

컴포넌트

props

UI

HOC

컴포넌트

새로운

컴포넌트

you already know HOC !

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

withRouter()

보통 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

withRouter(LoginButton)

주의할 점

  • 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';

Controlled Component

Uncontrolled Component

npx create-react-app controlled-uncontrolled-example

상태를 가지고 있는 엘리먼트

  • input

  • select

  • textarea

  • ...

엘리먼트의 '상태' 를 누가 관리하느냐

  • 엘리먼트를 가지고 있는 컴포넌트가 관리

    • Controlled

  • 엘리먼트의 상태를 관리하지 않고, 엘리먼트의 참조만 컴포넌트가 소유

    • Uncontrolled

import React from 'react';

export default class Controlled extends React.Component {
  state = { value: '' };

  render() {
    return (
      <div>
        <input />
      </div>
    );
  }
}

components/Controlled.jsx (1)

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

components/Controlled.jsx (2)

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

components/Controlled.jsx (3)

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

components/Controlled.jsx (3)

import React from 'react';

export default class Uncontrolled extends React.Component {
  _input = React.createRef();

  render() {
    return (
      <div>
        <input ret={this._input} />
      </div>
    );
  }
}

components/Uncontrolled.jsx (1)

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

components/Uncontrolled.jsx (2)

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;

components/Uncontrolled.jsx (2)

http 요청하기

// 책 목록보기

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}` },
);

7. Hooks & Context

1) Basic Hooks    2) Custom Hooks

3) Additional Hooks    4) React Router Hooks

5) 컴포넌트 간 통신    6) Context API

Basic Hooks

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

this.setState

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 [count, setCount] = useState(0);

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 [state, setState] = useState({count: 0});

const [스테이트 값, 스테이트 변경 함수] = useState(스테이트 초기값);

Functional Component

= Stateless Component

= Stateless Functional Component

Functional Component

!= Stateless Component

 

because state hook

  • 컴포넌트 사이에서 상태와 관련된 로직을 재사용하기 어렵습니다.

    • ​컨테이너 방식 말고, 상태와 관련된 로직

  • 복잡한 컴포넌트들은 이해하기 어렵습니다.

  • Class 는 사람과 기계를 혼동시킵니다.

    • ​컴파일 단계에서 코드를 최적화하기 어렵게 만든다.

  • ​this.state 는 로직에서 레퍼런스를 공유하기 때문에 문제가 발생할 수 있다.

    • ​좋은 것일까 ?

  • useState

    • state 를 대체 할 수 있다.

  • useEffect

    • 라이프 사이클 훅을 대체 할 수 있다.

      • componentDidMount

      • componentDidUpdate

      • componentWillUnmount

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

componentDidMount, componentDidUpdate

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;

useEffect(() => {});

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

componentWillUnmount

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;

useEffect(() => {return () => {}, [])

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

useEffect 자세히 알아보기

첫 번째 랜더링 (1)

from 리액트

"컴포넌트야,

state가 0 일 때의 UI를 보여줘."

첫 번째 랜더링 (2)

from 컴포넌트

  • "여기 랜더링 결과물로 <p>You clicked 0 times</p> 가 있어."
  • "그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마."
    () => { document.title = 'You clicked 0 times' }

첫 번째 랜더링 (3)

from 리액트

"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."

첫 번째 랜더링 (4)

from 브라우저

"좋아, 화면에 그려줄게."

첫 번째 랜더링 (5)

리액트

좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.

() => { document.title = 'You clicked 0 times' } 를 실행.

클릭 후, 랜더링 (1)

from 컴포넌트

"이봐 리액트, 내 상태를 1 로 변경해줘."

클릭 후, 랜더링 (2)

from 리액트

"상태가 1 일때의 UI를 줘."

클릭 후, 랜더링 (3)

from 컴포넌트

  • "여기 랜더링 결과물로 <p>You clicked 1 times</p> 가 있어."

  • "그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마."
    () => { document.title = 'You clicked 1 times' }.

클릭 후, 랜더링 (4)

from 리액트

"좋아. UI를 업데이트 하겠어.
이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해."

 

클릭 후, 랜더링 (5)

from 브라우저

"좋아, 화면에 그려줄게."

클릭 후, 랜더링 (6)

리액트

좋아, 이제 컴포넌트 컴포넌트가 준 이펙트를 실행할거야.

() => { document.title = 'You clicked 1 times' } 를 실행.

Custom Hooks

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

나만의 훅 만들기

useHasMounted vs withHasMounted

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

withHasMounted (HOC)

// hooks/useHasMounted.js

import { useState, useEffect } from 'react';

export default function useHasMounted() {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  }, []);

  return hasMounted;
}

useHasMounted (Custom Hook)

Additional Hooks

useReducer

useCallback, useMemo

useRef, useImperativeHandle,

useLayoutEffect

useDebugValue

useReducer

  • 다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우

  • 다음 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 [state, dispatch] = useReducer(reducer, initialState, init?)

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;

const 디펜던시 변경 없으면 고정 = useMemo(함수, 디펜던시)

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;

함수가 매번 새로 생성되는 경우, 최적화의 어려움 (feat. React.memo)

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;

useCallback

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;

createRef vs useRef

React Router Hooks

컴포넌트 간 통신

npx create-react-app component-communication

하위 컴포넌트를 변경하기

A 의 button 를 클릭하여 E 를 변경하려면

  1. <A /> 컴포넌트에서 button 에 onClick 이벤트를 만들고,

  2. button 을 클릭하면, <A /> 의 state 를 변경하여, <B /> 로 내려주는 props 를 변경

  3. <B /> 의 props 가 변경되면, <C /> 의 props 에 전달

  4. <C /> 의 props 가 변경되면, <D /> 의 props 로 전달

  5. <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 의 button 를 클릭하여 E 를 변경하려면

상위 컴포넌트를 변경하기

  1. <A /> 에 함수를 만들고, 그 함수 안에 state 를 변경하도록 구현, 그 변경으로 인해 p 안의 내용을 변경.

  2. 만들어진 함수를 props 에 넣어서, <B /> 로 전달

  3. <B /> 의 props 의 함수를 <C /> 의 props 로 전달

  4. <C /> 의 props 의 함수를 <D /> 의 props 로 전달

  5. <D /> 의 Props 의 함수를 <E /> 의 props 로 전달, <E /> 에서 클릭하면 props 로 받은 함수를 실행

E 의 button 를 클릭하여 A 의 p 를 변경하려면

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

E 의 button 를 클릭하여 A 의 p 를 변경하려면

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

Context API

useContext

npx create-react-app react-context-example

하위 컴포넌트 전체에 데이터를 공유하는 법

  • 데이터를 Set 하는 놈

    • 가장 상위 컴포넌트 => 프로바이더

  • 데이터를 Get 하는 놈

    • 모든 하위 컴포넌트에서 접근 가능

      • ​컨슈머로 하는 방법

      • 클래스 컴포넌트의 this.context 로 하는 방법

      • 펑셔널 컴포넌트의 useContext 로 하는 방법

데이터를 Set 하기

  1. 일단 컨텍스트를 생성한다.

  2. 컨텍스트.프로바이더 를 사용한다.

  3. ​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();

데이터를 Get 하기 (1) - Consumer

  1. 컨텍스트를 가져온다.

  2. 컨텍스트.컨슈머를 사용한다.

  3. ​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>
  );
}

데이터를 Get 하기 (2) - class

  1. static contextType 에 컨텍스트를 설정한다.

  2. 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>
  );
}

데이터를 Get 하기 (3) - functional

  1. useContext 로 컨텍스트를 인자로 호출한다.

  2. 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>
  );
}

8. React Testing

1) JavaScript Unit Test && Jest 사용하기

2) React Component Test

3) testing-library/react 활용하기

JavaScript Unit Test

Unit Test

  • 사람을 믿으시겠습니까 ? 테스트 코드를 믿으시겠습니까 ?

    • 실제로는 사람이 아니라, 사람의 감 입니다.

    • 코드는 거짓말을 하지 않습니다.

  • 통합테스트에 비해 빠르고 쉽습니다.

  • 통합테스트를 진행하기 전에 문제를 찾아낼 수 있습니다.

    • 그렇다고, 통합테스트가 성공하리란 보장은 없습니다.

  • 테스트 코드가 살아있는(동작을 설명하는) 명세가 됩니다.

    • 테스트를 읽고 어떻게 동작하는지도 예측 가능합니다.

  • 소프트웨어 장인이 되려면 TDD 해야죠..

    • 선 코딩 후, (몰아서) 단위테스트가 아니라

facebook/jest

  •  리액트의 영향이 크겠지만 가장 핫한 테스트 도구

  • 👩🏻‍💻 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"
  }
}

package.json

test('adds 1 + 2 to equal 3', () => {
  expect(1 + 2).toBe(3);
});

example.test.js

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

it (= test), describe, expect

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

.to~

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

.not.to~

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

async test with done callback

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

async test with promise

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

async test with async-await

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

React Component Test

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

App.test.js

어떤 파일을 테스트 해주나요?

  • __tests__ 폴더 안의 .js 파일

  • .test.js 로 끝나는 파일

  • .spec.js 로 끝나는 파일

testing-library/react 활용하기

Button 컴포넌트

  • 컴포넌트가 정상적으로 생성된다.
  • "button" 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다.
  • 버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다." 라고 쓰여진다.
  • 버튼을 클릭하기 전에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.
  • 버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.
  • 버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.

컴포넌트가 정상적으로 생성된다.

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

"button" 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다.

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;

버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다." 라고 쓰여진다.

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;

버튼을 클릭하기 전에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.

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;

버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.

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

버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.

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;

버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.

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;

버튼을 클릭하면, 5초 동안 버튼이 비활성화 된다.

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

9. React Advanced

1) Optimizing Performance

2) React.createPortal

3) React.forwardRef

Optimizing Performance

필요할 때만 랜더한다.

Reconciliation

  • 랜더 전후의 일치 여부를 판단하는 규칙

  • 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

  • 개발자가 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" />;
  }
}

DOM 엘리먼트의 타입이 같은 경우 (1)

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" }} />;
  }
}

DOM 엘리먼트의 타입이 같은 경우 (2)

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

자식에 대한 재귀적 처리 (1)

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

자식에 대한 재귀적 처리 (2)

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

자식에 대한 재귀적 처리 (3)

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

setState 와 함께 일어나는 일

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

shouldComponentUpdate

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

PureComponent

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

onClick={() => {}}

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

React.memo

const Person = React.memo(props => {
  console.log("Person render");

  const { name, age } = props;
  return (
    <ul>
      {name} / {age}
    </ul>
  );
});

React.createPortal

public/index.html

<!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>

src/index.css

body {...}

#modal {
  position: absolute;
  top: 0;
  left: 0;
}

src/components/Modal.jsx

import ReactDOM from 'react-dom';

const Modal = ({ children }) =>
  ReactDOM.createPortal(children, document.querySelector('#modal'));

export default Modal;

src/pages/Home.jsx

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

React.forwardRef

10. Deploy React App

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 프로젝트 배포 이해하기

SPA Deploy

git clone https://github.com/xid-mark/tic-tac-toe.git

cd tic-tac-toe

npm ci

npm run build

npm run build (in create-react-app Project)

  • npm run build

    • production 모드로 빌드되어, 'build' 폴더에 파일 생성

      • ​이렇게 만들어진 파일들을 웹서버를 통해 사용자가 접근할 수 있도록 처리​

    • build/static 폴더 안에 JS, CSS 파일들이 생성

{Project} 폴더 바로 아래

build 라는 폴더가 만들어지고,

그 안에 Production 배포를 위한

파일들이 생성됩니다.

SPA Deploy 의 특징

  • 모든 요청을 서버에 하고 받아오는 형태가 아님

  • 라우팅 경로에 상관없이 리액트 앱을 받아 실행

  • 라우팅은 받아온 리액트 앱을 실행 후, 적용

  • static 파일을 제외한 모든 요청을 index.html 로 응답해 주도록 작업

serve 패키지로

React Wep App 배포하기

  • serve 라는 패키지를 전역으로 설치합니다.

  • serve 명령어를 -s 옵션으로 build 폴더를 지정하여 실행합니다.

    • ​-s 옵션은 어떤 라우팅으로 요청해도 index.html 을 응답하도록 합니다.
npm install serve -g
serve -s build

AWS S3 에

React Wep App 배포하기

Amazon S3 정적 웹 사이트 호스팅

버킷 생성

정적 웹사이트 호스팅 설정

퍼블릭 액세스 차단 해제

버킷 정책 설정

버킷 정책 설정

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
	  "Principal": "*",
      "Action": [
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::react-camp/*"
      ]
    }
  ]
}

build => s3

s3 static webhosting endpoint

NginX 로

React Wep App 배포하기

Ubuntu 에 NginX 최신 버전 설치하기

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

NginX 설치 완료

sudo service nginx start

/etc/nginx/conf.d/default.conf

server {
    listen       80;
    server_name  localhost;

    root   /home/ubuntu/tic-tac-toe/build;
    index  index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

ec2 endpoint

node.js express 로

React Wep App 배포하기

npm i express

tic-tac-toe/server.js

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

node server.js - http://127.0.0.1:9000

서버사이드 렌더링 이해하기

Server Side Rendering

  • 서버에서 응답을 가져올때, 기존 처럼 static file 만을 가져오는 것이 아니고, 먼저 서버에서 응답 값을 만들어서 내려주고, 그 후에 static file 을 내려줍니다.​

  • static file 을 다 내려받고, 리액트 앱을 브라우저에서 실행한 뒤에는 SPA 처럼 동작하게 됩니다.

React Server Side Rendering

  • React Component 를 브라우저가 아니라 Node.js 에서 사용

  • ReactDOMServer.renderToString(<App />);

    • 결과가 문자열

    • 이것을 응답으로 내려준다.

  • 라우팅, 리덕스 와 같은 처리를 서버에서 진행하고 내려준다.

    • 복잡, 어렵

  • ​JSX 가 포함된 리액트 코드를 서버에서 읽을 수 있도록 babel 설정을 해야 한다.

11. React 로 만드는 쇼핑몰 프로젝트

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([]);
}, []);

React 2021

By Woongjae Lee

React 2021

리액트 2021

  • 7,843