๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ’ฌ/ใ…ใ……ใ…Œใ…‹ใ…ใ…… ์ฑŒ๋ฆฐ์ง€

39์ผ์ฐจ

๐Ÿ™

39์ผ์ฐจ

 

Part 10. React ๋กœ ์‡ผํ•‘๋ชฐ ๋งŒ๋“ค๊ธฐ (React ๊ธฐ๋ณธ)

Ch 8. React Testing

Ch 9. React Advanced

 


 

 

Ch 8. React Testing

 

 

 ๐Ÿ”— JavaScript Unit Test & Jest ์‚ฌ์šฉํ•˜๊ธฐ 

 

https://jestjs.io/docs/expect

 

Expect · Jest

When you're writing tests, you often need to check that values meet certain conditions. expect gives you access to a number of "matchers" that let you validate different things.

jestjs.io

https://github.com/testing-library/jest-dom

 

GitHub - testing-library/jest-dom: Custom jest matchers to test the state of the DOM

:owl: Custom jest matchers to test the state of the DOM - GitHub - testing-library/jest-dom: Custom jest matchers to test the state of the DOM

github.com

 

$ npm i jest -D
$ npm test # test ์‹คํ–‰
$ npx jest --watchAll # test ์ž๋™ ์‹คํ–‰
// pakeage.json

{
  "scripts": {
    "test": "jset"
  },
  ...
// ./example.test.js

describe("expect test", () => {
  it("37 to equal 37", () => {
    expect(37).toBe(37);
  });
  
  it("{age: 39} to equal {age: 39}", () => {
    expect({ age: 39 }).toEqual({ age: 39 });
  });
  
  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);
  });
  
  it(".toBeGreaterThanOrEqual", () => {
    expect(10).toBeGreaterThanOrEqual(10);
  });
  
  it(".toBeInstanceOf", () => {
    class Foo {}
    expect(new Foo()).toBeInstanceOf(Foo);
  });
});

 

 

 

 ๐Ÿ”— React Component Test 

 

$ npm test # test ์‹คํ–‰

 

 

 ๐Ÿ”— testing-library/react ํ™œ์šฉํ•˜๊ธฐ 

 

https://reactjs.org/docs/testing-recipes.html#act

 

Testing Recipes – React

A JavaScript library for building user interfaces

reactjs.org

 

// ./components/Button.test.js

import { act, fireEvent, getByText, render } from "@testing-library/react";
import Button from "./Button";

describe("Button ์ปดํฌ๋„ŒํŠธ (@testing-library/react)", () => {
  it("์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.", () => {
    const button = render(<Button />);
    expect(button).not.toBe(null);
  });

  it('"button"์ด๋ผ๊ณ  ์“ฐ์—ฌ์žˆ๋Š” ์—˜๋ฆฌ๋จผํŠธ๋Š” HTMLButtonElement ์ด๋‹ค.', () => {
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");
    expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
  });

  it('๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด, p ํƒœ๊ทธ ์•ˆ์— "๋ฒ„ํŠผ์ด ๋ฐฉ๊ธˆ ๋ˆŒ๋ ธ๋‹ค." ๋ผ๊ณ  ์“ฐ์—ฌ์ง„๋‹ค.', () => {
    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);
    const p = getByText("๋ฒ„ํŠผ์ด ๋ฐฉ๊ธˆ ๋ˆŒ๋ ธ๋‹ค.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });

  it('๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ธฐ ์ „์—๋Š”, p ํƒœ๊ทธ ์•ˆ์— "๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ์ง€ ์•Š์•˜๋‹ค." ๋ผ๊ณ  ์“ฐ์—ฌ์ง„๋‹ค.', () => {
    const { getByText } = render(<Button />);
    const p = getByText("๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ์ง€ ์•Š์•˜๋‹ค.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });

  it('๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ณ  5์ดˆ ๋’ค์—๋Š”, p ํƒœ๊ทธ ์•ˆ์— "๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ์ง€ ์•Š์•˜๋‹ค." ๋ผ๊ณ  ์“ฐ์—ฌ์ง„๋‹ค.', () => {
    jest.useFakeTimers();

    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);

    // 5์ดˆ ํ๋ฅธ๋‹ค.
    act(() => {
      jest.advanceTimersByTime(5000);
    });

    const p = getByText("๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ์ง€ ์•Š์•˜๋‹ค.");
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });

  it("๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด, 5์ดˆ ๋™์•ˆ ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑํ™” ๋œ๋‹ค.", () => {
    jest.useFakeTimers();

    const { getByText } = render(<Button />);
    const buttonElement = getByText("button");
    fireEvent.click(buttonElement);

    // ๋น„ํ™œ์„ฑํ™”
    // expect(buttonElement.disabled).toBeTruthy();
    expect(buttonElement).toBeDisabled();

    // 5์ดˆ ํ๋ฅธ๋‹ค.
    act(() => {
      jest.advanceTimersByTime(5000);
    });

    // ํ™œ์„ฑํ™”
    // expect(buttonElement.disabled).toBeFalsy();
    expect(buttonElement).not.toBeDisabled();
  });
});
// ./components/Button.jsx

import { useEffect, useRef, useState } from "react";

const BUTTON_TEXT = {
  NORMAL: "๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ์ง€ ์•Š์•˜๋‹ค.",
  CLICKED: "๋ฒ„ํŠผ์ด ๋ฐฉ๊ธˆ ๋ˆŒ๋ ธ๋‹ค.",
};

export default function Button() {
  const [message, setMessage] = useState(BUTTON_TEXT.NORMAL);
  const timer = useRef();

  useEffect(() => {
    return (
      () => {
        if (timer.current) {
          clearTimeout(timer.current);
        }
      },
      []
    );
  });

  return (
    <div>
      <button onClick={click} disabled={message === BUTTON_TEXT.CLICKED}>
        button
      </button>
      <p>{message}</p>
    </div>
  );

  function click() {
    setMessage(BUTTON_TEXT.CLICKED);
    timer.current = setTimeout(() => {
      setMessage(BUTTON_TEXT.NORMAL);
    }, 5000);
  }
}
// ./App.js

import logo from "./logo.svg";
import "./App.css";
import Button from "./components/Button";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        <Button />
      </header>
    </div>
  );
}

export default App;

 

 

 ๐Ÿ”— enzyme ํ™œ์šฉํ•˜๊ธฐ 

 

 


 

 

Ch 9. React Advanced

 

 

 ๐Ÿ”— Optimizing Performance : ํ•„์š”ํ•  ๋•Œ๋งŒ ๋ Œ๋”ํ•œ๋‹ค. = ๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋Š” ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

  • Reconciliation (ํ™”ํ•ด)
    • ๋ Œ๋” ์ „ํ›„์˜ ์ผ์น˜ ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๋Š” ๊ทœ์น™
    • ์„œ๋กœ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ๋‘ ์—˜๋ฆฌ๋จผํŠธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ํŠธ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด๋‚ธ๋‹ค.
    • ๊ฐœ๋ฐœ์ž๊ฐ€ key prop์„ ํ†ตํ•ด, ์—ฌ๋Ÿฌ ๋ Œ๋”๋ง ์‚ฌ์ด์—์„œ ์–ด๋–ค ์ž์‹ ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•„์•ผ ํ• ์ง€ ํ‘œ์‹œํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค

 

 

// ./App.js
// Class Component

import "./App.css";
import React from "react";

class Person extends React.PureComponent {
  // 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 (
      <div>
        {name} / {age}
      </div>
    );
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 39 },
      { id: 2, name: "Hanna", age: 28 },
    ],
  };

  render() {
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <ul>
          {persons.map((person) => {
            return (
              <Person
                {...person}
                key={person.id}
                onClick={this.toPersonClick}
              />
            );
          })}
        </ul>
      </div>
    );
  }

  _change = (e) => {
    this.setState({
      ...this.state,
      text: e.target.value,
    });
  };

  toPersonClick = () => {};
}

export default App;
// ./App.js
// Functional Component

import "./App.css";
import React, { useState, useCallback } from "react";

const Person = React.memo(({ name, age }) => {
  console.log("Person render");
  return (
    <div>
      {name} / {age}
    </div>
  );
});

function App() {
  const [state, setState] = React.useState({
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 39 },
      { id: 2, name: "Hanna", age: 28 },
    ],
  });

  const toPersonClick = React.useCallback(() => {}, []);

  const { text, persons } = state;

  return (
    <div>
      <input type="text" value={text} onChange={change} />
      <ul>
        {persons.map((person) => {
          return <Person {...person} key={person.id} onClick={toPersonClick} />;
        })}
      </ul>
    </div>
  );

  function change(e) {
    setState({
      ...state,
      text: e.target.value,
    });
  }
}

export default App;

 

 

 ๐Ÿ”— React.createPortal 

 

https://ko.reactjs.org/docs/portals.html

 

Portals – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

// ./public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
	...
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="modal"></div> // โญ
    ...
  </body>
</html>
// ./components/modal.jsx

import ReactDOM from "react-dom";

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

export default Modal;
// ./App.js

import "./App.css";
import React, { useState, useCallback } from "react";
import Modal from "./components/modal";

function App() {
  const [visible, setVisible] = useState(false);

  const open = () => {
    setVisible(true);
  };

  const close = () => {
    setVisible(false);
  };

  return (
    <div>
      <button onClick={open}>open</button>
      {visible && (
        <Modal>
          <div
            style={{
              width: "100vw",
              height: "100vh",
              background: "rgba(0,0,0,0.5",
            }}
            onClick={close}
          >
            Hello
          </div>
        </Modal>
      )}
    </div>
  );
}

export default App;

 

 

 ๐Ÿ”— React.forwardRef 

 

https://ko.reactjs.org/docs/forwarding-refs.html

 

Forwarding Refs – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

// ./App.js

import "./App.css";
import React, { useState, useCallback, useRef } from "react";
import MyInput from "./components/MyInput";

function App() {
  const myInputRef = useRef();

  const click = () => {
    console.log(myInputRef.current.value);
  };

  return (
    <div>
      <MyInput ref={myInputRef} />
      <button onClick={click}>send</button>
    </div>
  );
}

export default App;
// ./components/MyInput.jsx

import React from "react";

export default React.forwardRef(function MyInput(props, ref) {
  return (
    <div>
      <p>MyInput</p>
      <input ref={ref} />
    </div>
  );
});