웹 풀사이클 데브코스 TIL [Day 52] - 리액트 Props

JaeKyung Hwang·2024년 2월 2일
0
post-thumbnail

2024.02.02(금)

🔑key

리액트에서 array로 반환되는 모든 JSX 요소들에는 unique한 key 속성을 지정해야 함 🔗

Warning: Each child in a list should have a unique “key” prop.
  • array 요소가 정렬에 의해 이동하거나, 삽입되거나, 삭제될 수 있기 때문에 key를 사용하여 리액트에게 각 component가 어떤 array 항목에 해당하는지 알려주어야 함
    • 이 key는 추후에 리액트가 어떤 일이 일어났는지 추론하고 DOM tree를 올바르게 업데이트하는 데 도움이 됨
  • key source 🔗
    data sourcekey source
    database에서 가져온 datadatabase의 unique한 id
    local하게 생성된 data증가하는 숫자(incrementing counter), crypto.randomUUID(), uuid
  • key는 절대 바뀌면 안됨!

key로 사용하면 안되는 source

  • 배열의 index: key = {index}
    • key를 지정하지 않는 경우 리액트가 자동으로 사용하는 key 값이기도 하지만 array가 변경되었을 때 종종 bug가 생길 수 있기 때문에 권장하지 않음
  • 즉석으로 생성되는 값(fly): key = {Math.random()}
    • render 간 key가 일치하지 않아 매번 모든 components와 DOM이 다시 생성되는 문제가 발생
    • 속도가 느릴 뿐만 아니라 list item들 내의 사용자 입력도 손실됨

🎁Props

Component 간에 data를 전달하기 위한 속성 🔗

  • 함수의 매개변수와 비슷하다고 생각하면 됨
  • “부모 component → 자식 component”의 단방향 데이터 흐름

  • 자식 component에게 props 전달
    • 태그에 전달할 수 있는 props는 HTML Standard에 미리 정의되어 있지만, 사용자가 원하는 어떠한 props든 사용 가능!

      export default function Profile() {
        return (
          <Avatar
            **person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
            size={100}**
          />
        );
      }
  • 자식 component에서 props 읽기
    • 구조분해할당 사용

    • 함수처럼 default 값도 설정 가능 → 해당 prop 값이 빠졌거나 undefined일 때 default로 설정한 값이 사용됨

      function Avatar(**{ person, size = 100 }**) {
        // ...
      }
  • 하위 component로 props를 그대로 전달하는 경우 다음과 같이 spread 문법 사용 가능
    // 변경 전
    function Profile({ person, size, isSepia, thickBorder }) {
      return (
        <div className="card">
          <Avatar
            person={person}
            size={size}
            isSepia={isSepia}
            thickBorder={thickBorder}
          />
        </div>
      );
    }
    // 변경 후
    function Profile(props) {
      return (
        <div className="card">
          <Avatar {...props} />
        </div>
      );
    }
  • props도 state와 마찬가지로 read-only snapshot!
  • Component를 중첩해서 사용하는 경우
    • 보통 container나 visual wrapper 형태에 많이 사용
      <Card>
        <Avatar />
      </Card>
    • 부모 component에서 children이라는 prop, 즉 props.children에 자식 component가 담겨서 props를 그대로 전달할 수 있음
    • 이 때 props.children태그로 감싸서 return해야 함
      import Avatar from './Avatar.js';
      
      function Card({ children }) {
        return (
          <div className="card">
            {children}
          </div>
        );
      }
      
      export default function Profile() {
        return (
          <Card>
            <Avatar
              size={200}
              person={{ 
                name: 'Katsuko Saruhashi',
                imageId: 'YfeOqp2'
              }}
            />
          </Card>
        );
      }

📝실습

  • App.tsx

    // /* eslint-disable */
    import React, { useState } from 'react';
    // import logo from './logo.svg';
    import './App.css';
    
    interface Post {
      id: number;
      title: string;
      content: string;
      date: string;
    }
    
    /* React.FC는 주로 props 타입을 명시할 때 주로 사용 */
    const App: React.FC = () => {
      let title: string = "나의 블로그";
      let [post, setPost] = useState<Post[]>([
        {
          id: 0,
          title: "title1",
          content: "content1",
          date: "2030.12.31"
        },
        {
          id: 1,
          title: "title2",
          content: "content2",
          date: "2030.12.31"
        },
        {
          id: 2,
          title: "title3",
          content: "content3",
          date: "2030.12.31"
        }
      ]);
      let [like, setLike] = useState<number[]>([0, 0, 0]);
    
      let [detail, setDetail] = useState<boolean>(false);
    
      let [index, setIndex] = useState<number>(0);
    
      let [input, setInput] = useState<string>('');
    
      const handleLike = (postIdx: number): void => {
        const cpLike = [...like];
        cpLike[postIdx] += 1;
        setLike(cpLike);
      };
    
      const handleDetail = (postIdx: number): void => {
        setDetail(detail ? false : true);
        setIndex(postIdx);
      }
    
      const handleAddPost = (): void => {
        const newPost: Post = {
          id: post.length + 1,
          title: input,
          content: "Empty Content",
          date: new Date().toLocaleDateString()
        };
        setPost([...post, newPost]);
    
        const cpLike = [...like];
        cpLike.push(0);
        setLike(cpLike);
    
        setInput('');
      }
    
      const handleRemovePost = (postIdx: number): void => {
        let cpPost: Post[] = [...post];
        cpPost.splice(postIdx, 1);
        setPost(cpPost);
        // setPost(post.filter((p: Post) => p.id !== postIdx));  // id가 array index가 아닐 때 사용하면 좋을 듯
    
        const cpLike = [...like];
        cpLike.splice(postIdx, 1);
        setLike(cpLike);
      }
    
      return (
        <div className='App'>
          <div className='title-nav'>
            <h1>{title}</h1>
          </div>
    
          <div className="container">
            <div className="board">
              <input type="text" onChange={(e) => setInput(e.target.value)} />
              <button onClick={handleAddPost}>추가</button>
            </div>
          </div>
    
          <div className='container'>
            <div className='board'>
              {
                post.map((p: Post, idx) => {
                  return (
                    <div className='post' key={idx}>
                      <h3 onClick={() => handleDetail(idx)}>{p.title}</h3>
                      <p>{p.date}<span onClick={() => handleLike(idx)}>❤️</span>{like[idx]}</p>
                      <button className='del-btn' onClick={() => handleRemovePost(idx)}>삭제</button>
                    </div>
                  );
                })
              }
            </div>
          </div>
          {detail ? <Detail post={post} index={index}></Detail> : null}
          {/* <Timer></Timer> */}
        </div>
      );
    }
    
    const Timer: React.FC = () => {
      let [count, setCount] = useState<number>(0);
    
      const startCount = () => {
        setInterval(() => setCount(count => count + 1), 1000);
      };
    
      return (
        <div>
          <h1>타이머 : {count}</h1>
          <button onClick={startCount}>시작</button>
        </div>
      );
    }
    
    interface DetailProps {
      post: Post[];
      index: number;
    }
    
    const Detail: React.FC<DetailProps> = ({ post, index }) => {
      return (
        <div className='detail'>
          <h3>{post[index].title}</h3>
          <h4>{post[index].content}</h4>
          <p>{post[index].date}</p>
        </div>
      );
    }
    
    export default App;
  • App.css

    .del-btn {
      display: inline-block;
      padding: 10px 20px;
      font-size: 12px;
      background-color: white;
      border-color: pink;
      color: pink;
    }
    
    button {
      display: inline-block;
      padding: 10px 20px;
      font-size: 12px;
      font-weight: bold;
      border: 1px solid #9dc6f8;
      color: #9dc6f8;
      background-color: white;
      border-radius: 5px;
      margin-left: 10px;
      cursor: pointer;
    }
    
    button:hover {
      color: black;
      background-color: lightgray;
      border-color: lightgray;
    }
    
    input {
      padding: 10px;
      font-size: 16px;
      border: 1px solid #ccc;
      border-radius: 5px;
      margin: 10px 0px;
    }
    
    input:focus {
      border-color: #9dc6f8;
    }
    
    .container {
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    
    .board {
      background-color: aliceblue;
      border-radius: 8px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      overflow: hidden;
    }
    
    .post {
      border-bottom: 1px solid #282c34;
      padding: 15px;
      text-align: left;
    }
    
    .post:last-child {
      border-bottom: none;
    }
    
    .post h3 {
      color: #333;
    }
    
    .post p {
      color: #666;
    }
    
    div {
      box-sizing: border-box;
    }
    
    .detail {
      text-align: left;
      padding: 20px;
      background-color: rgb(143, 199, 199);
      margin-top: 50px;
    }
    
    .title-nav {
      background-color: #282c34;
      width: 100%;
      display: flex;
      padding: 20px;
      color: white;
    }
    
    .list {
      text-align: left;
      padding-left: 20px;
      border-bottom: 1px solid gray;
    }
    
    .App {
      text-align: center;
    }
    
    .App-logo {
      height: 40vmin;
      pointer-events: none;
    }
    
    @media (prefers-reduced-motion: no-preference) {
      .App-logo {
        animation: App-logo-spin infinite 20s linear;
      }
    }
    
    .App-header {
      background-color: #282c34;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: calc(10px + 2vmin);
      color: white;
    }
    
    .App-link {
      color: #61dafb;
    }
    
    @keyframes App-logo-spin {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

  • +) Event Bubbling: 이벤트가 상위 태그까지 전파되는 것
    callback함수에 (e) => {e.stopPropagation();} 추가해서 해결
profile
이것저것 관심 많은 개발자👩‍💻

0개의 댓글