2024.02.02(금)
리액트에서 array로 반환되는 모든 JSX 요소들에는 unique한 key 속성을 지정해야 함 🔗
Warning: Each child in a list should have a unique “key” prop.
data source | key source |
---|---|
database에서 가져온 data | database의 unique한 id |
local하게 생성된 data | 증가하는 숫자(incrementing counter), crypto.randomUUID(), uuid |
⛔ key로 사용하면 안되는 source
- 배열의 index:
key = {index}
- key를 지정하지 않는 경우 리액트가 자동으로 사용하는 key 값이기도 하지만 array가 변경되었을 때 종종 bug가 생길 수 있기 때문에 권장하지 않음
- 즉석으로 생성되는 값(fly):
key = {Math.random()}
- render 간 key가 일치하지 않아 매번 모든 components와 DOM이 다시 생성되는 문제가 발생
- 속도가 느릴 뿐만 아니라 list item들 내의 사용자 입력도 손실됨
Component 간에 data를 전달하기 위한 속성 🔗
태그에 전달할 수 있는 props는 HTML Standard에 미리 정의되어 있지만, 사용자가 원하는 어떠한 props든 사용 가능!
export default function Profile() {
return (
<Avatar
**person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}**
/>
);
}
구조분해할당 사용
함수처럼 default 값도 설정 가능 → 해당 prop 값이 빠졌거나 undefined
일 때 default로 설정한 값이 사용됨
function Avatar(**{ person, size = 100 }**) {
// ...
}
// 변경 전
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>
);
}
<Card>
<Avatar />
</Card>
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);
}
}
(e) => {e.stopPropagation();}
추가해서 해결