๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Front-end/React

styled-component, useState ์‚ฌ์šฉํ•ด์„œ Tag ๋งŒ๋“ค๊ธฐ๋ฅผ ๊ฐ€์žฅํ•œ testCase ์‚ฝ์งˆ๊ธฐ

by ciocio 2021. 9. 15.

๐Ÿ“Œ  Tag ๋ž€?

 

๊ฒ€์ƒ‰์ฐฝ์— "UI tags" ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋ณผ ์ˆ˜ ์žˆ๋Š” ์ด๋ฏธ์ง€๋“ค

 

ํ‚ค์›Œ๋“œ ์ค‘์‹ฌ์˜ ๋ฒ„ํŠผ๋ฒ„ํŠผ ์•„๋‹™๋‹ˆ๋‹ค์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค. (์ •ํ™•ํžˆ๋Š” ๋ฒ„ํŠผ์€ ์•„๋‹˜. "๋ฒ„ํŠผ์ฒ˜๋Ÿผ" ๋™์ž‘ํ•  ๋ฟ)

์ผ๋ฐ˜์ ์ธ ๋ฒ„ํŠผ๊ณผ์˜ ์ฐจ์ด๊ฐ€ ์žˆ๋‹ค๋ฉด, ํด๋ฆญํ–ˆ์„ ๋•Œ ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ๋ฅผ ์ฆ‰๊ฐ์ ์œผ๋กœ ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ฒ„ํŠผ์ด ์•„๋‹ˆ๋‹ˆ๊นŒ ๋‹น์—ฐํ•œ ์ด์•ผ๊ธฐ ...

๋‹ค๋งŒ, ์„ ํƒ๋œ ํƒœ๊ทธ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ •๋ณด๋ฅผ ์กฐํ•ฉํ•ด ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ๊ฐ€ ๊ตฌ์„ฑ๋œ๋‹ค.

 

ex. ํ•€ํ„ฐ๋ ˆ์ŠคํŠธ์— ์ฒ˜์Œ ๋“ค์–ด๊ฐ”์„ ๋•Œ ๊ด€์‹ฌ์‚ฌ ํƒœ๊ทธ๋“ค์„ ํด๋ฆญํ•˜๋ฉด ๋‚ด ํ”ผ๋“œ๊ฐ€ ํƒœ๊ทธ ๊ด€๋ จ ์ด๋ฏธ์ง€๋“ค๋กœ ์ฑ„์›Œ์ง„๋‹ค.

ex. ๋…ธ์…˜์—์„œ ๋…ธํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉฐ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ํƒœ๊ทธ๋“ค์„ ๋‹ฌ์•„๋†“๋Š”๋‹ค๋ฉด, ๋‚˜์ค‘์— ํ•ด๋‹น ํƒœ๊ทธ๊ฐ€ ์žˆ๋Š” ๋…ธํŠธ๋“ค๋งŒ ์ทจํ•ฉํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

๐Ÿ“Œ  Tag๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž.

 

โ—พ  Tag ์ƒํƒœ๋งŒ ์ถ”์ ํ•  ๋•Œ

 

export const Tag = () => {

  // dsfault ๊ฐ’์€ ๋„ฃ์–ด๋„ ๋˜๊ณ  ์•ˆ ๋„ฃ์–ด๋„ ๋จ
  const initialTags = [];

  // ํƒœ๊ทธ ๋ชฉ๋ก ์ƒํƒœ ๊ด€๋ฆฌ
  const [tags, setTags] = useState(initialTags);

  // ํƒœ๊ทธ๋ฅผ ์ง€์› ์„ ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const removeTags = (indexToRemove) => {
    setTags(tags.filter((_, idx) => { return idx !== indexToRemove}));
  };
  
  // ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const addTags = (event) => {

    const text = event.target.value;
	
    // ์ถ”๊ฐ€ํ•˜๋ ค๋Š” ํƒœ๊ทธ๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š๊ณ  && ๊ธฐ์กด ํƒœ๊ทธ ๋ชฉ๋ก์— ์†ํ•˜์ง€ ์•Š์€ ์ƒˆ๋กœ์šด ํƒœ๊ทธ์ผ๋•Œ๋งŒ ์ถ”๊ฐ€
	if(text.length !== 0 && !tags.includes(text)){
      setTags([...tags, text]);
      event.target.value = '';
    }
    // ๋นˆ ํƒœ๊ทธ๊ฑฐ๋‚˜ ์ด๋ฏธ ์žˆ๋Š” ํƒœ๊ทธ ์ผ๋• input box ๋ฅผ ๊น”๋”ํžˆ ๋น„์›Œ์คฌ๋‹ค
    else{
      event.target.value = '';
    }
    
  }
  return (
    <>
      <TagsInput>
        <ul id='tags'>
          {tags.map((tag, index) => (
            <li key={index} className='tag'>
              <span className='tag-title'>{tag}</span>
              <!--๊ฐ‘์ž๊ธฐ ๊ถ๊ธˆํ•œ์  -> ์ด๋ฒคํŠธ ํ—จ๋“ค๋Ÿฌ๋Š” ์ „ํ•ด์ฃผ๋Š” ์ธ์ž๊ฐ’์ด ์ƒ๋‹นํžˆ ๋‹ค์–‘ํ•˜๋‹ค ?-->
              <!--์ด๋ฒคํŠธ ๊ฐ์ฒด ๋‚ด์— index ํ”„๋กœํผํ‹ฐ ๊ฐ’์ด ๋”ฐ๋กœ ์žˆ๋Š”๊ฑฐ๊ฒ ์ง€ ???!!!-->
              <span className='tag-close-icon' onClick={() => {removeTags(index)}}>&times;</span>
            </li>
          ))}
        </ul>
        <input
          className='tag-input'
          type='text'
          // ํ‚ค ๊ฐ’์ด Enter์ผ ๋•Œ๋งŒ ์ด๋ฒคํŠธ ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ์ฃผ๋„๋ก ๊ตฌํ˜„
          onKeyUp={(e) => e.key === 'Enter' ? addTags(e) : null}
          placeholder='Press enter to add tags'
        />
      </TagsInput>
    </>
  );
};

 

 

โ—พ  Tag ์ƒํƒœ์™€ Input Value๋ฅผ ์ถ”์ ํ•  ๋•Œ

 

export const Tag = () => {

  // dsfault ๊ฐ’์€ ๋„ฃ์–ด๋„ ๋˜๊ณ  ์•ˆ ๋„ฃ์–ด๋„ ๋จ
  const initialTags = [];

  // ํƒœ๊ทธ ๋ชฉ๋ก ์ƒํƒœ ๊ด€๋ฆฌ
  const [tags, setTags] = useState(initialTags);
  
  // ์ž…๋ ฅ ๋ฌธ์ž ์ƒํƒœ ๊ด€๋ฆฌ
  const [text, setTexts] = useState('');

  // ๋ฌธ์ž๊ฐ€ ์ž…๋ ฅ๋˜์—ˆ์„ ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const addTexts = (event) => {
    setTexts(event.target.value);
  }

  // ํƒœ๊ทธ๋ฅผ ์ง€์› ์„ ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const removeTags = (indexToRemove) => {
    setTags(tags.filter((_, idx) => { return idx !== indexToRemove}));
  };
  
  // ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  const addTags = () => {
	
    // ์ถ”๊ฐ€ํ•˜๋ ค๋Š” ํƒœ๊ทธ๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š๊ณ  && ๊ธฐ์กด ํƒœ๊ทธ ๋ชฉ๋ก์— ์†ํ•˜์ง€ ์•Š์€ ์ƒˆ๋กœ์šด ํƒœ๊ทธ์ผ๋•Œ๋งŒ ์ถ”๊ฐ€
	if(text.length !== 0 && !tags.includes(text)){
      setTags([...tags, text]);
      setText('');
    }
    // ๋นˆ ํƒœ๊ทธ๊ฑฐ๋‚˜ ์ด๋ฏธ ์žˆ๋Š” ํƒœ๊ทธ ์ผ๋• input box ๋ฅผ ๊น”๋”ํžˆ ๋น„์›Œ์คฌ๋‹ค
    else{
      setText('');
    }
    
  }
  return (
    <>
      <TagsInput>
        <ul id='tags'>
          {tags.map((tag, index) => (
            <li key={index} className='tag'>
              <span className='tag-title'>{tag}</span>
              <span className='tag-close-icon' onClick={() => {removeTags(index)}}>&times;</span>
            </li>
          ))}
        </ul>
        <input
          className='tag-input'
          type='text'
          value={text}
          onChange={addTexts}
          // ํ‚ค ๊ฐ’์ด Enter์ผ ๋•Œ๋งŒ ์ด๋ฒคํŠธ ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ์ฃผ๋„๋ก ๊ตฌํ˜„
          onKeyUp={(e) => e.key === 'Enter' ? addTags : null}
          placeholder='Press enter to add tags'
        />
      </TagsInput>
    </>
  );
};

 

ํ˜„์žฌ ๊ตฌํ˜„ํ•œ ์ƒํƒœ๋Š” ์ž…๋ ฅ์ฐฝ์ด ํ•˜๋‚˜๋ผ input ๊ฐ’์˜ ์ƒํƒœ๋ฅผ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ๋น„ํšจ์œจ์ ์ด์—ˆ๋‹ค.

๊ทธ๋ž˜๋„ useState๋ฅผ ์–ด๋–ป๊ฒŒ ์จ์•ผํ•˜๋Š” ์ง€ ๊ฐ์„ ์žก์„ ์ˆ˜ ์žˆ์–ด ์ข‹์•˜๋‹ค. :)

 

 

๐Ÿ“Œ  ์ด๊ฑด ์™œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๊ณ , ์ €๊ฑด ์™œ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๋Š”๊ฐ€ ????

 

 

โœ…  ์ฝ”๋“œ ์š”๊ตฌ ์‚ฌํ•ญ : '์ค‘๋ณต๋œ ๊ฐ’์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ, Enter๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.'

โœ…  ๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ Enter key๊ฐ€ ํด๋ฆญ๋˜์—ˆ์„ ๋‹น์‹œ์˜ input value๋ฅผ console๋กœ ์ฐ์–ด๋ณด์•˜๋‹ค.

 

๐Ÿ˜‚  ์ฒซ๋ฒˆ์งธ ๊ฒฝ์šฐ  -> ์ž…๋ ฅ๊ฐ’์ด ๊ฐ™์€ ํƒœ๊ทธ๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ input value๋ฅผ ์‚ญ์ œํ•˜์ง€ ์•Š๋Š”๋‹ค.  ->  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ

 

 

๐Ÿ˜ก  ๋‘๋ฒˆ์งธ ๊ฒฝ์šฐ  ->  ์ž…๋ ฅ๊ฐ’์ด ๊ฐ™์€ ํƒœ๊ทธ๋ฅผ ์ž…๋ ฅํ–ˆ์„ ๋•Œ input value๋ฅผ ์‚ญ์ œํ•œ๋‹ค.  ->  ํ…Œ์ŠคํŠธ ๋ฏธํ†ต๊ณผ

 

 

๋‘˜ ๋‹ค ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์ง€ ์•Š๋Š” ์ ์€ ๋™์ผํ•˜๋‹ค.

๋‹ค๋งŒ input box์˜ ์ƒํƒœ๊ฐ€ ๋‹ค๋ฅผ ๋ฟ์ธ๋ฐ .... ๋‚œ ๊น”๋”ํ•˜๊ฒŒ ํ•œ๋‹ค๊ณ  input value๋ฅผ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋กœ ๋น„์›Œ์คฌ๋Š”๋ฐ ....

 

์ด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์›ํ•˜๋Š” ๊ฑด ๋ญ์˜€์„๊นŒ ????  ์š”๊ตฌ ์‚ฌํ•ญ์„ ์ถฉ๋ถ„ํžˆ ๊ตฌํ˜„ํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ ํ†ต๊ณผ๊ฐ€ ์•ˆ๋˜๋‹ˆ๊นŒ ๋„ˆ๋ฌด ์†์ƒํ–ˆ๋‹ค.

์ด๋•Œ๋ถ€ํ„ฐ์˜€๋‚˜์š” ์ œ๊ฐ€ ํ…Œ์ŠคํŠธ์ผ€์ด์Šค ๋ฌธ๋ฒ•์„ ํŒŒ๊ธฐ ์‹œ์ž‘ํ•œ ๊ฒŒ ...

 

 

โœ…  ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์‚ดํŽด๋ณด์ž.

 

it('Enterํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์‹ค์ œ ํƒœ๊ทธ๊ฐ€ ์ถ”๊ฐ€๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', async () => {
    const { queryAllByRole, container } = render(<Tag />);
    const input = container.querySelector('input');

    expect(input).toBeTruthy();

    userEvent.type(input, 'kimcoding');
    fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' });
    await waitFor(() => {
      expect(queryAllByRole('listitem').length).toBeGreaterThan(1);
    });

    await waitFor(() => {
      const tagLength = queryAllByRole('listitem').length;
      expect(
        queryAllByRole('listitem')[tagLength - 1].textContent.slice(0, 9)
      ).toBe(input.value);
    });
  });

  

โœ”  queryAllByRole ์€ ์–ด๋–ค ์—ญํ• ์ธ๊ฐ€

 

"React Testing Library"

querySelector ์™€ ๊ฐ™์€ DOM API ๊ฐ€ ์•„๋‹ˆ์—ˆ๋‹ค !! (์ถฉ๊ฒฉ)

์ฐพ์•„๋ณด๋‹ˆ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ž‘์„ฑ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋”ฐ๋กœ ์žˆ๋”๋ผ. ๊ทธ ์ค‘์— ์†ํ•œ ํ•˜๋‚˜์˜ ํ•จ์ˆ˜์˜€๋‹ค.

 

โœ”  ๊ทธ๋Ÿผ Role์€ ๋ฌด์—‡์ธ๊ฐ€

 

role-attribute ์ข…๋ฅ˜๋“ค

getElementById ๋Š” HTML ํƒœ๊ทธ์˜ id๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ element๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ ,

getElementByClassName ์€ HTML ํƒœ๊ทธ์˜ class ์ด๋ฆ„์„ ๊ธฐ์ค€์œผ๋กœ element๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

๋ญ”๊ฐ€ Role๋„ ๋น„์Šทํ•˜๊ฒŒ HTML ์–ดํŠธ๋ฆฌ๋ทฐํŠธ์ธ๊ฐ€ ? ํ•ด์„œ ์ฐพ์•„๋ดค์ง€๋งŒ ๋‚˜์˜ค์ง€ ์•Š์•˜๋‹ค.

์•Œ๊ณ ๋ณด๋‹ˆ Role์€ ์ƒ๊ฐ๋ณด๋‹ค ์•„์ฃผ ์•„์ฃผ ๋„“๊ณ  ํฌ๊ด„์ ์ธ ๊ฐœ๋…์„ ๊ฐ€์ง„ ์•„์ด์˜€๋‹ค.

๋‚ด๊ฐ€ ์•Œ๊ณ ์žˆ๋Š” "HTML ์–ดํŠธ๋ฆฌ๋ทฐํŠธ"๋“ค์„ ํฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค !ใ…!

 

โœ”  listitem์€ li ์†์„ฑ์˜ ๋ชจ์Œ์ด์—ˆ๋‹ค

 

 

role์ด๋ผ๋Š”๊ฒŒ attribute๋ž‘์€ ๋˜ ๋‹ค๋ฅด๊ฒŒ ... ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—๋งŒ ์“ฐ์ด๋Š” ๊ฑด๊ฐ€ ?? ์‹ถ๊ณ . ์ผ๋‹จ ๋” ์•Œ์•„๋ด์•ผ๊ฒ ๋‹ค.

li ํƒœ๊ทธ๋กœ ๊ฒ€์ƒ‰ํ•˜๊ณ , ํ•ด๋‹น ๋ฐฐ์—ด์˜ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ์„ ๋•Œ ํ…Œ์ŠคํŠธ์ผ€์ด์Šค์— ๋งž๋Š” ๊ฒฐ๊ณผ๊ฐ€ ์ถœ๋ ฅ๋˜์—ˆ๋‹ค.

 

โœ”  input์˜ value๊ฐ’์ด ๋น„๊ต ๊ธฐ์ค€์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‚ด ๋กœ์ง์€ ํ†ต๊ณผํ•  ์ˆ˜ ์—†์—ˆ๋‹ค.

 

listitem์ด ๊ฐ€์ง„ ๋ฐฐ์—ด์˜ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค ๊ฐ’์˜ textContent์˜ ๋ฌธ์ž์—ด ๋ณต์‚ฌ๋ณธ์€ 'kimcoding'์ด์—ˆ๋‹ค.

์ฒซ๋ฒˆ์งธ ๊ฒฝ์šฐ์˜ input value๋Š” 'kimcoding'์ด์—ˆ๊ณ , ๋‘๋ฒˆ์งธ ๊ฒฝ์šฐ์˜ input value๋Š” ' '์ด์—ˆ๋‹ค.

์ž…๋ ฅ๊ฐ’์ด ๊ธฐ์กด ํƒœ๊ทธ์™€ ์ค‘๋ณต๋˜์—ˆ์„ ๋•Œ ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š” ๋กœ์ง์„ ์งฐ์ง€๋งŒ ์˜์™ธ์˜ ๋ณ€์ˆ˜์— ๊ฑธ๋ ธ๋‹ค ใ…‹ใ…‹ใ…‹ 

 

 

๐Ÿ“  ๋Š๋‚€์  

 

โœ”  ํ…Œ์ŠคํŠธ์ผ€์ด์Šค์˜ ์ด๋ฆ„์ด ๋ฐ”๋€Œ์–ด์•ผํ• ๊ฒƒ๊ฐ™๋‹ค.  -->  ํ˜น์‹œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ณต๋ถ€์‹œํ‚ค๊ธฐ ์œ„ํ•œ ํฐ๊ทธ๋ฆผ์ด์—ˆ๋‚˜ ..?

โœ”  UX์— ๋Œ€ํ•ด ๋‹ค์‹œ ์ƒ๊ฐํ•ด๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.  -->  ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ํƒœ๊ทธ๊ฐ€ ์ค‘๋ณต๋˜์—ˆ๋‹ค๋Š” ๊ฑธ ์–ด๋–ป๊ฒŒ ์ธ์ง€์‹œํ‚ค๋Š” ๊ฒŒ ์ข‹์„๊นŒ ?

์–ด์ฉŒ๋ฉด input ์ฐฝ์ด ๋น„์›Œ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋” ํšจ์œจ์ ์ด๋ž€ ์ƒ๊ฐ๋„ ๋“ค์—ˆ๋‹ค. ๋‚˜์ฒ˜๋Ÿผ input ์ฐฝ์ด ๋น„์›Œ์ง€๋ฉด ์•„ ๋‚ด๊ฐ€ ์ž…๋ ฅ ์ž์ฒด๋ฅผ ์•ˆํ–ˆ์—ˆ๋‚˜ ? ํ•˜๋ฉด์„œ ๋‹ค์‹œ ์ž…๋ ฅํ•ด๋ณด๋Š” side effect๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜๋„ ์žˆ๊ณ  ใ…‹ใ…‹ "ํƒœ๊ทธ ๊ฐ’์ด ์ค‘๋ณต๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋ผ๋Š” ๋ฌธ๊ตฌ๋ฅผ ๋„์šฐ๊ณ  ์‚ฌ๋ผ์ง€๊ฒŒ ํ•˜๋Š” ๊ฒƒ๋„ ๊ดœ์ฐฎ์€ ๋ฐฉ๋ฒ•๊ฐ™๊ณ , input์ฐฝ ์ž์ฒด๋ฅผ ํ”๋“ค๋ฉฐ ๊ฐ’์„ ๊ฑฐ๋ถ€ํ•˜๋Š” ๋ชจ์…˜๋„ ์žฌ๋ฐŒ์„ ๊ฒƒ ๊ฐ™๋‹ค ใ…Žใ…Žใ…Ž *(๋จธ์“ฑ) 

 

 

๐Ÿ“Œ  Tag ๊ตฌํ˜„ ๊ฒฐ๊ณผ

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€