MAENG.
React

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

2022. 06. 06

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 ์ƒํƒœ๊ฐ’๊ณผ ๊ทธ ์ƒํƒœ๊ฐ’์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š”ย handle ๋ฅผ ๋ฐฐ์—ด์— ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ์ƒํƒœ๊ฐ’์„ย initialState ์ธ์ž๋ฅผ ํ†ตํ•ด ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธ์ž๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœย 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์—์„œ ์ œ๊ณตํ•˜๋Š” App.js์— BrowserRouter ๋˜๋Š” HashRotuer ๋ฅผ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๊ฐ์‹ธ ๋ผ์šฐํŒ…์„ ๊ตฌํ˜„ํ•˜๋Š”๋ฐ, 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 functiton)๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋กย 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

findBy*ย ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ฟผ๋ฆฌ๋Š” ์กฐ๊ฑด์— ์ผ์น˜ํ•˜๋Š” 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 ๋ฅผ ์„ ํƒ ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.