[React] 8์žฅ. Hooks (useState, useEffect, useReducer, useMemo, useCallback, useRef)

๊ฒจ๋ ˆยท2024๋…„ 11์›” 20์ผ

[React] ๋ฆฌ์•กํŠธ ์Šคํ„ฐ๋””

๋ชฉ๋ก ๋ณด๊ธฐ
42/95

๐Ÿ“ Hooks
ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” useState, ๋ Œ๋”๋ง ์งํ›„ ์ž‘์—…์„ ์„ค์ •ํ•˜๋Š” useEffect ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด ๊ธฐ์กด์˜ ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•  ์ˆ˜ ์—†์—ˆ๋˜ ๋‹ค์–‘ํ•œ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์คŒ.


โ‘  useState

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ Hook์œผ๋กœ ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ๊ฐ€๋ณ€์ ์ธ ์ƒํƒœ๋ฅผ ์ง€๋‹ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์คŒ. ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค๋ฉด useState๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋จ.

// Counter.jsx
import React, { useState } from 'react';
 
const Counter = () => {
  // ํ˜„์žฌ 0์„ ๋„ฃ์–ด ์คŒ => ๊ฒฐ๊ตญ ์นด์šดํ„ฐ์˜ ๊ธฐ๋ณธ๊ฐ’์„ 0์œผ๋กœ ์„ค์ •ํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ
  // ์ด ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•จ.
  // ๋ฐฐ์—ด์˜ ์ฒซ ๋ฒˆ์งธ ์›์†Œ๋Š” ์ƒํƒœ ๊ฐ’, ๋‘ ๋ฒˆ์งธ ์›์†Œ๋Š” ์ƒํƒœ๋ฅผ ์„ค์ •ํ•˜๋Š” ํ•จ์ˆ˜
  // ์ด ํ•จ์ˆ˜์— ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋„ฃ์–ด์„œ ํ˜ธ์ถœํ•˜๋ฉด ์ „๋‹ฌ๋ฐ›์€ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฐ’์ด ๋ฐ”๋€Œ๊ณ  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง๋จ.
  const [value, setValue] = useState(0);
 
  return (
    <div>
      <p>
        ํ˜„์žฌ ์นด์šดํ„ฐ ๊ฐ’์€ <b>{value}</b>์ž…๋‹ˆ๋‹ค.
      </p>
      <button onClick={() => setValue(value + 1)}>+1</button>
      <button onClick={() => setValue(value - 1)}>-1</button>
    </div>
  );
};
 
export default Counter;
// App.jsx
import React from 'react';
import Counter from './features/8_Hooks/Counter';

const App = () => {
  return <Counter />;
};

export default App;
  • useState๋Š” ์ฝ”๋“œ ์ƒ๋‹จ์—์„œ import ๊ตฌ๋ฌธ์„ ํ†ตํ•ด ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉ.
  • useState ํ•จ์ˆ˜์˜ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ์ƒํƒœ์˜ ๊ธฐ๋ณธ๊ฐ’์„ ๋„ฃ์–ด์คŒ.


โ“๐Ÿค” useState๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ•˜๋ ค๋ฉด?
ํ•˜๋‚˜์˜ useState ํ•จ์ˆ˜๋Š” ํ•˜๋‚˜์˜ ์ƒํƒœ ๊ฐ’๋งŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ!
์ปดํฌ๋„ŒํŠธ์—์„œ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ์ƒํƒœ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ๋ผ๋ฉด???
๐Ÿ‘‰ useState๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ•˜๋ฉด ๋จ!

// Info.jsx
import React, { useState } from 'react';

// 8.1.1 useState๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ•˜๊ธฐ
const Info = () => {
  const [name, setName] = useState('');
  const [nickname, setNickname] = useState('');

  const onChangeName = (e) => {
    setName(e.target.value);
  };

  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };

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

export default Info;
// App.jsx
import React from 'react';
import Info from './features/8_Hooks/Info';

const App = () => {
  return <Info />;
};

export default App;


โ‘ก useEffect

๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” Hook์œผ๋กœ ํด๋ž˜์Šคํ˜• ์ปดํฌ๋„ŒํŠธ์˜ componentDidMount์™€ componentDidUpdate๋ฅผ ํ•ฉ์นœ ํ˜•ํƒœ๋กœ ๋ณด์•„๋„ ๋ฌด๋ฐฉํ•จ.

// Info.jsx
import React, { useState, useEffect } from 'react';

const Info = () => {
  const [name, setName] = useState('');
  const [nickname, setNickname] = useState('');
  useEffect(() => {
    console.log('๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!');
    console.log({
      name,
      nickname,
    });
  });

  const onChangeName = (e) => {
    setName(e.target.value);
  };

  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };

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

export default Info;


โ“๐Ÿค” ๋งˆ์šดํŠธ๋  ๋•Œ๋งŒ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์„ ๋•?
useEffect์—์„œ ์„ค์ •ํ•œ ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™”๋ฉด์— ๋งจ ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ๋งŒ ์‹คํ–‰ํ•˜๊ณ , ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด...

๐Ÿ‘‰ ํ•จ์ˆ˜์˜ ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋น„์–ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ๋„ฃ์–ด ์ฃผ๋ฉด ๋จ.

// Info.jsx -useEffect
useEffect(() => {
    console.log('๋งˆ์šดํŠธ๋  ๋•Œ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.');
  }, []);

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ ๋‚˜ํƒ€๋‚  ๋•Œ๋งŒ ์ฝ˜์†”์— ๋ฌธ๊ตฌ๊ฐ€ ๋‚˜ํƒ€๋‚˜๊ณ , ๊ทธ ์ดํ›„์—๋Š” ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Œ.

โ“๐Ÿค” ํŠน์ • ๊ฐ’์ด ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งŒ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์„ ๋•?

  • ํด๋ž˜์Šคํ˜• ์ปดํฌ๋„ŒํŠธ๋ผ๋ฉด...
    ๐Ÿ‘‰ props ์•ˆ์— ๋“ค์–ด ์žˆ๋Š” value ๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งŒ ํŠน์ • ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•จ.
componentDidUpdate(prevProps, prevState) {
if (prevProps.value !== this.props.value) {
  doSomething();
}
}
  • ์ด๋Ÿฌํ•œ ์ž‘์—…์„ useEffect์—์„œ ํ•ด์•ผ ํ•œ๋‹ค๋ฉด?
    ๐Ÿ‘‰ useEffect์˜ ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ๋˜๋Š” ๋ฐฐ์—ด ์•ˆ์— ๊ฒ€์‚ฌํ•˜๊ณ  ์‹ถ์€ ๊ฐ’์„ ๋„ฃ์–ด ์ฃผ๋ฉด ๋จ.

    ๐Ÿ‘‰ ๋ฐฐ์—ด ์•ˆ์—๋Š” useState๋ฅผ ํ†ตํ•ด ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ๋ฅผ ๋„ฃ์–ด๋„ ๋˜๊ณ , props๋กœ ์ „๋‹ฌ๋ฐ›์€ ๊ฐ’์„ ๋„ฃ์–ด๋„ ๋จ.
// Info.jsx-useEffect
useEffect(() => {
    console.log(name);
  }, [name]);


โœ”๏ธ ๋’ท์ •๋ฆฌํ•˜๊ธฐ
useEffect๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ Œ๋”๋ง๋˜๊ณ  ๋‚œ ์งํ›„๋งˆ๋‹ค ์‹คํ–‰๋˜๋ฉฐ, ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐฐ์—ด์— ๋ฌด์—‡์„ ๋„ฃ๋Š”์ง€์— ๋”ฐ๋ผ ์‹คํ–‰๋˜๋Š” ์กฐ๊ฑด์ด ๋‹ฌ๋ผ์ง.

โ“๐Ÿค” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋˜๊ธฐ ์ „์ด๋‚˜ ์—…๋ฐ์ดํŠธ๋˜๊ธฐ ์ง์ „์— ์–ด๋– ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด?
๐Ÿ‘‰ useEffect์—์„œ ๋’ท์ •๋ฆฌ(cleanup) ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜


// Info.jsx-useEffect
import React, { useState, useEffect } from 'react';

const Info = () => {
  const [name, setName] = useState('');
  const [nickname, setNickname] = useState('');
  
  // 8.2.3 ๋’ท์ •๋ฆฌํ•˜๊ธฐ
  useEffect(() => {
    console.log('effect');
    console.log(name);
    return () => {
      console.log('cleanup');
      console.log(name);
    };
  });

  const onChangeName = (e) => {
    setName(e.target.value);
  };

  const onChangeNickname = (e) => {
    setNickname(e.target.value);
  };

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

export default Info;
// useState๋ฅผ ์‚ฌ์šฉํ•ด App ์ปดํฌ๋„ŒํŠธ์—์„œ 
// Info ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ€์‹œ์„ฑ ๋ฐ”๊ฟœ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ
import React, { useState } from 'react';
import Info from './features/8_Hooks/Info';

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

  return (
    <div>
      <button
        onClick={() => {
          setVisible(!visible);
        }}
      >
        {visible ? '์ˆจ๊ธฐ๊ธฐ' : '๋ณด์ด๊ธฐ'}
      </button>
      <hr />
      {visible && <Info />}
    </div>
  );
};

export default App;

์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ ์ฝ˜์†”์— effect๊ฐ€ ๋‚˜ํƒ€๋‚˜๊ณ , ์‚ฌ๋ผ์งˆ ๋•Œ cleanup์ด ๋‚˜ํƒ€๋‚จ. ์ด์ œ ์ธํ’‹์— ์ด๋ฆ„์„ ์ ์–ด ๋ณด๊ณ  ์ฝ˜์†”์— ์–ด๋–ค ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž!

๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ๋’ท์ •๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ๊ณ„์† ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ. ๊ทธ๋ฆฌ๊ณ  ๋’ท์ •๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋Š” ์—…๋ฐ์ดํŠธ๋˜๊ธฐ ์ง์ „์˜ ๊ฐ’์„ ๋ณด์—ฌ์คŒ.


โ“๐Ÿค” ์˜ค์ง ์–ธ๋งˆ์šดํŠธ๋  ๋•Œ๋งŒ ๋’ท์ •๋ฆฌ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด?
๐Ÿ‘‰ useEffect ํ•จ์ˆ˜์˜ ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋น„์–ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ๋„ฃ์œผ๋ฉด ๋จ.

// Info.jsx-useEffect
useEffect(() => {
    console.log('effect');
    console.log(name);
    return () => {
      console.log('cleanup');
      console.log(name);
    };
  }, []);

โ‘ข useReducer

useState๋ณด๋‹ค ๋” ๋‹ค์–‘ํ•œ ์ปดํฌ๋„ŒํŠธ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ด ์ฃผ๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Hook์ด๋‹ค.

ํ˜„์žฌ ์ƒํƒœ, ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์€ ์•ก์…˜(action) ๊ฐ’์„ ์ „๋‹ฌ๋ฐ›์•„ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋กœ ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜์—์„œ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ๋งŒ๋“ค ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ ๋ถˆ๋ณ€์„ฑ์„ ์ง€์ผœ์ค˜์•ผ ํ•จ.

// Counter.jsx
// 8.3.1 ์นด์šดํ„ฐ ๊ตฌํ˜„ํ•˜๊ธฐ
import React, { useReducer } from 'react';

function reducer(state, action) {
  // action.type์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ž‘์—… ์ˆ˜ํ–‰
  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>
        ํ˜„์žฌ ์นด์šดํ„ฐ ๊ฐ’์€ <b>{state.value}</b>์ž…๋‹ˆ๋‹ค.
      </p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
    </div>
  );
};

export default Counter;

  • ์นด์šดํ„ฐ ๊ตฌํ˜„ํ•˜๊ธฐ
import React, { useReducer } from 'react';
 
function reducer(state, action) {
  // action.type์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ž‘์—… ์ˆ˜ํ–‰
  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>
        ํ˜„์žฌ ์นด์šดํ„ฐ ๊ฐ’์€ <b>{state.value}</b>์ž…๋‹ˆ๋‹ค.
      </p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
    </div>
  );
};
 
export default Counter;
// App.jsx
import React from 'react';
import Counter from './features/8_Hooks/Counter';

const App = () => {
  return <Counter />;
};

export default App;

useReducer์˜ ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๋ฅผ ๋„ฃ๊ณ , ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ํ•ด๋‹น ๋ฆฌ๋“€์„œ์˜ ๊ธฐ๋ณธ๊ฐ’์„ ๋„ฃ์–ด์คŒ.

์ด Hook์„ ์‚ฌ์šฉํ•˜๋ฉด state ๊ฐ’๊ณผ dispatch ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„ ์˜ด. ์—ฌ๊ธฐ์„œ state๋Š” ํ˜„์žฌ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ๋Š” ์ƒํƒœ๊ณ , dispatch๋Š” ์•ก์…˜์„ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ํ•จ์ˆ˜์ž„.

dispatch(action)๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๋กœ, ํ•จ์ˆ˜ ์•ˆ์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์•ก์…˜ ๊ฐ’์„ ๋„ฃ์–ด ์ฃผ๋ฉด ๋ฆฌ๋“€์„œ ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๊ตฌ์กฐ! useReducer๋ฅผ ์‚ฌ์šฉ ์‹œ, ํฐ ์žฅ์ ์€ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ๋กœ์ง์„ ์ปดํฌ๋„ŒํŠธ ๋ฐ”๊นฅ์œผ๋กœ ๋นผ๋‚ผ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ!

  • ์ธํ’‹ ์ƒํƒœ ๊ด€๋ฆฌํ•˜๊ธฐ
    useReducer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Info ์ปดํฌ๋„ŒํŠธ์—์„œ ์ธํ’‹ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด ๋ณด์ž.
    ๊ธฐ์กด์—๋Š” ์ธํ’‹์ด ์—ฌ๋Ÿฌ ๊ฐœ์—ฌ์„œ useState๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉํ–ˆ์Œ.
    useReducer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ์กด์— ํด๋ž˜์Šคํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ input ํƒœ๊ทธ์— name ๊ฐ’์„ ํ• ๋‹นํ•˜๊ณ , e.target.name์„ ์ฐธ์กฐํ•˜์—ฌ setState๋ฅผ ํ•ด ์ค€ ๊ฒƒ๊ณผ ์œ ์‚ฌํ•œ ๋ฐฉ์‹์œผ๋กœ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ.
// Info_input.jsx
// 8.3.2 ์ธํ’‹ ์ƒํƒœ ๊ด€๋ฆฌํ•˜๊ธฐ
import React, { useReducer } from 'react';

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

const Info_input = () => {
  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>
  );
};

export default Info_input;
// App.jsx
import React from 'react';
import Info_input from './features/8_Hooks/Info_input';

const App = () => {
  return <Info_input />;
};

export default App;

useReducer์—์„œ์˜ ์•ก์…˜์€ ๊ทธ ์–ด๋–ค ๊ฐ’๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ! ๊ทธ๋ž˜์„œ ์ด๋ฒˆ์—๋Š” ์ด๋ฒคํŠธ ๊ฐ์ฒด๊ฐ€ ์ง€๋‹ˆ๊ณ  ์žˆ๋Š” e.target ๊ฐ’ ์ž์ฒด๋ฅผ ์•ก์…˜ ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ–ˆ์Œ. ์ด๋Ÿฐ ์‹์œผ๋กœ ์ธํ’‹์„ ๊ด€๋ฆฌํ•˜๋ฉด ์•„๋ฌด๋ฆฌ ์ธํ’‹์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์ ธ๋„ ์ฝ”๋“œ๋ฅผ ์งง๊ณ  ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ ๊ฐ€๋Šฅ!


โ‘ฃ useMemo

useMemo๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—ฐ์‚ฐ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Œ.

// Average.jsx
import React, { useCallback, useMemo, useState } 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 = e => {
      const nextList = list.concat(parseInt(number));
      setList(nextList);
      setNumber('');
    };
   
    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> {getAverage(list)}
        </div>
      </div>
    );
  };
   
  export default Average;

// App.jsx
import React from 'react';
import Average from './features/8_Hooks/Average';  

const App = () => {
  return <Average />;
};

export default App;

์ˆซ์ž๋ฅผ ๋“ฑ๋กํ•  ๋•Œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ธํ’‹ ๋‚ด์šฉ์ด ์ˆ˜์ •๋  ๋•Œ๋„ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  getAverage ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋จ... ์ธํ’‹ ๋‚ด์šฉ์ด ๋ฐ”๋€” ๋•Œ๋Š” ํ‰๊ท ๊ฐ’์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•  ํ•„์š”๋Š” ์—†์Œ! ์ด๋ ‡๊ฒŒ ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์€ ๋‚ญ๋น„!!!

useMemo Hook์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Œ! ๋ Œ๋”๋งํ•˜๋Š” ๊ณผ์ •์—์„œ ํŠน์ • ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ ์—ฐ์‚ฐ์„ ์‹คํ–‰ํ•˜๊ณ , ์›ํ•˜๋Š” ๊ฐ’์ด ๋ฐ”๋€Œ์ง€ ์•Š์•˜๋‹ค๋ฉด ์ด์ „์— ์—ฐ์‚ฐํ–ˆ๋˜ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•ด๋ณด์ž.

// Average.jsx
// ========================= ์œ„ ์ฝ”๋“œ๋ฅผ useMemo Hook์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ์ตœ์ ํ™”ํ•˜๊ธฐ
// ๋ Œ๋”๋งํ•˜๋Š” ๊ณผ์ •์—์„œ ํŠน์ • ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ ์—ฐ์‚ฐ์„ ์‹คํ–‰ํ•˜๊ณ ,
// ์›ํ•˜๋Š” ๊ฐ’์ด ๋ฐ”๋€Œ์ง€ ์•Š์•˜๋‹ค๋ฉด
// ์ด์ „์— ์—ฐ์‚ฐํ–ˆ๋˜ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹
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('');
  };

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

export default Average;

useMemo๋ฅผ ํ†ตํ•œ ์—ฐ์‚ฐ ์ตœ์ ํ™”์‹œํ‚ค๋ฉด list ๋ฐฐ์—ด์˜ ๋‚ด์šฉ์ด ๋ฐ”๋€” ๋•Œ๋งŒ getAverage ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.


โ‘ค useCallback

๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•จ.(useMemo์™€ ์ƒ๋‹นํžˆ ๋น„์Šท) ์ด Hook์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ํ•„์š”ํ•  ๋•Œ๋งŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Œ.

๋ฐฉ๊ธˆ ๊ตฌํ˜„ํ•œ Average ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด๋ฉด onChange์™€ onInsert๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•จ. ์ด๋ ‡๊ฒŒ ์„ ์–ธํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋  ๋•Œ๋งˆ๋‹ค ์ด ํ•จ์ˆ˜๋“ค์ด ์ƒˆ๋กœ ์ƒ์„ฑ๋จ. ๋Œ€๋ถ€๋ถ„ ๋ฌธ์ œ์—†์ง€๋งŒ, ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง์ด ์ž์ฃผ ๋ฐœ์ƒํ•˜๊ฑฐ๋‚˜ ๋ Œ๋”๋งํ•ด์•ผ ํ•  ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์ด ๋ถ€๋ถ„์„ ์ตœ์ ํ™”ํ•ด ์ฃผ๋Š” ๊ฒƒ์ด ์ข‹์Œ.

useCallback์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•œ๋ฒˆ ์ตœ์ ํ™”ํ•ด๋ณด์ž!

// Average.jsx
import React, { useState, useMemo, useCallback } 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 = useCallback(e => {
    setNumber(e.target.value);
}, []); // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ๋งŒ ํ•จ์ˆ˜ ์ƒ์„ฑ
const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  }, [number, list]); // number ํ˜น์€ 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>
  );
};
 
export default Average;

useCallback์˜ ์ฒซ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ์ƒ์„ฑํ•˜๊ณ  ์‹ถ์€ ํ•จ์ˆ˜๋ฅผ ๋„ฃ๊ณ , ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ๋ฐฐ์—ด์„ ๋„ฃ๋Š”๋‹ค. ์ด ๋ฐฐ์—ด์—๋Š” ์–ด๋–ค ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ ํ•จ์ˆ˜๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ช…์‹œํ•ด์•ผ ํ•จ!

  • onChange์ฒ˜๋Ÿผ ๋น„์–ด ์žˆ๋Š” ๋ฐฐ์—ด์„ ๋„ฃ๊ฒŒ ๋˜๋ฉด?
    ๐Ÿ‘‰ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋  ๋•Œ ๋‹จ ํ•œ ๋ฒˆ๋งŒ ํ•จ์ˆ˜๊ฐ€ ์ƒ์„ฑ๋˜๋ฉฐ, onInsert์ฒ˜๋Ÿผ ๋ฐฐ์—ด ์•ˆ์— number์™€ list๋ฅผ ๋„ฃ๊ฒŒ ๋˜๋ฉด ์ธํ’‹ ๋‚ด์šฉ์ด ๋ฐ”๋€Œ๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ํ•ญ๋ชฉ์ด ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ํ•จ์ˆ˜๊ฐ€ ์ƒ์„ฑ๋จ.

  • ํ•จ์ˆ˜ ๋‚ด๋ถ€์—์„œ ์ƒํƒœ ๊ฐ’์— ์˜์กดํ•ด์•ผ ํ•  ๋•?
    ๐Ÿ‘‰ ๊ทธ ๊ฐ’์„ ๋ฐ˜๋“œ์‹œ ๋‘ ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ ์•ˆ์— ํฌํ•จ์‹œ์ผœ์ค˜์•ผ ํ•จ.
    (์˜ˆ๋ฅผ ๋“ค์–ด onChange์˜ ๊ฒฝ์šฐ ๊ธฐ์กด์˜ ๊ฐ’์„ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์„ค์ •๋งŒ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฐ์—ด์ด ๋น„์–ด ์žˆ์–ด๋„ ์ƒ๊ด€์—†์ง€๋งŒ, onInsert๋Š” ๊ธฐ์กด์˜ number์™€ list๋ฅผ ์กฐํšŒํ•ด์„œ nextList๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฐ์—ด ์•ˆ์— number์™€ list๋ฅผ ๊ผญ ๋„ฃ์–ด์ค˜์•ผ ํ•จ.)

  • ์ฐธ๊ณ ๋กœ ๋‹ค์Œ ๋‘ ์ฝ”๋“œ๋Š” ์™„์ „ํžˆ ๋˜‘๊ฐ™์€ ์ฝ”๋“œ!
    useCallback์€ ๊ฒฐ๊ตญ useMemo๋กœ ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ๋” ํŽธํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Hook. ์ˆซ์ž, ๋ฌธ์ž์—ด, ๊ฐ์ฒด์ฒ˜๋Ÿผ ์ผ๋ฐ˜ ๊ฐ’์„ ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด useMemo๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ•จ์ˆ˜๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋ ค๋ฉด useCallback์„ ์‚ฌ์šฉํ•˜๋ฉด ๋จ.

// ์˜ˆ์‹œ ์ฝ”๋“œ
useCallback(() => {
  console.log('hello world!');
}, [])
 
useMemo(() => {
  const fn = () => {
    console.log('hello world!');
  };
  return fn;
}, [])

โ‘ฅ useRef

ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ ref๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด ์คŒ.
useRef๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ref๋ฅผ ์„ค์ •ํ•˜๋ฉด useRef๋ฅผ ํ†ตํ•ด ๋งŒ๋“  ๊ฐ์ฒด ์•ˆ์˜ current ๊ฐ’์ด ์‹ค์ œ ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ๊ฐ€๋ฆฌํ‚ด.

โ“๐Ÿค” ๋ฆฌ์•กํŠธ์—์„œ ref๋ž€ ๋ฌด์—‡์ธ๊ฐ€?
๐Ÿ‘‰ ref๋Š” React์—์„œ ํŠน์ • DOM ์š”์†Œ๋‚˜ React ์ปดํฌ๋„ŒํŠธ์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” "์ฐธ์กฐ(reference)"์ด๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ React๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ƒํƒœ๋‚˜ props๋กœ ๊ด€๋ฆฌํ•˜๊ณ , DOM ์š”์†Œ๋ฅผ ์ง์ ‘ ์กฐ์ž‘ํ•˜์ง€ ์•Š๋„๋ก ์„ค๊ณ„๋์œผ๋‚˜ ํŠน์ • ์ƒํ™ฉ์—์„œ๋Š” DOM์— ์ง์ ‘ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ React์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๋Š” ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ†ตํ•ฉํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ๋Š”๋ฐ ์ด๋Ÿฐ ๊ฒฝ์šฐ์— ref๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

// Average.jsx
import React, { useState, useMemo, useCallback, useRef } 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 inputEl = useRef(null);
 
  const onChange = useCallback(e => {
    setNumber(e.target.value);
  }, []); // ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ๋งŒ ํ•จ์ˆ˜ ์ƒ์„ฑ
const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEl.current.focus();
  }, [number, list]); // number ํ˜น์€ list๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ ํ•จ์ˆ˜ ์ƒ์„ฑ
 
  const avg = useMemo(() => getAverage(list), [list]);
 
  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl} />
      <button onClick={onInsert}>๋“ฑ๋ก</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>ํ‰๊ท ๊ฐ’:</b> {avg}
      </div>
    </div>
  );
};
 
export default Average;

โœ”๏ธ ๋กœ์ปฌ ๋ณ€์ˆ˜ ์‚ฌ์šฉํ•˜๊ธฐ
์ถ”๊ฐ€๋กœ ์ปดํฌ๋„ŒํŠธ ๋กœ์ปฌ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ๋„ useRef๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Œ. ์—ฌ๊ธฐ์„œ ๋กœ์ปฌ ๋ณ€์ˆ˜๋ž€, ๋ Œ๋”๋ง๊ณผ ์ƒ๊ด€์—†์ด ๋ฐ”๋€” ์ˆ˜ ์žˆ๋Š” ๊ฐ’์„ ์˜๋ฏธํ•จ.

  • ํด๋ž˜์Šค ํ˜•ํƒœ๋กœ ์ž‘์„ฑ๋œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฝ์šฐ์—๋Š” ๋กœ์ปฌ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Œ.
// ์˜ˆ์‹œ ์ฝ”๋“œ
import React, { Component } from 'react';
 
class MyComponent extends Component {
  id = 1
  setId = (n) => {
    this.id = n;
  }
  printId = () => {
    console.log(this.id);
  }
  render() {
    return (
      <div>
        MyComponent
      </div>
    );
  }
}
 
export default MyComponent;

ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘์„ฑํ•œ๋‹ค๋ฉด?

// ์˜ˆ์‹œ ์ฝ”๋“œ
import React, { useRef } from 'react';

const RefSample = () => {
 const id = useRef(1);
 const setId = (n) => {
   id.current = n;
 }
 const printId = () => {
   console.log(id.current);
 }
 return (
   <div>
     refsample
   </div>
 );
};

export default RefSample;

ref ์•ˆ์˜ ๊ฐ’์ด ๋ฐ”๋€Œ์–ด๋„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์—๋Š” ์ฃผ์˜!
๋ Œ๋”๋ง๊ณผ ๊ด€๋ จ๋˜์ง€ ์•Š์€ ๊ฐ’์„ ๊ด€๋ฆฌํ•  ๋•Œ๋งŒ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•จ.


profile
ํ˜ธ๋–ก ์‹ ๋ฌธ์ง€์—์„œ ๊ฐœ๋ฐœ์ž๋กœ ํ™˜์ƒ

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