ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ - ์ƒํƒœ ๊ด€๋ฆฌ ๋ชจํ‚น

date
2025-10-19
order
4

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ - ์ƒํƒœ ๊ด€๋ฆฌ ๋ชจํ‚น

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

ProductInfoTable

const ProductInfoTable = () => {
  const { cart, removeCartItem, changeCartItemCount } = useCartStore(state =>
    pick(state, 'cart', 'removeCartItem', 'changeCartItemCount'),
  );
  const { user } = useUserStore(state => pick(state, 'user'));

  return (
    <TableContainer component={Paper} sx={{ wordBreak: 'break-word' }}>
      <Table aria-label="์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฆฌ์ŠคํŠธ">
        <TableBody>
          {Object.values(cart).map(item => (
            <ProductInfoTableRow
              key={item.id}
              item={item}
              user={user}
              removeCartItem={removeCartItem}
              changeCartItemCount={changeCartItemCount}
            />
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

๋‹จ์œ„ ํ…Œ์ŠคํŠธ?

ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์ธ ProductInfoTableRow๋„ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ(์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ)์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ ๋” ์ •ํ™•ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ด ์ปดํฌ๋„ŒํŠธ๋จผ์ € ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด์•ผํ•˜์ง€ ์•Š์„๊นŒ? ์•„๋‹๊นŒ?

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

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

  • ์ฐพ์•„๋ณด๋‹ˆ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์‚ฌ์šฉ๋˜์–ด ๋ณต์žกํ•œ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง์„ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” (<Button variant={} size={} disabled={} onClick={} ...... />) ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š”๊ฒƒ์ด ์ข‹๊ณ  ๊ทธ ์™ธ์˜ ๋Œ€๋ถ€๋ถ„์˜ ๋ Œ๋”๋ง ์ปดํฌ๋„ŒํŠธ๋“ค์€ ์‚ฌ์‹ค ํ†ตํ•ฉํ…Œ์ŠคํŠธ์— ๊ฒ์ฆํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.
  • ๊ทธ๋ž˜๋„ ์—ฌ์ „ํžˆ ์œ ๋‹› ํ…Œ์ŠคํŠธ์˜ ์œ ์šฉ์„ฑ์— ๋Œ€ํ•ด ์˜๋ฌธ์ด ๋“ ๋‹ค.

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

// ProductInfoTable.spec.jsx
beforeEach(() => {
  mockUseUserStore({user: {id: 1}});
  mockUseCartStore({
    cart: {
      6: {
        id: 6,
        title: 'Handmade Cotton Fish',
        price: 809,
        description:
          'The slim & simple Maple Gaming Keyboard from Dev Byte comes with a sleek body and 7- Color RGB LED Back-lighting for smart functionality',
        images: [
          'https://user-images.githubusercontent.com/35371660/230712070-afa23da8-1bda-4cc4-9a59-50a263ee629f.png',
        ],
        count: 3,
      },
      7: {
        id: 7,
        title: 'Awesome Concrete Shirt',
        price: 442,
        description:
          'The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J',
        images: [
          'https://user-images.githubusercontent.com/35371660/230762100-b119d836-3c5b-4980-9846-b7d32ea4a08f.png',
        ],
        count: 4,
      },
    },
    totalCount: 7,
    totalPrice: 500,
  });
});

์ด๋ ‡๊ฒŒ ์…‹์—…์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๋ชจํ‚นํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ๋“ค์„ ์ง„ํ–‰ํ•˜๋ฉด ๋œ๋‹ค.

it('์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํฌํ•จ๋œ ์•„์ดํ…œ๋“ค์˜ ์ด๋ฆ„, ์ˆ˜๋Ÿ‰, ํ•ฉ๊ณ„๊ฐ€ ์ œ๋Œ€๋กœ ๋…ธ์ถœ๋œ๋‹ค', async () => {
  await render(<ProductInfoTable />);

  const [firstItem, secondItem] = screen.getAllByRole('row');

  expect(
    within(firstItem).getByText('Handmade Cotton Fish'),
  ).toBeInTheDocument();
  expect(within(firstItem).getByRole('textbox')).toHaveValue('3');
  expect(within(firstItem).getByText('$2,427.00')).toBeInTheDocument();

  expect(
    within(secondItem).getByText('Awesome Concrete Shirt'),
  ).toBeInTheDocument();
  expect(within(secondItem).getByRole('textbox')).toHaveValue('4');
  expect(within(secondItem).getByText('$1,768.00')).toBeInTheDocument();
});

getBy, queryBy

it('ํŠน์ • ์•„์ดํ…œ์˜ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ์•„์ดํ…œ์ด ์‚ฌ๋ผ์ง„๋‹ค', async () => {
  const { user } = await render(<ProductInfoTable />);
  const [, secondItem] = screen.getAllByRole('row');
  const deleteButton = within(secondItem).getByRole('button');

  expect(screen.getByText('Awesome Concrete Shirt')).toBeInTheDocument();

  await user.click(deleteButton);

  expect(screen.queryByText('Awesome Concrete Shirt')).not.toBeInTheDocument();
});

getByText๋Š” DOM์— ํ•ด๋‹น ํ…์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ•ด๋‹น ์š”์†Œ๊ฐ€ ์‚ฌ๋ผ์ ธ์•ผํ•˜๋Š”๋ฐ ์‚ฌ๋ผ์ง„๊ฒƒ์„ ํ™•์ธํ•˜๋ ค๋ฉด queryBy ๋งค์ฒ˜๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

๊ณต์‹๋ฌธ์„œ์˜ ์„ค๋ช…

  • getBy...: Returns the matching node for a query, and throw a descriptive error if no elements match or if more than one match is found (use getAllBy instead if more than one element is expected).
  • queryBy...: Returns the matching node for a query, and return null if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use queryAllBy instead if this is OK).

๊ทธ๋Ÿฌ๋ฉด ์™œ queryBy ๋กœ ํ†ต์ผํ•˜์ง€ ์•Š๊ณ  getBy ๋งค์ฒ˜๋„ ์‚ฌ์šฉํ• ๊นŒ?