๐Ÿ’™ React ํ™œ์šฉ๋ฒ• ํ•ต์‹ฌ ์š”์•ฝ

Jake_Youngยท2020๋…„ 12์›” 3์ผ
7
post-thumbnail

์ด ๊ธ€์€ "๋ฆฌ์•กํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ (๊น€๋ฏผ์ค€, ๊ธธ๋ฒ—)"์„ ์š”์•ฝํ•œ ๊ธ€์ž…๋‹ˆ๋‹ค.
๋งŽ์€ ์˜ˆ์ œ ์ฝ”๋“œ๋“ค์ด ์ด ์ฑ…์—์„œ ์ธ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
์ž์„ธํ•˜๊ณ  ์ •ํ™•ํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•˜์‹œ๋ ค๋ฉด ์ฑ…์„ ์ง์ ‘ ๊ตฌ๋งคํ•ด,
์ฝ์–ด๋ณด์‹œ๊ธฐ๋ฅผ ์ถ”์ฒœ๋“œ๋ฆฝ๋‹ˆ๋‹ค.


โ“ ์™œ ์ด ๊ธ€์„ ์“ฐ๋Š”๊ฐ€?

  • ์›น ๊ฐœ๋ฐœ์„ ์‹œ์ž‘ํ•œ ์ง€ ๋งŒ์œผ๋กœ 2๋…„ ์ •๋„ ๋จ
  • ๊ฐ€์žฅ ์ฒ˜์Œ ๋ฐฐ์› ์œผ๋ฉฐ ๊ฐ€์žฅ ๋งŽ์ด ํ™œ์šฉํ•œ ๋ทฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” React
  • ๊ทธ๊ฐ„ React๋ฅผ ๊ณต๋ถ€ํ•˜๋ฉฐ ์‚ฌ์šฉํ•˜์˜€๋งŒ ์ด๋ฅผ ์ •๋ฆฌํ•œ ์ ์ด ํ•œ๋ฒˆ๋„ ์—†์Œ
  • ํ˜„์žฌ ๋‹ค๋‹ˆ๋Š” ํšŒ์‚ฌ์˜ ํ‡ด์‚ฌ๋ฅผ ์•ž๋‘๊ณ  ์ธ์ˆ˜์ธ๊ณ„๋ฅผ ์œ„ํ•ด ์ด ๊ธ€์„ ์ž‘์„ฑ
  • ์ด ๊ธ€์€ React๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ฐœ๋…์„ ์ •๋ฆฌํ•˜์—ฌ ์š”์•ฝ
  • ์ „์—ญ ์ƒํƒœ๊ด€๋ฆฌ์—๋Š” Redux์™€ Redux-Thunk๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…

๐Ÿ˜€ React๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

  • Virtual DOM์ด ํ•ต์‹ฌ
  • React๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ DOM์„ ๋ณต์‚ฌํ•˜์—ฌ Virtual DOM์„ ์ƒ์„ฑ
  • ํŽ˜์ด์ง€์— ์ˆ˜์ •์ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๋•Œ, Virtual DOM๊ณผ ๋น„๊ต
  • Virtual DOM ๋•๋ถ„์— ์ˆ˜์ •์ด ํ•„์š”ํ•œ ์š”์†Œ๋งŒ ์—…๋ฐ์ดํŠธ
  • View ์ชฝ์—์„œ ํšจ์œจ์ ์ธ Rendering ๊ฐ€๋Šฅ

๐Ÿ˜€ Virtual DOM


๐Ÿ˜ Single Page Application(SPA)

  • ๊ธฐ์กด์—๋Š” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์— ๋”ฐ๋ผ ๋งค๋ฒˆ html ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
  • SPA์—์„œ๋Š” javascript๋ฅผ ์ด์šฉํ•ด html์˜ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅ
  • SPA์—์„œ๋Š” ์ฝ”๋“œ ํŒŒ์ผ์ด Component๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ
  • ๊ทธ๋ž˜์„œ React์—์„œ๋Š” CSS ์ฝ”๋“œ๋„ Component ํŒŒ์ผ ๋‚ด๋ถ€์— ํ•จ๊ป˜ ์ž‘์„ฑ

๐Ÿ˜ SPA์˜ Component ๊ตฌ์กฐ


โญ styled-components

  • React์˜ ์—ฌ๋Ÿฌ๊ฐ€์ง€ CSS ๋ฐฉ๋ฒ• ์ค‘ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋‚˜์™€ ๋„๋ฆฌ ์“ฐ์ด๋Š” ๋ฐฉ์‹
  • HTML Element๋ฅผ ๊ทธ๋Œ€๋กœ ์“ฐ์ง€ ์•Š๊ณ  ์ด๋ฆ„๋ถ€ํ„ฐ ๋‚ด์šฉ๊นŒ์ง€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
  • ํ•˜๋‚˜์˜ ํŒŒ์ผ์— ๋…ผ๋ฆฌ์™€ ๋””์ž์ธ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์ปดํฌ๋„ŒํŠธ์˜ ์ฝ”๋“œ ํŒŒ์•…์ด ์šฉ์ด

โญ styled-components ์ฝ”๋“œ ์˜ˆ์ œ


import styled from 'styled-components'

const Button = styled.button`
  background: white;
  color: black;
`

<Button>ํด๋ฆญ</Button>

๐Ÿ˜‚ Re-rendering ์กฐ๊ฑด

โŒ Life Cycle: Class Component <- ์ง€์–‘, ๊ณผ๊ฑฐ๋ฐฉ์‹

  • Life Cycle์€ Mount, Update and Unmount์˜ 3 ๊ฐ€์ง€๋กœ ๊ตฌ๋ถ„
  • mount๋ž€ DOM์ด ์ƒ์„ฑ๋˜๊ณ  ๋ธŒ๋ผ์šฐ์ € ์ƒ์— ๋‚˜ํƒ€๋‚˜๋Š” ์ƒํƒœ
  • update๋Š” ๋‹ค์Œ ์„ธ ๊ฐ€์ง€ ๊ฒฝ์šฐ์— ๋ฐœ์ƒํ•˜๋Š” ์ƒํƒœ
    1. props๋‚˜ state๊ฐ€ ๋ฐ”๋€” ๋•Œ
    2. ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋  ๋•Œ
    3. this.forceUpdate๋กœ ๊ฐ•์ œ๋กœ ํŠธ๋ฆฌ๊ฑฐ ๋  ๋•Œ
  • unmount๋ž€ ์ปดํฌ๋„ŒํŠธ๋ฅผ DOM์—์„œ ์ œ๊ฑฐํ•˜๋Š” ์ƒํƒœ

โญ Life Cycle: Class Component


โญ Class Component ์˜ˆ์‹œ


import React from 'react'

export const Component extends React.Component {

  constructor(props, context) {
    super(props, context);
    this.state = {
      name: "",
      age: "",
      country: ""
    };
  }

  componentDidUpdate(prevProps) {
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

  render() {
    return <h1>Hello, World!!</h1>
  }
}

โญ Life Cycle: Functional Component (์ถ”์ฒœ) โœ…

  • Hooks์„ ์‚ฌ์šฉํ•ด ์ฝ”๋“œ์˜ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ๊ฒฐ์ •
  • Mount์™€ Unmount๋Š” useEffect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„
  • Update๋Š” Hooks์˜ ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ Watchingํ•  ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•˜์—ฌ ๊ตฌํ˜„

โญ mount ์ž‘๋™ ๋ฐฉ๋ฒ•

useEffect(() => {
  "ํŽ˜์ด์ง€๊ฐ€ ๋งˆ์šดํŠธ ๋˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธ ๋œ ์ดํ›„ ์‹คํ–‰ํ•  ๋ช…๋ น์–ด";
}, []);

โญ unmount ์ž‘๋™ ๋ฐฉ๋ฒ•

useEffect(() => {
  return () => {
    "์—…๋ฐ์ดํŠธ๊ฐ€ ๋˜๊ธฐ ์ „์ด๋‚˜ ์–ธ๋งˆ์šดํŠธ ๋˜๊ธฐ ์ „ ์‹คํ–‰ํ•  ๋ช…๋ น์–ด";
  };
}, []);

โญ update ์ž‘๋™ ๋ฐฉ๋ฒ•

hooks(() => {}, ["์—ฌ๊ธฐ ์ž…๋ ฅํ•œ ๋ณ€์ˆ˜๋“ค์ด ์—…๋ฐ์ดํŠธ๋˜๋ฉด Hooks๊ฐ€ ์‹คํ–‰"]);

๐Ÿ˜† JSX

Flask, Django์˜ Jinja Template๊ณผ ๊ฐœ๋… ๋น„๊ต

  • jsx๋ž€ javascript์˜ ํ™•์žฅ ๋ฌธ๋ฒ•
  • Component์˜ return ๋ถ€๋ถ„์— HTML ์ฝ”๋“œ ๋Œ€์‹  ์‚ฌ์šฉ๋˜๋Š” javascript ๋ฌธ๋ฒ•
  • Flask์™€ Django์—์„œ View ํŒŒํŠธ์— ์‚ฌ์šฉ๋˜๋Š”,
  • HTML ๋ฌธ๋ฒ• ์ค‘๊ฐ„์—์„œ ์‚ฌ์šฉ๋˜๋Š” Python ์ฝ”๋“œ ๋น„์Šทํ•œ ๊ฒƒ๊ณผ ๊ฐ™์€ ์—ญํ• 

โญ Flask, Django์˜ Jinja Template๊ณผ ์ฝ”๋“œ ๋น„๊ต

// in Jinja Template
{% for example in examples %}
  <div>
    Hello, {{bank.name}} !!
  </div>
{% endfor %}

// in React
{
  examples.map(example=>{
    <div>
      Hello, {bank.name} !!
    </div>
  })
}

โญ JSX์˜ ๊ทœ์น™

  1. component๋Š” ํ•˜๋‚˜์˜ ๋ถ€๋ชจ ์š”์†Œ๋กœ ๊ฐ์‹ธ์ ธ์•ผ ํ•จ
  2. js ํ‘œํ˜„์‹์„ ์“ฐ๋ ค๋ฉด {}๋กœ ๊ฐ์‹ธ์ฃผ์–ด์•ผ ํ•จ
  3. if ๋Œ€์‹  ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ์”€: boolean ? true : false
  4. && ์—ฐ์‚ฐ์ž๋ฅผ ์ด์šฉํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง
  5. ์ธ๋ผ์ธ ์Šคํƒ€์ผ๋ง์€ camelCase๋กœ ์ž‘์„ฑ: background-color -> backgroundColor
  6. class ๋Œ€์‹  className
  7. <input>, <br> ๊ฐ™์ด HTML Element๋ฅผ ๊ผญ ๋‹ซ์•„์ฃผ์–ด์•ผ ํ•จ
  8. ์ฃผ์„์€ {/_ ... _/} ํ˜•ํƒœ๋กœ ์จ์•ผํ•จ: //๋กœ๋Š” ์ฃผ์„ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€

๐Ÿ˜ƒ Hooks

  • Hooks์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ
const [variable, control Function] = useHooks(( ) => { }, [variables watching at])

โญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์‹œ ์ฃผ์˜์‚ฌํ•ญ (๋ถˆ๋ณ€์„ฑ)

  • ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•  ๋•Œ๋Š” ๋ถˆ๋ณ€์„ฑ์„ ์ฃผ์˜
  • ๊ธฐ์กด์˜ ์ƒํƒœ๊ฐ€ object ํ˜•ํƒœ์ผ ๋•Œ ์ด๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋ฉด ์•ˆ๋จ
  • ๊ธฐ์กด์˜ ์ƒํƒœ๊ฐ’์„ ๋ณต์‚ฌํ•œ ์ƒˆ๋กœ์šด object๋ฅผ ๋งŒ๋“ค๊ณ ,
  • ์—ฌ๊ธฐ์— ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ƒˆ๋กœ์šด ์ƒํƒœ๋กœ ์—…๋ฐ์ดํŠธ ํ•ด์•ผํ•จ
  • ์–•์€ ๋น„๊ต๋ฅผ ํ†ตํ•ด ๋น ๋ฅด๊ณ  ํšจ๊ณผ์ ์œผ๋กœ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•จ
  • ์ƒํƒœ๋Š” ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๊ตํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋ฐ์ดํ„ฐ์˜ ์ฃผ์†Œ๊ฐ’์„ ๋น„๊ตํ•˜๊ธฐ ๋•Œ๋ฌธ

โญ useState

  • ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ Hook์œผ๋กœ ์ƒํƒœ ๋ณ€์ˆ˜์™€ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜
  • ContextAPI๋ฅผ ์“ธ ๋•Œ ์‚ฌ์šฉ๋จ

โญ useState ์˜ˆ์‹œ

import React, { useState } from "react";

const Counter = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>ํ˜„์žฌ ๊ฐ’์€ {value}์ž…๋‹ˆ๋‹ค.</p>
      <button
        onClick={() => {
          setValue(value + 1);
        }}
      >
        +1
      </button>
      <button
        onClick={() => {
          setValue(value - 1);
        }}
      >
        -1
      </button>
    </div>
  );
};

โญ useEffect

  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ๋  ๋•Œ๋งˆ๋‹ค ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ•˜๋Š” Hook
  • ํด๋ž˜์Šคํ˜• ์ปดํฌ๋„ŒํŠธ์˜ componentDidMount์™€ componentDidUpdate๋ฅผ ํ•ฉ์นœ ํ˜•ํƒœ
  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ mount ๋˜๊ฑฐ๋‚˜ update ๋œ ์ดํ›„์— ์–ด๋– ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด
  • Callback ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉ
  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ unmount ๋˜๊ฑฐ๋‚˜ update ๋˜๊ธฐ ์ง์ „์— ์–ด๋– ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด
  • Cleanup ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉ

โญ useEffect ์ฝ”๋“œ ์˜ˆ์‹œ

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

const Info = () => {
  const [name, setName] = useState("");
  const onChangeName = (e) => {
    setName(e.target.value);
  };
  useEffect(() => {
    // Mount & after Update Section
    console.log("It is wrote before rendering with new state");
    console.log(name);
    return () => {
      // Cleanup Part: Unmount & before Update Section
      console.log("It is wrote before update with prev state");
      console.log(name);
    };
  }, [name]);

  return (
    <div>
      <div>
        <input value={name} onChange={onChange} />
      </div>
      <div>์ด๋ฆ„: {name}</div>
    </div>
  );
};

โญ useReducer

  • useState๋ณด๋‹ค ๋” ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์— ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” Hook
  • ์ด๋Š” ๋ฆฌ๋•์Šค์—์„œ๋„ ์‚ฌ์šฉ๋˜๋Š” ํ•ต์‹ฌ ๊ฐœ๋…
  • ์ง€๊ธˆ ๋‹น์žฅ ์ดํ•ด๊ฐ€ ์–ด๋ ต๋‹ค๋ฉด, ๋ฆฌ๋•์Šค๋ฅผ ์‚ฌ์šฉํ•ด๋ณธ ์ดํ›„ ๋‹ค์‹œ ์ฝ์–ด๋„ ๋ฌด๊ด€
  • ๋ฆฌ๋“€์„œ๋Š” ํ˜„์žฌ ์ƒํƒœ์™€ ์—…๋ฐ์ดํŠธ์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์€ ์•ก์…˜๊ฐ’์œผ๋กœ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜

โญ useReducer ์ฝ”๋“œ ์˜ˆ์‹œ 1

import React, { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { value: state.value + 1 };
    case "DECREMENT":
      return { value: state.value - 1 };
    default:
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { value: 0 });
};

return (
  <div>
    <p>ํ˜„์žฌ ๊ฐ’์€ {state.value} ์ž…๋‹ˆ๋‹ค.</p>
    <button onClick={() => dispatch({ type: "INCREMENT" })}> +1 </button>
    <button onClick={() => dispatch({ type: "DECREMENT" })}> -1 </button>
  </div>
);

export default Counter;

โญ useReducer ์ฝ”๋“œ ์˜ˆ์‹œ 2

import React, {useReducer} from 'react'

function reducer(state, action) {
  retun {
    ...state,
    [action.name]: action.value
  }
}

const Info = () => {
  const [state, dispatch] = useReducer(reducer, {
    name: "",
    nickname: "",
  }
  const {name, nickname} = state
  const onChange = e => {
    dispatch(e.target)
  }

return (
  <div>
    <div>
      <input name="name" value={name} onChange={onChange} />
      <input name="nickname" value={nickname} onChange={onChange} />
    </div>
    <div>
      <div><b>์ด๋ฆ„:</b> {name}</div>
      <div><b>๋‹‰๋„ค์ž„:</b> {nickname}</div>
    </div>
  </div>
  )
}

โญ useMemo

  • ๋ Œ๋”๋งํ•˜๋Š” ๊ณผ์ •์—์„œ ํŠน์ • ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ ์—ฐ์‚ฐ์„ ์‹คํ–‰ํ•˜๊ณ ,
  • ์›ํ•˜๋Š” ๊ฐ’์ด ๋ฐ”๋€Œ์ง€ ์•Š์•˜๋‹ค๋ฉด ์ด์ „์— ์—ฐ์‚ฐํ–ˆ๋˜ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ Hook

โญ useMemo ์ฝ”๋“œ ์˜ˆ์‹œ

import React, { useState, useMemo } from "react";

const getAverage = (numbers) => {
  console.log("ํ‰๊ท ๊ฐ’ ๊ณ„์‚ฐ ์ค‘..");
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState("");

  const onChange = (e) => {
    setNumber(e.target.value);
  };
  const onInsert = () => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };

  // ์•„๋ž˜ avg๊ฐ€ ์—†์–ด์„œ getAverage(list)๋ฅผ ๋ฐ”๋กœ ์“ฐ๋ฉด ๋งค๋ฒˆ ์ƒˆ๋กœ ๊ณ„์‚ฐ
  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>๋“ฑ๋ก</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>ํ‰๊ท ๊ฐ’: </b> {avg}
      </div>
    </div>
  );
};

โญ useCallback

  • useCallback์€ useMemo์˜ ํŠน์ˆ˜ํ•œ ์ผ€์ด์Šค ์ค‘ ํ•˜๋‚˜
  • ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉ
  • useMemo ์˜ˆ์‹œ์ฝ”๋“œ(๋ฐ”๋กœ ์ง์ „ ์˜ˆ์‹œ์ฝ”๋“œ)๋ฅผ ๋ณด๋ฉด onChange์™€ onInsert ํ•จ์ˆ˜๊ฐ€ ์žˆ์Œ
  • ์ด ๋‘ ํ•จ์ˆ˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์–ด ์ž์›์„ ๋‚ญ๋น„ํ•จ
  • ์ตœ์ ํ™” ํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด useCallback์œผ๋กœ ๊ฐœ์„  ๊ฐ€๋Šฅํ•จ

โญ useCallback ์ฝ”๋“œ ์˜ˆ์‹œ (์ด์ „ ์˜ˆ์‹œ์—์„œ ์ผ๋ถ€ ์ˆ˜์ •)

const onChange = useCallback((e) => {
  setNumber(e.target.value);
}, []);

const onInsert = useCallback(() => {
  const nextList = list.concat(parseInt(number)); // ๋ถˆ๋ณ€์„ฑ!!
  setList(nextList);
  setNumber("");
}, [number, list]);

โญ useCallback๊ณผ useMemo ๋น„๊ต

// ์•„๋ž˜์˜ ๋‘ ํ•จ์ˆ˜๋Š” ์‚ฌ์‹ค์ƒ ๊ฐ™์€ ํ•จ์ˆ˜์ด๋‹ค.

useCallback(() => {
  console.log("hello world");
}, []);

useMemo(() => {
  const fn = () => {
    console.log("hello world");
  };
  return fn;
}, []);

โญ useRef

  • useRef๋ฅผ ์ด์šฉํ•ด DOM element์— ์ง์ ‘ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • useMemo ์˜ˆ์‹œ์ฝ”๋“œ์—์„œ onInsert ํ•จ์ˆ˜ ์‹คํ–‰ ํ›„,
  • ๋‹ค์‹œ input element์— ํฌ์ปค์Šค๊ฐ€ ์žกํžˆ๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
  • ๋ Œ๋”๋ง๊ณผ ์ƒ๊ด€ ์—†๋Š” ๋กœ์ปฌ ๋ณ€์ˆ˜๋กœ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ

โญ useRef ์ฝ”๋“œ ์˜ˆ์‹œ


const inputEl = useRef(null)
...
const onInsert = useCallback(()=> {
  ...
  input.El.current.focus()
})
...
  return (
  ...
    <input ref={inputEl}/>
  ...
  )

๐Ÿ˜‹ Router

โญ react-router-dom 1

  • React๋Š” ์ตœ์†Œํ•œ์˜ ๊ธฐ๋Šฅ๋งŒ ์ œ๊ณตํ•จ
  • ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๋†’์€ ์ž์œ ๋„๋ฅผ ๋ถ€์—ฌํ•จ
  • ์ด ๋•Œ๋ฌธ์— router ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋„ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”
  • ์ด๋Š” ์ผ์ฃผ์ผ์— 350๋งŒ๋ฒˆ ๋‹ค์šด ๋ฐ›์•„์ง€๋Š” ์‚ฌ์‹ค์ƒ ํ‘œ์ค€ router ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

โญ react-router-dom 2

  • ๋ผ์šฐํ„ฐ์˜ ์ตœ์ƒ๋‹จ์—๋Š” <Router>๊ฐ€ ์žˆ์–ด์•ผ ํ•จ
  • <Link>๋Š” <a>ํƒœ๊ทธ์™€ ๊ฑฐ์˜ ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•จ
  • ํ•˜์ง€๋งŒ <a>๋ฅผ ์“ฐ๊ฒŒ ๋˜๋ฉด ์ „์ฒด ํŽ˜์ด์ง€๊ฐ€ refresh ๋˜์–ด SPA๋ผ ํ•  ์ˆ˜ ์—†์Œ
  • ๊ทธ๋ž˜์„œ React์—์„œ๋Š” url์— ๋”ฐ๋ฅธ ํ™”๋ฉด ๋ Œ๋”๋ง์„ <Link>๋กœ ๊ตฌํ˜„ํ•ด์•ผํ•จ
  • <Switch>๋Š” url์— ๋”ฐ๋ผ ํ•˜๋‚˜์˜ ํ™”๋ฉด์„ ๋ Œ๋”๋งํ•จ
  • ์ด๋•Œ ๋ Œ๋”๋ง ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” <Switch> ์•„๋ž˜ ์žˆ๋Š” <Route> ์ค‘ ํ•˜๋‚˜
  • <Switch>๋Š” ํ˜„์žฌ url๊ณผ ๋งค์น˜๋˜๋Š” ์ฒซ๋ฒˆ์งธ <Route>๋ฅผ ๋ Œ๋”๋ง

โญ react-router-dom ์ฝ”๋“œ ์˜ˆ์‹œ

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

๐Ÿ˜Ž Redux(+Redux-Thunk) 1

Action

  • ์•ก์…˜์€ ๊ฐ์ฒด
  • type ํ•„๋“œ๋Š” ํ•„์ˆ˜ ํ•„๋“œ
  • type ํ•„๋“œ๊ฐ€ ์ด ์•ก์…˜์˜ ์ด๋ฆ„

Const Action = {
  TYPE: "Name of this Action"
}

๐Ÿ˜Ž Redux(+Redux-Thunk) 2

Action Creator

  • ์•ก์…˜ ๊ฐ์ฒด๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜
  • ๊ทธ ์˜๋ฏธ ์ด์ƒ์€ ์—†์Œ
  • ๊ทธ๋ƒฅ ์•ก์…˜ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด๋„ ๋จ

Store

  • ์ „์—ญ ์ƒํƒœ์™€ ๋ฆฌ๋“€์„œ ๊ทธ๋ฆฌ๊ณ  ์ค‘์š”ํ•œ ๋‚ด์žฅ ํ•จ์ˆ˜๋“ค์„ ์ง€๋‹Œ ๋ฆฌ๋•์Šค์˜ ํ•ต์‹ฌ
  • ์Šคํ† ์–ด๋Š” ๋ฆฌ์Šค๋„ˆ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œ์ผœ ๋””์ŠคํŒจ์น˜๊ฐ€ ์‹คํ–‰๋˜๋Š”์ง€ ํ•ญ์ƒ ๊ฐ์‹œ

๐Ÿ˜Ž Redux(+Redux-Thunk) 3

Dispatch

  • ์•ก์…˜ ๊ฐ์ฒด์„ ์‹คํ–‰(์ „๋‹ฌ)์‹œ์ผœ์ฃผ๋Š” ํ•จ์ˆ˜
  • ์Šคํ† ์–ด์˜ ๋‚ด์žฅ ํ•จ์ˆ˜ ์ค‘ ํ•˜๋‚˜
  • dispatch(action) ์ฒ˜๋Ÿผ ์‚ฌ์šฉ
  • dispatch๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์Šคํ† ์–ด๊ฐ€ ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ด ์ƒˆ๋กœ์šด ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ

Reducer

  • ํ˜„์žฌ ์ƒํƒœ์™€ ๋””์ŠคํŒจ์น˜๊ฐ€ ์ „๋‹ฌํ•œ ์•ก์…˜์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜
  • ๋ฆฌ๋“€์„œ๋Š” ๋‹ค์Œ ์˜ˆ์ œ ์ฝ”๋“œ์™€ ๊ฐ™์€ ๊ตฌ์กฐ๋กœ ์ž‘์„ฑ

โญ Reducer ์ฝ”๋“œ ์˜ˆ์‹œ

const initialState = {
  counter: 1,
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return {
        couter: state.counter + 1,
      };
    case DECREMENT:
      return {
        couter: state.counter - 1,
      };
    default:
      return state;
  }
}

โญ ๋ฆฌ๋•์Šค ์‚ฌ์šฉ ์‹œ ์ฃผ์˜ ์‚ฌํ•ญ

  • ๋ฆฌ๋“€์„œ๋Š” ์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜์—ฌ์•ผ ํ•จ
  • ์ˆœ์ˆ˜ํ•œ ํ•จ์ˆ˜๋ž€ ๋˜‘๊ฐ™์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ˜ธ์ถœ๋œ ๋ฆฌ๋“€์„œ๋Š”...
  • ์–ธ์ œ๋‚˜ ๊ฐ™์€ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ
  • ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ๋žœ๋ค ๊ฐ’์ด๋‚˜ Date ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋จ
  • ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์„ ํ•˜๋Š” ๊ฒƒ๋„ ์•ˆ๋จ
  • ์ด๋Ÿฌํ•œ ์ž‘์—…์€ ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜ ๋ฐ–์ด๋‚˜ ๋ฆฌ๋•์Šค ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ์ž‘์—…ํ•ด์•ผํ•จ

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ


โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ App.js

import React from "react";
import "./styles.css";
import CounterContainer from "./containers/CounterContainer";
import TodosContainer from "./containers/TodosContainer";

export default function App() {
  return (
    <div className="App">
      <CounterContainer />
      <hr />
      <TodosContainer />
    </div>
  );
}

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ index.js

import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { composeWithDevTools } from "redux-devtools-extension";
import rootReducer from "./modules";

import App from "./App";

const store = createStore(rootReducer, composeWithDevTools());

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ CounterContainer.js

import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import Counter from "../components/Counter";
import { increase, decrease } from "../modules/counter";

const CounterContainer = () => {
  const number = useSelector((state) => state.counter.number);
  const dispatch = useDispatch();
  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  return (
    <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
  );
};

export default React.memo(CounterContainer);

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ TodosContainer.js

import React from "react";
import { useSelector } from "react-redux";
import { changeInput, insert, toggle, remove } from "../modules/todos";
import Todos from "../components/Todos";
import useActions from "../lib/useActions";

const TodosContainer = () => {
  const { input, todos } = useSelector(({ todos }) => ({
    input: todos.input,
    todos: todos.todos,
  }));

  const [onChangeInput, onInsert, onToggle, onRemove] = useActions(
    [changeInput, insert, toggle, remove],
    []
  );

  return (
    <Todos
      input={input}
      todos={todos}
      onChangeInput={onChangeInput}
      onInsert={onInsert}
      onToggle={onToggle}
      onRemove={onRemove}
    />
  );
};

export default TodosContainer;

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ modules/counter.js

import { createAction, handleActions } from "redux-actions";

// ์•ก์…˜ ํƒ€์ž… ์ •์˜
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

// ์ดˆ๊ธฐ ์ƒํƒœ ๋ฐ ๋ฆฌ๋“€์„œ
const initialState = {
  number: 0,
};

const counter = handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 }),
    [DECREASE]: (state, action) => ({ number: state.number - 1 }),
  },
  initialState
);

export default counter;

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ modules/todos.js 1

import { createAction, handleActions } from "redux-actions";
import produce from "immer";

const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";

export const changeInput = createAction(CHANGE_INPUT, (input) => input);

let id = 3;

export const insert = createAction(INSERT, (text) => ({
  id: id++,
  text,
  done: false,
}));

export const toggle = createAction(TOGGLE, (id) => id);

export const remove = createAction(REMOVE, (id) => id);

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ modules/todos.js 2

const initialState = {
  input: "",
  todos: [
    {
      id: 1,
      text: "learning basic redux",
      done: true,
    },
    {
      id: 2,
      text: "using redux and react",
      done: false,
    },
  ],
};

const todos = handleActions(
  {
    [CHANGE_INPUT]: (state, { payload: input }) =>
      produce(state, (draft) => {
        draft.input = input;
      }),
    [INSERT]: (state, { payload: todo }) =>
      produce(state, (draft) => {
        draft.todos.push(todo);
      }),
    [TOGGLE]: (state, { payload: id }) =>
      produce(state, (draft) => {
        const todo = draft.todos.find((todo) => todo.id === id);
        todo.done = !todo.done;
      }),
    [REMOVE]: (state, { payload: id }) =>
      produce(state, (draft) => {
        const index = draft.todos.findIndex((todo) => todo.id === id);
        draft.todos.splice(index, 1);
      }),
  },
  initialState
);

export default todos;

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ modules/index.js

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

const rootReducer = combineReducers({
  counter,
  todos,
});

export default rootReducer;

// components/Counter.js

import React from "react";

const Counter = ({ number, onIncrease, onDecrease }) => {
  return (
    <div>
      <h1>{number}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
};

export default Counter;

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ components/Todos.js 1

import React from "react";

const TodoItem = ({ todo, onToggle, onRemove }) => {
  return (
    <div>
      <input
        type="checkbox"
        onClick={() => onToggle(todo.id)}
        checked={todo.done}
        readOnly={true}
      />
      <span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
        {todo.text}
      </span>
      <button onClick={() => onRemove(todo.id)}> delete </button>
    </div>
  );
};

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ components/Todos.js 2

const Todos = ({
  input,
  todos,
  onChangeInput,
  onInsert,
  onToggle,
  onRemove,
}) => {
  const onSubmit = (e) => {
    e.preventDefault();
    onInsert(input);
    onChangeInput("");
  };
  const onChange = (e) => onChangeInput(e.target.value);

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button type="submit">enroll</button>
      </form>
      <div>
        {todos.map((todo) => {
          return (
            <TodoItem
              todo={todo}
              key={todo.id}
              onToggle={onToggle}
              onRemove={onRemove}
            />
          );
        })}
      </div>
    </div>
  );
};

export default Todos;

โญ Redux ์˜ˆ์ œ ํด๋” ๊ตฌ์กฐ lib/useActions.js

import { bindActionCreators } from "redux";
import { useDispatch } from "react-redux";
import { useMemo } from "react";

export default function useActions(actions, deps) {
  const dispatch = useDispatch();
  return useMemo(
    () => {
      if (Array.isArray(actions)) {
        return actions.map((a) => bindActionCreators(a, dispatch));
      }
      return bindActionCreators(actions, dispatch);
    },
    deps ? [dispatch, ...deps] : [dispatch]
  );
}

โญ LocalStorage์™€ SessionStorage

  • ๋ฆฌ์•กํŠธ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์ƒํƒœ๋Š” ํœ˜๋ฐœ์„ฑ ๋ฉ”๋ชจ๋ฆฌ
  • ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด, ๊ธฐ์กด์˜ ์ƒํƒœ๋Š” ๋ชจ๋‘ ์ดˆ๊ธฐํ™”
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Redux์™€ Storage๋ฅผ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์Œ
  • ์ž์„ธํ•œ ์—ฐ๋™ ๋ฐฉ๋ฒ•์€ Redux SessionStorage๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์ ์šฉ

โญ Redux-Thunk

  • ๋ฆฌ๋•์Šค๋ฅผ ํ†ตํ•ด API ํ†ต์‹ ์„ ํ•˜๊ฒŒ๋˜๋ฉด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํˆด์ด ํ•„์š”
  • ์ด ๋•Œ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ Redux-Thunk, 2.14M/week ์ •๋„ ๋‹ค์šด๋กœ๋“œ
  • ๊ณ ๊ธ‰ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ๋Š” Redux-Saga๊ฐ€ ์žˆ์œผ๋ฉฐ ์ด๋Š” 0.77M/week ์ •๋„ ๋‹ค์šด๋กœ๋“œ
  • Redux-Thunk๋Š” Async ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  Redux-Saga๋Š” Generator ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉ
  • Redux-Thunk๋Š” ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ ํ•จ์ˆ˜ ํ˜•ํƒœ์˜ ์•ก์…˜์„ ๋””์ŠคํŒจ์น˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ ๊ตฌ์กฐ


โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ App.js

import React from "react";
import "./styles.css";
import CounterContainer from "./containers/CounterContainer";
import SampleContainer from "./containers/SampleContainer";

export default function App() {
  return (
    <div className="App">
      <SampleContainer />
      <CounterContainer />
    </div>
  );
}

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ index.js

import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer from "./modules";
import { createLogger } from "redux-logger";
import ReduxThunk from "redux-thunk";

const rootElement = document.getElementById("root");
const logger = createLogger();

const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  rootElement
);

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ moduels/index.js

import { combineReducers } from "redux";
import counter from "./counter";
import sample from "./sample";
import loading from "./loading";

const rootReducer = combineReducers({
  counter,
  loading,
  sample,
});

export default rootReducer;

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ modules/loading.js

import { createAction, handleActions } from "redux-actions";

const START_LOADING = "loading/START_LOADING";
const FINISH_LOADING = "loading/FINISH_LOADING";

export const startLoading = createAction(
  START_LOADING,
  (requestType) => requestType
);

export const finishLoading = createAction(
  FINISH_LOADING,
  (requestType) => requestType
);

const initialState = {};

const loading = handleActions(
  {
    [START_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: true,
    }),
    [FINISH_LOADING]: (state, action) => ({
      ...state,
      [action.payload]: false,
    }),
  },
  initialState
);

export default loading;

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ moduels/counter.js

import { createAction, handleActions } from "redux-actions";

const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";

export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

export const increaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(increase());
  }, 1000);
};

export const decreaseAsync = () => (dispatch) => {
  setTimeout(() => {
    dispatch(decrease());
  }, 1000);
};

const initialState = 0;

const counter = handleActions(
  {
    [INCREASE]: (state) => state + 1,
    [DECREASE]: (state) => state - 1,
  },
  initialState
);

export default counter;

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ module/sample.js

import { handleActions } from "redux-actions";
import * as api from "../lib/api";
import createRequestThunk from "../lib/createRequestThunk";

const GET_POST = "sample/GET_POST";
const GET_POST_SUCCESS = "sample/GET_POST_SUCCESS";

const GET_USERS = "sample/GET_USER";
const GET_USERS_SUCCESS = "sample/GET_USER_SUCCESS";

export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

const initialState = {
  post: null,
  users: null,
};

const sample = handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      post: action.payload,
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      users: action.payload,
    }),
  },
  initialState
);

export default sample;

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ lib/api.js

import axios from "axios";

export const getPost = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);

export const getUsers = (id) =>
  axios.get(`https://jsonplaceholder.typicode.com/users`);

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ lib/createRequestThunk.js

import { startLoading, finishLoading } from "../modules/loading";

export default function createRequestThunk(type, request) {
  const SUCCESS = `${type}_SUCCESS`;
  const FAILURE = `${type}_FAILURE`;
  return (params) => async (dispatch) => {
    dispatch({ type });
    dispatch(startLoading(type));
    try {
      const response = await request(params);
      dispatch({ type: SUCCESS, payload: response.data });
      dispatch(finishLoading(type));
    } catch (e) {
      dispatch({ type: FAILURE, payload: e, error: true });
      dispatch(finishLoading(type));
      throw e;
    }
  };
}

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ containers/CounterContainer.js

import React from "react";
import { connect } from "react-redux";
import { increaseAsync, decreaseAsync } from "../modules/counter";
import Counter from "../components/Counter";

const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
  return (
    <Counter
      number={number}
      onIncrease={increaseAsync}
      onDecrease={decreaseAsync}
    />
  );
};

export default connect(
  (state) => ({
    number: state.counter,
  }),
  {
    increaseAsync,
    decreaseAsync,
  }
)(CounterContainer);

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ containers.SampleContainer.js

import React, { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample";

const SampleContainer = ({
  getPost,
  getUsers,
  post,
  users,
  loadingPost,
  loadingUsers,
}) => {
  useEffect(() => {
    const fn = async () => {
      try {
        await getPost(1);
        await getUsers(1);
      } catch (e) {
        console.log(e);
      }
    };
    fn();
  }, [getPost, getUsers]);
  return (
    <Sample
      post={post}
      users={users}
      loadingPost={loadingPost}
      loadingUsers={loadingUsers}
    />
  );
};

export default connect(
  ({ sample, loading }) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: loading["sample/GET_POST"],
    loadingUsers: loading["sample/GET_USERS"],
  }),
  {
    getPost,
    getUsers,
  }
)(SampleContainer);

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ components/Counter.js

import React from "react";

const Counter = ({ onIncrease, onDecrease, number }) => {
  return (
    <div>
      <h1>{number}</h1>
      <button
        onClick={() => {
          onIncrease();
        }}
      >
        +1
      </button>
      <button
        onClick={() => {
          onDecrease();
        }}
      >
        -1
      </button>
    </div>
  );
};

export default Counter;

โญ Redux-Thunk ์˜ˆ์‹œ ์ฝ”๋“œ components/Sample.js

import React from "react";

const Sample = ({ loadingPost, loadingUsers, post, users }) => {
  return (
    <div>
      <section>
        <h1>Post</h1>
        {loadingPost && "loading..."}
        {!loadingPost && post && (
          <div>
            <h3>{post.title}</h3>
            <h3>{post.body}</h3>
          </div>
        )}
      </section>
      <hr />
      <section>
        <h1>users list</h1>
        {loadingUsers && "loading..."}
        {!loadingUsers && users && (
          <ul>
            {users.map((user) => (
              <li key={user.id}>
                {user.username} ({user.email})
              </li>
            ))}
          </ul>
        )}
      </section>
    </div>
  );
};

export default Sample;

โญ ContextAPI

  • ๊ทธ๋Ÿฐ๋ฐ ์ตœ๊ทผ ํŠธ๋ Œ๋“œ๋Š” Redux ๋Œ€์‹  ContextAPI๋ฅผ ์‚ฌ์šฉ
  • ์ „์—ญ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ
  • ๋กœ๊ทธ์ธ ์ •๋ณด, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ™˜๊ฒฝ ์„ค์ •, ํ…Œ๋งˆ ๋“ฑ์„ ์ „์—ญ์—์„œ ์‚ฌ์šฉํ•  ๋•Œ ์œ ์šฉ
  • Redux, React Router, styled-components ๋“ฑ์ด Context API๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„
  • Consumer ์ปดํฌ๋„ŒํŠธ ๋ฌธ๋ฒ•์ด ์žˆ๋Š”๋ฐ ์ด๋Š” Render Props์— ๋Œ€ํ•œ ์ดํ•ด๊ฐ€ ํ•„์š”
  • ๊ทธ๋ž˜์„œ ์ตœ์‹  ๋ฐฉ๋ฒ•์ธ Hook์„ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋กœ ์ •๋ฆฌ
  • ํ•˜์ง€๋งŒ ์ด ๋ฐฉ๋ฒ•์€ Functional component์—์„œ๋งŒ ์ž‘๋™๋œ๋‹ค๋Š” ํ•œ๊ณ„

โญ ContextAPI ๊ฐœ๋…๋„


โญ ContextAPI ์˜ˆ์ œ ์ฝ”๋“œ ๊ฒฐ๊ณผ๋ฌผ


โญ ContextAPI ์˜ˆ์ œ ์ฝ”๋“œ App.js

import React from "react";
import "./styles.css";
import ColorBox from "./ColorBox";
import SelectColors from "./SelectColors";
import { ColorProvider } from "./colorContext";

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <ColorProvider>
        <div
          style={{
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            flexDirection: "column",
          }}
        >
          <SelectColors />
          <br />
          <ColorBox />
        </div>
      </ColorProvider>
    </div>
  );
}

โญ ContextAPI ์˜ˆ์ œ ์ฝ”๋“œ colorContext.js

import React, { createContext, useState } from "react";

// ์ปจํ…์ŠคํŠธ์˜ ๊ธฐ๋ณธ๊ฐ’์„ ์ง€์ •
const ColorContext = createContext({
  state: { color: "black", subcolor: "red" },
  action: {
    setColor: () => {},
    setSubcolor: () => {},
  },
});

export const ColorProvider = ({ children }) => {
  const [color, setColor] = useState("black");
  const [subcolor, setSubcolor] = useState("red");

  const value = {
    state: { color, subcolor },
    actions: { setColor, setSubcolor },
  };

  return (
    <ColorContext.Provider value={value}>{children}</ColorContext.Provider>
  );
};

export default ColorContext;

โญ ContextAPI ์˜ˆ์ œ ์ฝ”๋“œ ColorBox.js

import React, { useContext } from "react";
import ColorContext from "./colorContext";

const ColorBox = () => {
  const { state } = useContext(ColorContext);
  return (
    <>
      <div
        style={{
          height: "64px",
          width: "64px",
          background: state.color,
        }}
      />
      <div
        style={{
          height: "32px",
          width: "32px",
          background: state.subcolor,
        }}
      />
    </>
  );
};

export default ColorBox;

โญ ContextAPI ์˜ˆ์ œ ์ฝ”๋“œ SelectColors.js

import React, { useContext } from "react";
import ColorContext from "./colorContext";

const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

const SelectColors = () => {
  const { actions } = useContext(ColorContext);
  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        flexDirection: "column",
      }}
    >
      <h2>Choose the colors</h2>
      <div style={{ display: "flex" }}>
        {colors.map((color) => (
          <div
            key={color}
            style={{
              background: color,
              width: "24px",
              height: "24px",
              cursor: "pointer",
            }}
            onClick={() => {
              actions.setColor(color);
            }}
            onContextMenu={(e) => {
              e.preventDefault();
              actions.setSubcolor(color);
            }}
          />
        ))}
      </div>
    </div>
  );
};

export default SelectColors;

๐Ÿ˜„ ์„ฑ๋Šฅ์ตœ์ ํ™”

React.lazy์™€ Suspense

  • build์‹œ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•˜๋‚˜์˜ ํŒŒ์ผ๋กœ ๋นŒ๋“œ
  • ์ด๋Š” ๋‹น์žฅ ํ•„์š”ํ•˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ๊นŒ์ง€ ํฌํ•จํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํŽ˜์ด์ง€ ๋กœ๋”ฉ์— ๋ถ€๋‹ด
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์ด ์ฝ”๋“œ ๋น„๋™๊ธฐ ๋กœ๋”ฉ
  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ•จ์ˆ˜๋‚˜ ๊ฐ์ฒด ํ˜น์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•„์š”ํ•œ ์‹œ์ ์— ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ
  • ๊ธฐ์กด์˜ import A from 'B'๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ ,
  • ์ฝ”๋“œ ์ค‘๊ฐ„์— import(ํŒŒ์ผ).then(result=>result.default())๋ฅผ ์‚ฌ์šฉํ•ด ๋กœ๋”ฉ

โญ React.lazy์™€ Suspense ์ฝ”๋“œ ์˜ˆ์ œ

import React, { useState, Suspense } from "react";
const SplitMe = React.lazy(() => import("./SplitMe"));

function App() {
  const [visible, setVisible] = useState(false);
  const onClick = () => {
    setVisible(true);
  };
  return (
    <div>
      <p onClick={onClick}>Hello React!</p>
      <Suspense fallback={<div>loading...</div>}>
        {visible && <SplitMe />}
      </Suspense>
    </div>
  );
}

โญ React.memo

  • ์ปดํฌ๋„ŒํŠธ์˜ props๊ฐ€ ๋ฐ”๋€Œ์ง€ ์•Š์•˜๋‹ค๋ฉด,
  • ๋ฆฌ๋ Œ๋”๋งํ•˜์ง€ ์•Š๋„๋ก ๊ธฐ๋Šฅํ•ด ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”
  • export default React.memo(Component Name)

โญ useCallback & useMemo

  • ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง ๋  ๋•Œ๋งˆ๋‹ค ํ•จ์ˆ˜๋‚˜ ๋ณ€์ˆ˜๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•œ Hook
  • Hooks ์„ค๋ช… ์„น์…˜์—์„œ ์„ค๋ช…ํ•œ ๋ฐฉ์‹๋Œ€๋กœ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ

๐ŸŽ‡ Javascript ๋ฌธ๋ฒ• 1

โญ {[variable]: value}

  • object์˜ ํ‚ค์— ๋ณ€์ˆ˜์˜ ๊ฐ’์„ ๋„ฃ๊ณ  ์‹ถ์„ ๋• ๋ณ€์ˆ˜๋ฅผ ๋Œ€๊ด„ํ˜ธ๋กœ ๊ฐ์‹ผ ํ˜•ํƒœ๋กœ ์ „๋‹ฌ

โญ ๋ฌธ๋ฒ•: '...'

  • Spread Operator
  • Array๋‚˜ Object ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๊บผํ’€ ๋ฒ—๊ฒจ๋‚ด๊ณ  ๋Œ€์ž…ํ•˜๋Š” ๊ฒƒ
  • ๋ถˆ๋ณ€์„ฑ์„ ์ง€ํ‚ค๊ธฐ ์œ„ํ•ด ๊นŠ์ด๊ฐ€ ๊นŠ์€ Object๋ฅผ ๋ณต์‚ฌํ•  ๋•Œ๋Š” immer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”์ฒœ

๐ŸŽ‡ Javascript ๋ฌธ๋ฒ• 2

โญ <div hello> VS {hello}์˜ ์ฐจ์ด

  • HTML element์— props๋ฅผ ์„ค์ •ํ•  ๋•Œ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์กด์žฌ
  • propsName={propsValue}๋กœ ์“ฐ๊ฑฐ๋‚˜ propsName๋งŒ ์ ๋Š” ๋ฐฉ์‹
  • ํ›„์ž๋Š” props์— true ๊ฐ’์ด ๋Œ€์ž…๋˜์–ด ์ „๋‹ฌ๋จ์„ ์œ ์˜
  • javascript object์—์„œ๋„ {key: value} ๋Œ€์‹  {key}๋ผ๊ณ ๋งŒ ์ ๋Š” ๋ฌธ๋ฒ• ์กด์žฌ
  • ์ด ๋•Œ๋Š” key์˜ ๋ฌธ์ž์—ด ๊ฐ’์ด ํ‚ค๊ฐ€ ๋˜๊ณ  key์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ฐ’์ด ๋ฒจ๋ฅ˜๋กœ ๋Œ€์ž…

๐ŸŽ‡ Javascript ๋ฌธ๋ฒ• 3

โญ ๊ฐ์ฒด ๋น„๊ตฌ์กฐํ™” ํ• ๋‹น ๋ฌธ๋ฒ•

  • const {a, b} = d, const {a:b, c:d} = e
  • ์ฒซ๋ฒˆ์งธ์˜ ๊ฒฝ์šฐ a์—๋Š” d.a๊ฐ€ b์—๋Š” d.b๊ฐ€ ๋Œ€์ž…
  • ๋‘๋ฒˆ์งธ์˜ ๊ฒฝ์šฐ b์— e.a๊ฐ€ d์— e.c๊ฐ€ ๋Œ€์ž…

โญ Tagged ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด

  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๊ฐ์ฒด๋‚˜ ํ•จ์ˆ˜๋ฅผ ์˜จ์ „ํžˆ ์ „๋‹ฌํ•˜๊ณ  ์ถ”์ถœํ•จ
  • ํ•จ์ˆ˜์˜ ์ƒˆ๋กœ์šด ์‹คํ–‰ ๋ฐฉ์‹
console.log(`hello ${{ foo: "bar" }} ${() => "world"}`);
// ์ถœ๋ ฅ ๊ฒฐ๊ณผ: "hello [object Object] () => 'world'"

console.log`hello ${{ foo: "bar" }} ${() => "world"}`;
// ์ถœ๋ ฅ ๊ฒฐ๊ณผ: (3) ["hello ", " ", "", raw: Array(3)] {foo: "bar"} ()=>'world'

โญ Render Props ์˜ˆ์‹œ

// ์ค€๋น„

import React from 'react'

const RenderPropsSample = ({children}) => {
  return <div>๊ฒฐ๊ณผ: {children(5)}</div>
}

export default RenderPropsSample

// ๋‚˜์ค‘์— ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ..

<RenderPropsSample>{value => 2*value}</RenderPropsSample>

// ๊ทธ๋Ÿฌ๋ฉด "๊ฒฐ๊ณผ: 10"๊ฐ€ ๋ Œ๋”๋ง ๋จ

๐Ÿงก ๊ฟ€ํŒ 1

โญ ESLint, Prettier

  • Visual Studio Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์œ„์˜ ๋‘ extension์„ ๋‹ค์šด๋กœ๋“œ
  • settings์— ๋“ค์–ด๊ฐ€ Format on save๋ฅผ ์ฒดํฌ
  • ์ดํ›„ js ํŒŒ์ผ์„ ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น ํŒŒ์ผ์ด ํ‘œ์ค€์— ๋งž์ถฐ ์ž๋™ Reformatting

๐Ÿงก ๊ฟ€ํŒ 2

โญ react snippet

  • Visual Studio Code์—์„œ react snippet extension์„ ๋‹ค์šด๋กœ๋“œ
  • ์ดํ›„ ๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์งง๊ฒŒ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ์Œ
  • ์˜ˆ๋ฅผ๋“ค์–ด rfc(react functional component์˜ ์•ฝ์ž)๋ฅผ ์“ด ํ›„ TAB์„ ๋ˆ„๋ฅด๋ฉด,
  • functional component์˜ ๊ธฐ๋ณธ ๊ณจ๊ฒฉ ์ฝ”๋“œ๊ฐ€ ์ „๋ถ€ ๋กœ๋”ฉ
  • imp(import์˜ ์•ฝ์ž)๋ฅผ ์“ด ํ›„ TAB์„ ๋ˆ„๋ฅด๋ฉด,
  • ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ช…๊ณผ ๋ถˆ๋Ÿฌ์˜ฌ ๋ฉ”์˜๋“œ๋งŒ ์ž…๋ ฅํ•˜๋ฉด ๋˜๊ฒŒ ๋” ๋‹ค๋ฅธ ๋ฌธ์ž์—ด์€ ์ž๋™ ๋กœ๋”ฉ

๐Ÿ“– ์ฝ”๋“œ ์˜ˆ์‹œ ์ถœ์ฒ˜

  • ๋ฆฌ์•กํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ , ๊น€๋ฏผ์ค€, ๊ธธ๋ฒ—

๐Ÿ”ฅ ์ฃผ์˜์‚ฌํ•ญ

  • 2018๋…„ ์ด์ „์— ๋ฐœํ–‰๋œ ์ฑ…์€ class component๋ฅผ ๊ธฐ๋ฐ˜์ด๋ผ ์‚ฌ์ง€ ๋ง์•„์•ผ ํ•จโŒ
  • 2019๋…„ ์ดํ›„ ํŒ์ด functional component ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๋ช…๋˜์–ด ์žˆ์Œโœ…
profile
์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ ํŒŒ์ด์ฌ ๊ทธ๋ฆฌ๊ณ  ์ปดํ“จํ„ฐ์™€ ๋„คํŠธ์›Œํฌ

0๊ฐœ์˜ ๋Œ“๊ธ€