React

React Test ์ฝ”๋“œ ์ž‘์„ฑ

๐Ÿ’ก ๊ฐœ์š” ๋ชจ๋“  ๊ฐœ๋ฐœ์ž๋“ค์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์˜ ์ค‘์š”์„ฑ์— ๋Œ€ํ•ด์„œ ์•Œ์ง€๋งŒ, ์‰ฝ์‚ฌ๋ฆฌ ์‹ค๋ฌด์—์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๊ฐœ๋ฐœํ•˜๊ธฐ๋ž€ ์‰ฌ์šด ์ผ์ด ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์— ๊ณต๊ฐํ•˜์‹ค ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ๋‚˜ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ๋Š” ์‹œ๊ฐ์ ์ธ ๋ถ€๋ถ„์ด ๋งŽ๋‹ค ๋ณด๋‹ˆ ์•„๋ฌด๋ž˜๋„ ๋ˆˆ์œผ๋กœ ์ง์ ‘ ๊ฒ€์ฆํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์€๋ฐ์š”. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ดโ€ฆ

2022. 06. 06์ฝ๊ธฐ 12๋ถ„6638์ž

1. ๊ธฐ์ˆ ์Šคํƒ

  • react 18
  • react-router-dom v6
  • react-query
  • testing-library

๊ธฐ์ˆ  ์Šคํƒ์˜ ๊ฒฝ์šฐ React 18๊ณผ React-router-dom v6๊ณผ ๊ฐ™์ด ์ตœ์‹  ๋ฒ„์ „์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์˜€๊ณ , API ํ˜ธ์ถœ์„ ์œ„ํ•œ Axios์™€ ์„œ๋ฒ„ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ React-query๋ฅผ ์ด์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. React ๊ณต์‹ ํ™ˆํŽ˜์ด์ง€์—๋„ ๊ฒŒ์‹œ๋˜์–ด ์žˆ๋“ฏ Facebook์—์„œ๋Š” React Testing Library ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•˜๋Š” ๋งŒํผ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์€ Testing-library๋ฅผ ์ด์šฉํ•˜์—ฌ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

2. React Testing Library

React Testing Library(RTL)๋Š” ๊ธฐ์กด์˜ Enzyme์™€ ๊ฐ™์€ ๋„๊ตฌ์˜ ๊ตฌํ˜„ ๊ธฐ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•๋ก ์˜ ๋‹จ์ ์„ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•˜์˜€๊ณ , ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•๋ก ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ €, ๊ตฌํ˜„ ๊ธฐ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•๋ก  ์ด๋ž€, ์ฃผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€์— ๋Œ€ํ•ด์„œ ์ดˆ์ ์„ ๋‘์–ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

<div class="title">Hello</div>

์˜ˆ๋ฅผ ๋“ค์–ด ์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ์ปดํฌ๋„ŒํŠธ์— ์–ด๋–ค prop์ด ๋„˜์–ด๊ฐ€๊ณ  ํ˜„์žฌ state๊ฐ€ ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€, div ํƒœ๊ทธ๊ฐ€ ์“ฐ์˜€๊ณ  title์ด๋ผ๋Š” ํด๋ž˜์Šค๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€ ๋“ฑ์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

์ด์— ๋ฐ˜ํ•ด ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•๋ก ์˜ ๊ฒฝ์šฐ, ์„ธ๋ถ€์ ์ธ ๊ตฌํ˜„ ์‚ฌํ•ญ๋ณด๋‹ค๋Š” ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ์œ ์‚ฌํ•œ ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” div ํƒœ๊ทธ๊ฐ€ ์“ฐ์˜€๋Š”์ง€, title์ด๋ผ๋Š” ํด๋ž˜์Šค๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€๋ณด๋‹ค๋Š” ํ™”๋ฉด์— ์˜ฌ๋ฐ”๋ฅธ ์ฝ˜ํ…์ธ  ๋‚ด์šฉ์ด ํ‘œ๊ธฐ๋˜๋Š”๊ฐ€๋ผ๋Š” ๋ถ€๋ถ„์ด ๋” ์ค‘์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, RTL์€ Hello๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ํ™”๋ฉด์— ์žˆ๋Š”์ง€๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด์™€ ๊ฐ™์ด ๊ตฌํ˜„๋ณด๋‹ค ๊ธฐ๋Šฅ์— ์ดˆ์ ์„ ๋งž์ถ˜ ํ…Œ์ŠคํŠธ ๋ฐฉ์‹์€ ์‹ ๋ขฐ๋„๋ฅผ ๋†’์ž„๊ณผ ๋™์‹œ์— ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง ์‹œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • ๋นˆ๋„๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3. ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ

๋จผ์ €, ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋ฅผ ํ†ตํ•ด RTL์„ ํ™œ์šฉํ•ด์„œ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

const Home = () => {
  return (
    <section>
      <h1>Hello Word</h1>
      <p>์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž MAENG์ž…๋‹ˆ๋‹ค.</p>
    </section>
  )
}

์œ„์™€ ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•ด๋ณด๋ฉด ๋จผ์ € Hello world๋ผ๋Š” ๋ฌธ๊ตฌ์™€ ์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž MAENG์ž…๋‹ˆ๋‹ค.๋ผ๋Š” ๋ฌธ๊ตฌ๊ฐ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋˜์—ˆ์„ ๋•Œ ํ™”๋ฉด์— ๋…ธ์ถœ๋˜์–ด์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

RTL์„ ๊ฐ€์ง€๊ณ  ํ•ด๋‹น ๋‚ด์šฉ๋“ค์ด ์ œ๋Œ€๋กœ ๋…ธ์ถœ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

describe('Home', () => {
  it('renders Home', () => {
    const { getByText } = render(<Home />)
    expect(getByText('Hello world')).toBeInTheDocument()
    expect(getByText('์•ˆ๋…•ํ•˜์„ธ์š”. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž MAENG์ž…๋‹ˆ๋‹ค.')).toBeInTheDocument()
  })
})

4. ์œ ์ € ์ธํ„ฐ๋ž™์…˜ ํ…Œ์ŠคํŠธ

์ด์ œ ๋ฒ„ํŠผ์ด๋‚˜ ์ž…๋ ฅ ํ•„๋“œ ๋“ฑ ์œ ์ € ์ธํ„ฐ๋ž™์…˜์ด ์ด๋ฃจ์–ด์ง€๋Š” ๋ถ€๋ถ„์€ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์‚ดํŽด๋ณผ ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค.

const Button = ({ onClick }) => {
  return <button onClick={onClick}>๋ฒ„ํŠผ</button>
}

์œ„์™€ ๊ฐ™์€ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•ด๋ณผ ๋•Œ "๋ฒ„ํŠผ"์ด๋ผ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ฐ€์ง„ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด onClick ํ•จ์ˆ˜๊ฐ€ 1๋ฒˆ ์‹คํ–‰๋  ๊ฒƒ์„ ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์œ„์˜ ๋‚ด์šฉ์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

describe('Button', () => {
  it('renders Button', () => {
    const onClick = jest.fn() // ๊ฐ€์งœ ํ•จ์ˆ˜
    const { getByText } = render(<Button onClick={onClick} />)
    expect(getByText('๋ฒ„ํŠผ')).toBeInTheDocument()
    fireEvent.click(button)
    expect(onClick).toBeCalledTimes(1)
  })
})

์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด jest.fn()์„ ์‚ฌ์šฉํ•˜์—ฌ onClick ํ•จ์ˆ˜๋ฅผ ๋ชจํ‚นํ•˜๋Š”๋ฐ, ๋ชจํ‚น์— ๋Œ€ํ•ด์„œ๋Š” ํ•˜๋‹จ์— ๋”ฐ๋กœ ์ •๋ฆฌํ•˜์—ฌ ์„ค๋ช…ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ์—๋Š” ์ž…๋ ฅ ํ•„๋“œ์˜ ๊ฒฝ์šฐ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

const Form = () => {
  const [text, setText] = useState('')
 
  return (
    <form>
      <input
        type="text"
        placeholder="ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
        value={text}
        onChange={({ target: { value } }) => setText(value)}
      />
      <button disabled={!text}>์ œ์ถœ</button>
    </form>
  )
}

์œ„์™€ ๊ฐ™์€ ํผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•ด๋ณด๋ฉด "ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."๋ผ๋Š” placeholder๊ฐ€ ์žˆ๋Š” ์ž…๋ ฅ ํ•„๋“œ์— ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ํ•ด๋‹น ์ž…๋ ฅ ํ•„๋“œ์˜ ๊ฐ’์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์œผ๋กœ ๋ณ€ํ•  ๊ฒƒ์ด๊ณ , "์ œ์ถœ"์ด๋ผ๋Š” ํ…์ŠคํŠธ๋ฅผ ์ง€๋‹Œ ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™”๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์œ„์˜ ๋‚ด์šฉ์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

describe('Form', () => {
  it('renders Form', () => {
    const { getByText, getByPlaceholderText } = render(<Form />)
 
    const input = getByPlaceholderText('ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.')
    const button = getByText('์ œ์ถœ')
 
    expect(button).toBeDisabled()
    fireEvent.change(input, { target: { value: 'Maeng Input Test' } })
    expect(input.value).toBe('Maeng Input Test')
    expect(button).toBeEnabled()
  })
})

5. Custom Hook ํ…Œ์ŠคํŠธ

์š”์ฆ˜ React์—์„œ๋Š” ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์™€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ›…์„ ์ด์šฉํ•ด์„œ ๊ฐ„๋‹จํ•˜๋ฉด์„œ๋„ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ธ ๋ฐฉ์‹์œผ๋กœ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ํ•˜๊ธฐ์—, ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ˜๋ณต์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๊ฑฐ๋‚˜ ๋ณต์žกํ•œ ๊ณ„์‚ฐ์ด ํฌํ•จ๋œ ํ›…๋“ค์€ ๋ณ„๋„์˜ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ React๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋‹จ์ˆœ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง๋ฟ ์•„๋‹ˆ๋ผ ์ด๋Ÿฌํ•œ ํ›…๋“ค์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ์ด๋ฃจ์–ด์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ ์ปค์Šคํ…€ ํ›…์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์ด Hook ํ…Œ์ŠคํŠธ
  2. Custom Hook์„ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด ํ…Œ์ŠคํŠธ
  3. @testing-library/react-hooks์˜ renderHook์„ ์‚ฌ์šฉ

์ €๋Š” ์ด ์ค‘์—์„œ @testing-library/react-hooks๋ฅผ ์ด์šฉํ•ด์„œ ์ปค์Šคํ…€ ํ›…์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

@testing-library/react-hooks ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋ณ„๋„์˜ React Component์˜ ๋„์›€ ์—†์ด๋„ React Hook์„ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์„ค๋ช…๋“œ๋ฆฌ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

const useInput = (initialValue = null) => {
  const [value, setValue] = useState(initialValue)
  const handler = useCallback((e) => {
    setValue(e.target.value)
  }, [])
  return [value, handler]
}

์œ„์˜ useInput์ด๋ผ๋Š” ์ปค์Šคํ…€ ํ›…์€ ์ž…๋ ฅ ๊ฐ’์„ ๋‹ด์€ value ์ƒํƒœ๊ฐ’๊ณผ ๊ทธ ์ƒํƒœ๊ฐ’์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” handler๋ฅผ ๋ฐฐ์—ด์— ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ์ƒํƒœ๊ฐ’์„ initialValue ์ธ์ž๋ฅผ ํ†ตํ•ด ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธ์ž๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ null์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Custom Hook ํ…Œ์ŠคํŠธ์˜ ๊ฒฝ์šฐ, ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ง€๋Š” ๋ถ€๋ถ„์ด ์•„๋‹ˆ๋‹ค ๋ณด๋‹ˆ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ์™€๋Š” ์กฐ๊ธˆ ๋‹ค๋ฅด๊ฒŒ, ์‚ฌ์šฉ์ž ์ž…์žฅ์ด ์•„๋‹Œ Jest์—์„œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋“ฏ ๋ Œ๋”๋ง์ด ์•„๋‹Œ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋”ฐ๋ฅธ ๊ฒฐ๊ณผ๊ฐ’, ์ฆ‰ ๋™์ผํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•ด์„œ ๋™์ผํ•œ ๊ฒฐ๊ณผ๊ฐ’์„ ๋„์ถœํ•˜๋Š”์ง€๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ useInput ํ›…์„ ์ธ์ž ์—†์ด ํ˜ธ์ถœํ–ˆ์„ ๋•Œ value๋Š” null์ธ ์ƒํƒœ๊ฐ’์„ ์ง€๋‹ˆ๊ณ , handler ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๊ฒฝ์šฐ value์˜ ์ƒํƒœ๊ฐ’์€ handler์˜ ์ธ์ž๋กœ ๋“ค์–ด์˜จ ๊ฐ’์— ์˜ํ•ด ๋ณ€ํ™”๊ฐ€ ์ด๋ฃจ์–ด์งˆ ๊ฒƒ์ด๋ผ๊ณ  ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

describe('useInput', () => {
  it('update value when handler is called', () => {
    const { result } = renderHook(() => useInput())
    expect(result.current[0]).toBe(null)
    act(() => result.current[1]({ target: { value: 'Maeng Input Test' } }))
    expect(result.current[0]).toBe('Maeng Input Test')
  })
})

renderHook() ํ•จ์ˆ˜์— React Hook ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ธ์ž๋กœ ๋„˜๊ธฐ๋ฉด result ์†์„ฑ์„ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด result ๊ฐ์ฒด๋Š” current ์†์„ฑ์„ ๊ฐ–๋Š”๋ฐ, ์ด ์†์„ฑ์„ ํ†ตํ•ด์„œ ํ•ด๋‹น React Hook ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์— ์ง์ ‘ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6. API ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ

์ด์ œ API ํ˜ธ์ถœ์˜ ๊ฒฝ์šฐ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

API ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ์˜ ๊ฒฝ์šฐ๋„ Custom Hook ํ…Œ์ŠคํŠธ์™€ ๋น„์Šทํ•˜๊ฒŒ, ํ•ด๋‹น ํ›…์„ ํ˜ธ์ถœํ•˜์˜€์„ ๋•Œ ์„œ๋ฒ„๋กœ API ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์˜ฌ๋ฐ”๋ฅธ ์‘๋‹ต์„ ๋ฐ›๋Š”์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ €์˜ ๊ฒฝ์šฐ, API ํ˜ธ์ถœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ Axios๋ฅผ ์‚ฌ์šฉํ•˜์˜€๊ณ , ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ React-query๋ฅผ ์ด์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ํ•ด๋‹น ํฌ์ŠคํŠธ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ์— ๋Œ€ํ•œ ๊ธ€์ด๋ฏ€๋กœ Axios๋‚˜ React-query์— ๋Œ€ํ•œ ์„ค๋ช…์€ ์ƒ๋žตํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋จผ์ €, React-query๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฐ„๋‹จํ•œ ์ปค์Šคํ…€ ํ›…์„ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

const useFetchData = () => {
  const { data, isLoading, error } = useQuery('/data', async () => {
    const response = await API.get(`/data`)
    return response.data
  })
 
  return { data, isLoading, error }
}

useFetchData ํ›…์€ API๋ฅผ ์š”์ฒญํ•˜๊ณ  ์‘๋‹ต๊ฐ’์„ data์— ๋‹ด์•„ { data, isLoading, error } ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ useFetchData ํ›…์„ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// ๋ฐ์ดํ„ฐ ์š”์ฒญ ์™„๋ฃŒ ์ „
{
	data: undefined,
	isLoading: true,
	error: null
}
 
// ์ •์ƒ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์š”์ฒญ ์™„๋ฃŒ
{
	data: { name: 'Maeng' },
	isLoading: false,
	error: null
}

์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

describe('useFetchData', () => {
  it('fetch data', async () => {
    const { result, waitFor } = renderHook(() => useFetchData(), {
      wrapper: createQueryWrapper(),
    })
 
    await waitFor(() => !result.current.isLoading)
    expect(result.current.data?.name).not.toBe(undefined)
  })
})

React-query๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ณ„๋„์˜ Wrapper ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ createQueryWrapper๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด React-query๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Wrapper ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

// React-query๋ฅผ ์œ„ํ•œ Wrapper ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ
const createQueryWrapper = () => {
  const testQueryClient = createTestQueryClient()
  const Wrapper = ({ children }: { children }) => (
    <QueryClientProvider client={testQueryClient}>{children}</QueryClientProvider>
  )
  return Wrapper
}

ํ•˜์ง€๋งŒ ์œ„์ฒ˜๋Ÿผ API ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ช‡ ๊ฐ€์ง€ ๋ถˆํŽธ์‚ฌํ•ญ์„ ๊ฒช๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

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

๋‘ ๋ฒˆ์งธ๋Š” API๊ฐ€ DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์œ„์—์„œ๋Š” ๋‹จ์ˆœํžˆ data๊ฐ€ undefined๊ฐ€ ์•„๋‹˜์„ ํ…Œ์ŠคํŠธํ•˜์˜€์ง€๋งŒ, ์ข€ ๋” ์ƒ์„ธํ•˜๊ฒŒ data ๊ฐ’์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ๊ฒฝ์šฐ DB๊ฐ€ ์ˆ˜์ •๋จ์— ๋”ฐ๋ผ ์‘๋‹ต๊ฐ’์ด ๋งค๋ฒˆ ๋ฐ”๋€” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋˜ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์„ธ ๋ฒˆ์งธ๋Š” CORS ์ฒ˜๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ œ๊ฐ€ ๊ตฌํ˜„ํ•œ ์„œ๋ฒ„์˜ ๊ฒฝ์šฐ Access-Control-Allow-Origin์—, React์˜ ๊ฒฝ์šฐ ํฌํŠธ ๋ฒˆํ˜ธ๋กœ 3000๋ฒˆ์„ ์‚ฌ์šฉํ•˜๊ธฐ์— 3000๋ฒˆ๋งŒ ํ—ˆ์šฉํ•ด์ฃผ์—ˆ๋”๋‹ˆ CORS ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ํ™•์ธํ•ด ๋ณธ ๊ฒฐ๊ณผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ API ํ˜ธ์ถœ ์‹œ 80 ํฌํŠธ๋ฅผ ์ด์šฉํ•ด์„œ ์š”์ฒญํ•˜๊ณ  ์žˆ์–ด์„œ, ๋งŒ์•ฝ API ์„œ๋ฒ„์—์„œ 3000๋ฒˆ ํฌํŠธ๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด 80 ํฌํŠธ๋„ ํ—ˆ์šฉํ•˜๋„๋ก ์ถ”๊ฐ€์ ์ธ ์ž‘์—…์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๋ฉด ์ข‹์„๊นŒ ๊ณ ๋ฏผํ•˜๋˜ ์ค‘ axios-mock-adapter์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

axios-mock-adapter๋ฅผ ํ™œ์šฉํ•˜๋ฉด API ์š”์ฒญ๊ณผ ์‘๋‹ต์„ Mockingํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ API ์š”์ฒญ๊ณผ ์‘๋‹ต๊ฐ’์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด์—์„œ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ axios-mock-adapter๋ฅผ ํ™œ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์žฌ์ž‘์„ฑํ•ด๋ณธ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.

describe('useFetchData', () => {
  it('fetch data', async () => {
    mock.onGet('/data').reply(200, { name: 'Maeng' })
 
    const { result, waitFor } = renderHook(() => useGetBankList(), {
      wrapper: createQueryWrapper(),
    })
 
    await waitFor(() => !result.current.isLoading)
    expect(result.current.data.name).toBe('Maeng')
  })
})

์ด์ฒ˜๋Ÿผ axios-mock-adapter๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ง์ ‘ API ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š๊ณ ๋„ ์˜ˆ์ƒ๋˜๋Š” ์‘๋‹ต ์ƒํƒœ์™€ ๊ฐ’์„ ๋‚ด๋ ค์ฃผ๋„๋ก API๋ฅผ ๋ชจํ‚นํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

7. ๋ผ์šฐํŒ… ํ…Œ์ŠคํŠธ

๋‹ค์Œ์œผ๋กœ, ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” ๋ฆฌ์•กํŠธ ํ›…์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ผ์šฐํŒ…์€ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋Œ€๊ฐœ React์—์„œ๋Š” React-router-dom ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ผ์šฐํŒ… ์„ค์ •์„ ํ•˜๋Š”๋ฐ์š”. ์ตœ๊ทผ์— v6๊ฐ€ ๋ฆด๋ฆฌ์ฆˆ๋˜์–ด ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋จผ์ €, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ผ์šฐํŒ…์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

const Button = () => {
  const navigate = useNavigate()
 
  const onClick = () => {
    navigate('/')
  }
  return <button onClick={onClick}>๋ฒ„ํŠผ</button>
}

์ž, ๋ผ์šฐํŒ…์ด ๋ฐœ์ƒํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ์•ž์„œ ๋ง์”€๋“œ๋ ธ๋‹ค์‹œํ”ผ ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ด ์ปดํฌ๋„ŒํŠธ์—์„œ ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” "๋ฒ„ํŠผ"์ด๋ผ๋Š” ํ…์ŠคํŠธ๋ฅผ ์ง€๋‹Œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด "/"๋กœ ๋ผ์šฐํŒ…๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ์— ์•ž์„œ react-router-dom์˜ ๋™์ž‘์— ๋Œ€ํ•ด์„œ ์ƒ๊ฐํ•ด ๋ณด์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ธˆ ์ „์— react-query ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ๋•Œ Wrapper ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•˜์˜€๋“ฏ์ด, react-router-dom์ด ๋™์ž‘ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋„ Wrapper ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ React-router-dom์„ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ์‹ธ ๋ Œ๋”๋งํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” renderWithRouter ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

// React-router-dom์„ ์œ„ํ•œ Wrapping
const renderWithRouter = (children) => {
  return {
    ...render(<MemoryRouter>{children}</MemoryRouter>
    )
  };

๋ณดํ†ต React์—์„œ๋Š” React-router-dom์—์„œ ์ œ๊ณตํ•˜๋Š” BrowserRouter ๋˜๋Š” HashRouter๋ฅผ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ฐ์‹ธ ๋ผ์šฐํŒ…์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ, MemoryRouter๋Š” <BrowserHistory> ๋ฐ <HashHistory>์™€๋Š” ๋‹ฌ๋ฆฌ ๋ธŒ๋ผ์šฐ์ €์˜ ๊ธฐ๋ก ์Šคํƒ๊ณผ ๊ฐ™์€ ์™ธ๋ถ€ ์†Œ์Šค์— ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ…Œ์ŠคํŠธ์™€ ๊ฐ™์ด ๊ธฐ๋ก ์Šคํƒ์„ ์™„์ „ํžˆ ์ œ์–ดํ•ด์•ผ ํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์— ์ด์ƒ์ ์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ ๋ผ์šฐํŒ…์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์ค€๋น„๋ฅผ ๋งˆ์ณค์œผ๋‹ˆ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// useNavigate ๋ชจํ‚น
const mockedNavigator = jest.fn()
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => mockedNavigator,
}))
 
describe('Button', () => {
  it('renders Button', () => {
    const { getByText } = renderWithRouter(<Button />)
    expect(getByText('๋ฒ„ํŠผ')).toBeInTheDocument()
    fireEvent.click(button)
    expect(mockedNavigator).toBeCalledTimes(1)
  })
})

ํ•ด๋‹น ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋ฉด ์ •์ƒ์ ์œผ๋กœ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ €๋Š” ์•„์ง ๋งŒ์กฑํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

์ •ํ™•ํžˆ "/"๋กœ ๋ผ์šฐํŒ…๋˜๋Š”์ง€๊นŒ์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์—ฌ ์ข€ ๋” ์™„๋ฒฝํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ตฌ์„ฑ ์š”์†Œ๊ฐ€ ์ผ๋ถ€ ์ž‘์—…์—์„œ ๊ฒฝ๋กœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ MemoryRouter๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ๋Š” history์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— MemoryRouter๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” ์ถฉ๋ถ„ํ•˜์ง€ ์•Š๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , history์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” HistoryRouter๋ฅผ ํ™œ์šฉํ•˜๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ HistoryRouter๋ฅผ ํ™œ์šฉํ•ด์„œ renderWithRouter๋ฅผ ์žฌ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

const renderWithRouter = (renderComponent, route) => {
  const history = createMemoryHistory();
 
  if (route) {
    history.push(route);
  }
 
  return {
    ...render(
      <HistoryRouter history={history}>
        <Routes>{renderComponent()}</Routes>
      </HistoryRouter>
    ),
    history,
  };

์ด์ œ๋Š” history์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์— ๋Œ€ํ•ด ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์‹œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

describe('Button', () => {
  it('renders Button', () => {
    const { getByText, history } = renderWithRouter(
      () => (
        <>
          <Route path="/test" element={<Button />} />
          <Route path="/" element={<div />} />
        </>
      ),
      '/test',
    )
    expect(getByText('๋ฒ„ํŠผ')).toBeInTheDocument()
    fireEvent.click(button)
    await waitFor(() => {
      expect(history.location.pathname).toEqual('/')
    })
  })
})

์ด์ „ ์ฝ”๋“œ์™€ ๋‹ฌ๋ฆฌ useNavigate๋ฅผ ๋ชจํ‚นํ•˜์ง€ ์•Š์•„๋„ ๋˜๋ฉฐ, history ๊ฐ์ฒด์— ์•ก์„ธ์Šคํ•˜์—ฌ ์ •ํ™•ํ•˜๊ฒŒ "/"๋กœ ๋ผ์šฐํŒ…๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

8. ์‹ฌํ™” ํ…Œ์ŠคํŠธ

์ด์ œ๋Š” ์œ„์—์„œ ํ•™์Šตํ•œ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋ณตํ•ฉ์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์ด๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ ์ƒ์„ธ ์ฝ”๋“œ๋Š” ์ƒ๋žตํ•˜๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์œ„์ฃผ๋กœ ์ž‘์„ฑํ•˜๊ณ  ์„ค๋ช…๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

const UpdatePage = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const { data, error, isLoading, isError } = useFetchData(id)
  const { mutateAsync, isLoading: isMutating } = useUpdateData()
 
  const onFormSubmit = async (formData) => {
    await mutateAsync({ ...formData, id })
    navigate('/')
  }
 
  if (isLoading) {
    return <div data-testid="loader">Loading</div>
  }
 
  if (isError) {
    return <div>Error: {error.message}</div>
  }
 
  return (
    <div>
      <h1>Update Data</h1>
      <DataForm defaultValues={data} onFormSubmit={onFormSubmit} isLoading={isMutating} />
    </div>
  )
}

์—…๋ฐ์ดํŠธ ํŽ˜์ด์ง€์— ๋Œ€ํ•ด์„œ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…๋“œ๋ฆฌ์ž๋ฉด, ํ˜„์žฌ url๋กœ๋ถ€ํ„ฐ id ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ํ•ด๋‹น id์— ํ•ด๋‹นํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ํผ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’์ด ์„ธํŒ…๋˜๋ฉฐ, ์ด๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•  ๊ฒฝ์šฐ ๋ฃจํŠธ("/")๋กœ ๋ผ์šฐํŒ…๋˜๋Š” ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.

์—…๋ฐ์ดํŠธ ํŽ˜์ด์ง€์—์„œ ์šฐ๋ฆฌ๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • url ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ฃผ์–ด์ง„ id๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.
  • ๋กœ๋”ฉ ์ค‘์—๋Š” ๋กœ๋”ฉ ํ™”๋ฉด์ด ๋…ธ์ถœ๋œ๋‹ค.
  • ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์—๋Ÿฌ ๋ฌธ๊ตฌ๊ฐ€ ๋…ธ์ถœ๋œ๋‹ค.
  • ๋ฐ์ดํ„ฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์กฐํšŒ๋˜์—ˆ์„ ๊ฒฝ์šฐ "Update Data" ๋ฌธ๊ตฌ๊ฐ€ ๋…ธ์ถœ๋˜๊ณ , DataForm ์ปดํฌ๋„ŒํŠธ์— defaultValue๊ฐ’์œผ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค.
  • ํผ์—์„œ ์ œ์ถœ์ด ์ •์ƒ์ ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋ฉด ๋ฃจํŠธ("/")๋กœ ํŽ˜์ด์ง€ ์ด๋™์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์œ„์˜ ๊ธฐ๋Œ€์‚ฌํ•ญ๋“ค์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

jest.mock('./useFetchData', () => ({
  useFetchData: jest.fn(),
}))
 
jest.mock('./useUpdateData', () => ({
  useUpdateData: jest.fn(),
}))
 
jest.mock('./DataForm', () => ({
  DataForm: jest.fn(),
}))
 
describe('UpdatePage', () => {
  beforeEach(() => {
    useFetchData.mockImplementation(() => ({}))
    useUpdateData.mockImplementation(() => ({}))
    DataForm.mockImplementation(() => null)
  })
  it('fetches the data for the given id', () => {
    renderWithRouter(() => <Route path=":id" element={<UpdatePage />} />, '/test-id')
    expect(useFetchData).toHaveBeenCalledWith('test-id')
  })
 
  describe('while loading', () => {
    it('renders a loader', () => {
      useFetchData.mockImplementation(() => ({
        isLoading: true,
      }))
 
      const { getByTestId } = renderWithRouter(
        () => <Route path=":id" element={<UpdatePage />} />,
        '/test-id',
      )
      expect(getByTestId('loader')).toBeTruthy()
    })
  })
 
  describe('with an error', () => {
    it('renders an error message', () => {
      useFetchData.mockImplementation(() => ({
        isError: true,
        error: { message: 'Something went wrong' },
      }))
 
      const { container } = renderWithRouter(
        () => <Route path=":id" element={<UpdatePage />} />,
        '/test-id',
      )
      expect(container.innerHTML).toMatch('Error: Something went wrong')
    })
  })
 
  describe('with data', () => {
    it('renders the update page title and the data form', () => {
      useFetchData.mockImplementation(() => ({
        data: { foo: 'bar' },
      }))
 
      const { container } = renderWithRouter(
        () => <Route path=":id" element={<UpdatePage />} />,
        '/test-id',
      )
      expect(container.innerHTML).toMatch('Update Data')
      expect(DataForm).toBeCalledWith(
        expect.objectContaining({
          defaultValues: { foo: 'bar' },
        }),
        {},
      )
    })
 
    describe('on data form submit', () => {
      it('updates the data and navigates to the root page', async () => {
        DataForm.mockImplementation(({ onFormSubmit }) => (
          <button onClick={() => onFormSubmit({ foo: 'bar' })}>Submit</button>
        ))
        const mutateAsync = jest.fn()
 
        useUpdateData.mockImplementation(() => ({ mutateAsync }))
 
        const { getByText, history } = renderWithRouter(
          () => (
            <>
              <Route path=":id" element={<UpdatePage />} />
              <Route path="/" element={<div />} />
            </>
          ),
          '/test-id',
        )
 
        fireEvent.click(getByText('Submit'))
 
        expect(mutateAsync).toHaveBeenCalledWith({
          foo: 'bar',
          id: 'test-id',
        })
 
        await waitFor(() => {
          expect(history.location.pathname).toEqual('/')
        })
      })
    })
  })
})

9. ์ฐธ๊ณ 

Mocking

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์˜์กด์ ์ธ ๋ถ€๋ถ„์„ ์ง์ ‘ ์ƒ์„ฑํ•˜๊ธฐ๊ฐ€ ๋ถ€๋‹ด์Šค๋Ÿฝ๊ธฐ ๋•Œ๋ฌธ์— ๋ชจํ‚น์„ ๋งŽ์ด ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์€ ๊ธฐ๋Šฅ์ด ๋‹ค๋ฅธ ๊ธฐ๋Šฅ๋“ค๊ณผ ์—ฎ์—ฌ ์žˆ์„ ๊ฒฝ์šฐ ์ •ํ™•ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด๋‹น ์ฝ”๋“œ๊ฐ€ ์˜์กดํ•˜๋Š” ๋ถ€๋ถ„์„ ๊ฐ€์งœ๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ฒƒ์„ ๋ชจํ‚น์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

jest.fn()

Jest๋Š” ๊ฐ€์งœ ํ•จ์ˆ˜(mock function)๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก jest.fn() ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ์ด์šฉํ•ด์„œ ์ผํšŒ์„ฑ ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ๋‚ด๋ถ€์˜ ํ•จ์ˆ˜๋ฅผ ์ง„์งœ์ฒ˜๋Ÿผ ๊ตฌ๋™์‹œ์ผœ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

jest.mock()

jest.fn()์€ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ชจํ‚น ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค„ ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ, jest.mock()์€ ๊ทธ๋ฃน์œผ๋กœ ํ•œ ๋ฒˆ์— ๋ชจํ‚น ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค„ ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Jest API

๋‹ค์Œ์€ Jest๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” API ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค.

describe

์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๋ฌถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ž‘์€ ๋‹จ์œ„์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๊ทธ๋ฃนํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

it๊ณผ test

it๊ณผ test ๋‘ ๊ฐ€์ง€ ๋ชจ๋‘ ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ ์™„์ „ํžˆ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ it์€ test์˜ ๋ณ„์นญ์ด๋ฏ€๋กœ ๊ธฐ๋Šฅ์ ์œผ๋กœ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

expect

expect()์—๋Š” ์ฃผ๋กœ ํ…Œ์ŠคํŠธ ์ž…๋ ฅ ๊ฐ’ ๋˜๋Š” ๊ธฐ๋Œ€๊ฐ’์„ ๋„ฃ์Šต๋‹ˆ๋‹ค.

toBe

ํ…Œ์ŠคํŠธ์˜ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋Š” API์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ์˜ ์˜ˆ์ƒ ๊ฒฐ๊ณผ ๊ฐ’์„ ๋„ฃ์Šต๋‹ˆ๋‹ค.

beforeEach

ํ…Œ์ŠคํŠธ ํŒŒ์ผ์˜ ๊ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋Œ๊ธฐ ์ „์— ์ˆ˜ํ–‰ํ•  ๋กœ์ง์„ ๋„ฃ๋Š” API์ž…๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋งˆ๋‹ค ๋ฐ˜๋ณต๋˜๋Š” ๋กœ์ง์„ ๋„ฃ๊ธฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

RTL์˜ ๋‹ค์–‘ํ•œ Query

RTL์—๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด Variant์™€ Query๊ฐ€ ์กด์žฌํ•˜๋ฉฐ, ์ด ๋‘˜์˜ ์กฐํ•ฉ์„ ํ†ตํ•ด ํŠน์ • DOM์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Variant

getBy

getBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ์— ์—†์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

getAllBy

getAllBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ์— ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

queryBy

queryBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ์— ์กด์žฌํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

queryAllBy

queryAllBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ์— ์กด์žฌํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

findBy

findBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ํ•˜๋‚˜๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ•ด๋‹น DOM์„ ์„ ํƒํ•˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ timeout์ธ 4500ms ์ดํ›„์—๋„ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

findAllBy

findAllBy*๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” DOM ์—˜๋ฆฌ๋จผํŠธ ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ•ด๋‹น DOM์„ ์„ ํƒํ•˜๋Š” Promise๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ timeout์ธ 4500ms ์ดํ›„์—๋„ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์œผ๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

Queries

ByLabelText

ByLabelText๋Š” label์ด ์žˆ๋Š” input์˜ label ๋‚ด์šฉ์œผ๋กœ input์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

ByPlaceholderText

ByPlaceholderText๋Š” placeholder ๊ฐ’์œผ๋กœ input ๋ฐ textarea๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

ByText

ByText๋Š” ์—˜๋ฆฌ๋จผํŠธ๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํ…์ŠคํŠธ ๊ฐ’์œผ๋กœ DOM์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ ๋กœ ํ…์ŠคํŠธ ๊ฐ’์— ์ •๊ทœ์‹์„ ๋„ฃ์–ด๋„ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

ByAltText

ByAltText๋Š” alt ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์—˜๋ฆฌ๋จผํŠธ(์ฃผ๋กœ img)๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

ByTitle

ByTitle์€ title ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” DOM ํ˜น์€ title ์—˜๋ฆฌ๋จผํŠธ๋ฅผ ์ง€๋‹ˆ๊ณ  ์žˆ๋Š” SVG๋ฅผ ์„ ํƒํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋„์›€์ด ๋๋‹ค๋ฉด ๊ณต์œ ํ•ด ์ฃผ์„ธ์š”.
X์— ๊ณต์œ RSS ๊ตฌ๋…

๊ฐ™์€ ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๊ธ€ ยท React

ReactReact ์ƒํƒœ๊ด€๋ฆฌ๐Ÿ’ก ๋“ฑ์žฅ๋ฐฐ๊ฒฝ ์ดˆ๊ธฐ ์›น์„ ๋Œ์•„๋ณด๋ฉด Back-end ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์›นํŽ˜์ด์ง€๋ฅผ ๋™์ ์œผ๋กœ ๋ Œ๋”๋งํ•ด์ฃผ์–ด Front-end์—์„œ์˜ ์ƒํƒœ๊ด€๋ฆฌ๋Š” ๋ฌผ๋ก  ๋ณ„๋„์˜ Front-end ์„œ๋ฒ„๋„ ํ•„์š”ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ ์ฐจ ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ  ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์›ํ™œํ•œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ํŽ˜์ด์ง€ ๋ Œ๋”๋ง๊ณผ UI ์ž‘โ€ฆ2022. 03. 11์ฝ๊ธฐ 10๋ถ„
React์•„ํ† ๋ฏน ๋””์ž์ธ์•„ํ† ๋ฏน ๋””์ž์ธ ๋””์ž์ธ ์š”์†Œ๋“ค์„ ๋‚˜๋ˆ„์–ด ํŒŒ์•…ํ•˜๊ณ  ์ด ์š”์†Œ๋“ค์˜ ์กฐํ•ฉ์„ ํ†ตํ•ด์„œ ๋””์ž์ธ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์ปดํฌ๋„ŒํŠธ ์ค‘์‹ฌ์˜ ๋””์ž์ธ ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ๋ฆฌ์•กํŠธ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๋งŒ๋“ค์–ด์ง€๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ์ปดํฌ๋„ŒํŠธ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต๋˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋นผโ€ฆ2021. 05. 24์ฝ๊ธฐ 4๋ถ„