국비교육과정 정리/React

[React] Map, Filter를 이용한 컴포넌트 응용

백설마을꿀단지 2023. 1. 17.

Map, Filter를 이용한 컴포넌트 응용

목록 요소들(li 태그)을 반복처리하고자 한다면 map을 이용하면 된다. map 을 이용해 컴포넌트를 반복시킬 경우 반드시 요소에 key props를 전달해야 한다.

 

🤔 Key?

key는 요소에 고유성을 부여하는 속성이며 어떠한 항목을 변경, 추가 혹은 삭제하고자 할 때 해당 항목을 식별하기 위해서 사용된다. 리랜더링되었을 시 랜더링 전 key의 위치와 랜더링 이후 key의 위치를 비교하여 변경된 항목을 비교하게 되는데 만약 key 속성을 부여하지 않는다면 해당 요소의 위치를 찾을 수 없으므로 변경이 제대로 반영되지 않고 이상한 결과가 나타날 것이다.
일반적으로 key 값으로 랜덤으로 생성된 문자열(UUID or nanoId) 혹은 db의 pk값 처럼 중복되지 않고, 변하지 않는 값으로 설정한다. 

 

⛔ 배열의 index를 key 값으로 사용하는 것은 바람직하지 않다.

왜냐하면 배열이 순차적으로 값이 추가되는 것이 아니라 중간에 삽입 혹은 삭제되는 것처럼 비순차적인 변경이 이루어졌을 경우 데이터에 매칭된 key가 모두 꼬여버리게 될 수 있기 때문이다.

다음과 같은 경우에만 index를 key로 사용해야 안전하게 이용할 수 있다.

  1. 정적이고 변경되지 않는 경우
  2. 정렬을 하지 않는 경우(정렬을 하면 index가 변경됩니다.)
  3. 고유한 ID가 없는 경우
  4. 필터링 기능이 없는 경우

 

다음은 객체배열을 map으로 반복시키며 객체의 정보를 담은 li 태그를 만드는 예시이다.

반복되면서 생성되는 요소에는 반드시 key값을 주어야 한다. 지금은 id 라는 임시 고유값을 부여하여 사용하였다.

 

const IterationComponent2= () => {
   const arr = [
      {id: 1, topic: "Math"},
      {id: 2, topic: "Eng"},
      {id: 3, topic: "Kor"}
   ]
   /* 1) state 생성
   const [list, setList] = useState(arr);
   
   /* 2) map을 이용해 객체배열의 데이터를 담은 li 태그들을 생성 후
   li 태그들을 새로운 변수에 담는다. */
   const newList = list.map((item) => 
      <li key = {item.id}>
         {item.topic}
      </li>
   );
   
   /* 3) li 태그들으 담은 변수를 ul 사이에 삽입한다. */
   return (
      <>
         <ul>
            {newList}
         </ul>
      </>
   )
}

 

 

다음은 input 태그를 추가하여 사용자가 입력한 값을 객체로 만들어 state에 추가하도록 하였고, 생성된 li 태그를 더블클릭할 시 삭제되도록 만들었다.

 

 

1) input 태그 추가 후 사용자 입력값 state에 추가하기

   // 1) input에 입력된 값 가져오기
   const [data, setData] = useState('');
   const handleChange = (e) => {
      setData(e.target.value);
   }
   
   // 2) 버튼 클릭 시 input에 입력된 값을 state에 추가
   const handleClick= (e) => {
      let tmp = {id: list[list.length-1].id + 1, topic: data};
      
      // 3) 새로 생성된 객체가 추가된 임시 list를 생성한다.
      let tmpList = list.concat(tmp);
      
      // 4) set 함수 이용해서 임시 list를 state로 변경 후 input에 입력된 값 초기화
      setList(tmpList);
      setData('');
   }

 

🤔Array.prototype.concat()

concat() 메서드는 인자로 주어진 배열이나 값들을 기존 배열에 합쳐서 새 배열을 반환한다.

그렇기 때문에 기존의 배열을 변경하지 않고 인자가 추가된 새로운 배열을 얻을 수 있다.

 

원래 배열이나 객체인 state를 변경하기 위해서는 복사본을 만들고 복사본에 값을 추가한 후 복사본을 set 함수를 이용해서 변경사항을 반영시켜야 한다. 그러나 concat() 메서드를 이용하면 복사하지 않고도 값이 추가된 배열을 얻을 수 있다.

	  //기존 배열 복사하였을 때
	  let copy = [...list];
      let tmpIns = {id: copy.length+1, topic: e.target.previousElementSibling.value};
      copy.push(tmpIns);
      setList(copy);

 

2) 더블클릭 시 삭제

- li태그를 더블클릭하면 삭제될 수 있도록 map 에서 생성되는 li 태그에 onDoubleClick 이벤트 속성을 추가한다.

- 더블클릭 시 발생하는 이벤트 핸들링 함수를 map 코드 아래에 작성하게 되면 이벤트 핸들링이 선언되지 않았다는 에러가 발생하게 된다. 그 이유는 함수는 호이스팅이 되지 않기 때문이며 에러를 해결하기 위해서는 다음의 방법을 선택해서 사용하면 된다.

1) map을 사용한 코드 위에 이벤트핸들러 함수 선언 -> 해당 방법은 코드의 가독성이 떨어지고 복잡해지므로 비추천

2) 이벤트핸들링 함수를 태그 안에 직접 작성

3) 이벤트핸들링 함수를 익명함수로 만들고, 익명함수의 반환값으로 함수 호출

- filter를 사용해서 이벤트가 발생한 li 태그를 제외한 나머지 li태그를 담은 임시 list 생성 후 set함수로 state변경

   // 1) map에서 li 태그에 더블클릭 이벤트 속성 추가
   const newList = list.map((item) => 
      <li key = {item.id} onDoubleClick = { () => handleRemove(item.id)}>
         {item.topic}
      </li>
   );
   
   // 2) 이벤트핸들러 함수 작성
      const handleRemove = (id) => {
      let tmpList = list.filter( item => item.id !== id )
      setList(tmpList);
   }   
   
   return (
      <>
         <h3>목록 추가하기</h3>
         <input type="text" value = {data} onChange = {handleChange}/>
         <button type="button" onClick = {handleClick}>추가</button>
         <ul>
            {newList}
         </ul>
      </>
   )
}

 

위 내용을 바탕으로 실습 두 개를 진행하였다.

더보기

실습 1

- 객체배열에 담긴 이미지 속성을 map을 이용해 이미지 태그 생성하여 나열

- 이미지 선택 시 선택된 이미지가 위에 나타나도록 함

/* 
이미지를 가져오는 방법
1) 외부 서버에서 경로를 참조하는 방법 (가장 일반적인 방법)
<img src = "https://raw.githubusercontent.com/yopy0817/data_example/master/img/img1.png">

2) src 폴더에 img 폴더를 만들고 해당 폴더에 이미지 넣어서 참조
import img1 from '../img/img1.png';
<img src = {img1}> 
이미지가 많아지면 그만큼 import 해야하므로 권장x

이렇게 임포트해서 사용해야 한다.
3) public 폴더에 img 폴더 생성 후 이미지를 넣고 바로 참조
<img src = "/img/img1.png">

- 2, 3번은 이미지가 많아질 경우 용량문제로 사용을 잘 하지않는다.
*/

import { useState } from 'react';
import img2 from '../img/img2.png';


const IterationComponentQ = () => {


   const arr = [
      {src : '/img/img1.png', title : '아이폰10', price: 1000},
      {src : '/img/img2.png', title : '아이폰11', price: 2000},
      {src : '/img/img3.png', title : '아이폰12', price: 3000},
      {src : '/img/img4.png', title : '아이폰13', price: 4000},
  ]
   //1. state로 배열 담기
   const [list, setList] = useState(arr);
   const [img, setImg] = useState('');
   const newList = list.map((item) => {
      return <div onClick={() => handleClick(item)}>
         <img src = {item.src} alt='이미지' width="150" />
         <p>상품명: {item.title}</p>
         <p>가격: {item.price}</p>
      </div>
   });

   const handleClick = (item) => {
      // let show = document.querySelector(".show");
      // show.innerHTML = '<img src =' + item.src + ' key= {' + item.src +'} alt="이미지" width="200"/>'
      let tmp = <img src = {item.src} alt="이미지" width="200"/>;
      setImg(tmp)

   }


   return (
      <>
         <h3>이미지 데이터 반복</h3>
         <div className = "show">
            {img}
         </div>
         <div style={{display:"flex", justifyContent: "space-around"}}>
            {newList}
         </div>
      </>
   )
}

export default IterationComponentQ;

 

 

더보기

실습 2

- select는 컴포넌트 반복으로 option생성

- data는 state로 관리하고 화면에 li태그로 반복

- 셀렉트 체인지될 시 이벤트 객체 활용해서 선택된 값만 필터링해서 보여주기
 
import { useState } from 'react';
   /* 
   1, select는 컴포넌트 반복으로 option생성
   2, data는 state로 관리하고 화면에 li태그로 반복
   3, 셀렉트 체인지될 시 이벤트 객체 활용해서 선택된 값만 필터링해서 보여주기
   */

const IterationComponentQ2 = () => {
   
   const select = ['HTML', 'Javascript', 'CSS', 'Java', 'Oracle', 'Mysql'];
   
   
   //select는 변경되는 값이 아니므로 굳이 state를 사용할 필요가 없다.
   const makeOpt = select.map((item,index) => 
     <option key={index}> {item}</option>
   );
       
   const data = [
       {id: 1, type: 'Java', teacher: '이순신'},
       {id: 2, type: 'Java', teacher: '홍길자'},
       {id: 3, type: 'Javascript', teacher: '홍길동'},
       {id: 4, type: 'Oracle', teacher: '이순신'},
       {id: 5, type: 'Mysql', teacher: '이순신'},
       {id: 6, type: 'CSS', teacher: '박찬호'},
       {id: 7, type: 'HTML', teacher: 'coding404'},
   ];

   const [list, setList] = useState(data);
   
   const showList = list.map((item) => {
      return <li key={item.id}>{item.type} - {item.teacher}</li>
   })


   const handleChange = (e) => {
      let choice = e.target.value;
      
      //filter를 list에 걸어두면 list가 변하게 되므로
      //두 번째 실행부터 원하는 결과가 나타나지 않는다.
      //그러므로 변경되지 않는 원본 data를 이용해서 filter를 걸어야 한다.
      const tmpList = data.filter((item) => {
         return item.type === choice
      })

      setList(tmpList)

   }

   const [text, setText] = useState('');
   const inputText = (e) => {
      let tmp = e.target.value;
      setText(tmp);
   }

   const handleSearch = () => {
      let tmp = text.toUpperCase();
      
      const tmpList = data.filter((item) => (item.type.toUpperCase().includes(tmp)) || (item.teacher.toUpperCase().includes(tmp))
      )
      console.log(tmpList)
      setList(tmpList)
   }

   return (
      <>
         <h3>실습 2</h3>

         <div>
         <h5>검색기능 - 대소문자를 구분하지않고 teacher or type에 대응하는 데이터를 보여주기</h5>
         Search: <input type="text" onChange={inputText} value = {text}/>
         <button type="button" onClick={handleSearch}>검색</button>
         </div>
         <select onChange={handleChange}>
            {makeOpt}
         </select>

         <ul>
            {showList}
         </ul>
      </>
   )
}

export default IterationComponentQ2;

 

'국비교육과정 정리 > React' 카테고리의 다른 글

[React] ContextAPI  (0) 2023.01.25
[React] React에 CSS 적용하기  (0) 2023.01.18
[React] Hook(훅)  (0) 2023.01.17
[React] State, Event  (0) 2023.01.16
React  (0) 2023.01.15

댓글