import { useState } from "react";
import "./TodoList.css";
/*
이 예제에서 구현해볼 것:
- todos 배열을 map으로 순회하며 li 요소 생성
- 각 li에 key={todo.id} 필수 설정
- 상태 변경시 자동 리렌더링
- 빈 배열일 때 조건부 렌더링
*/
function TodoList() {
//로직 : javascript
// To do list 상태 관리 - react hook useState
// to do list 상태 정보를 저장하는 todos 변수, to do list 상태를 변화시키기 위한 함수 setTodos
// 아래 useState(초기값) 은 실제로는 API Server 연동을 통해 확보하지만 지금은 직접 입력한다 (JSON Array)
const [todos, setTodos] = useState([
{ id: 1, text: "점심 먹기", completed: false },
{ id: 2, text: "카드 놀이", completed: true },
]);
//새로운 to do 입력 상태
const [newTodo, setNewTodo] = useState("");
//할 일 to do 추가 함수
const addTodo = () => {
if (newTodo.trim()) {
const todo = {
id: Date.now(), //시간정보를 이용해 고유 아이디 생성
text: newTodo,
completed: false,
};
setTodos([...todos, todo]); //기존 배열에 새 항목 추가한 새 배열 생성해 할당
setNewTodo(""); //입력창 초기화
}
};
//To do 항목 삭제 함수
const deleteTodo = (id) => {
//console.log("삭제할 to do id " + id);
//to do list 중 해당 id를 가진 to do 요소를 삭제하고 to do list 를 리렌더링 하기 위해서는 react hook state를 이용해야 한다 => 변경 위해서는 useState hook 함수가 반환한 두번째 요소인 set계열 함수를 이용해 업데이트해야 한다.
// filter 함수는 todo.id != id 이 true 이면 새 배열 요소로 추가
//다시 말하면 삭제할 to do id 이면 != 에 의해 false 가 나올 것이고 이는 새 배열 요소에서 제외됨 -> 즉 삭제 효과
setTodos(todos.filter((todo) => todo.id != id));
};
//화면 렌더링 : jsx
return (
<div className="todo-list">
<h2>할 일 목록</h2>
{/* 새 할 일 추가 */}
<div className="add-todo">
<input
type="text"
placeholder="새 할 일을 입력하세요"
value={newTodo} //입력 폼 요소 값 value를 리액트 state 상태 값으로 관리
onChange={(event) => setNewTodo(event.target.value)} //입력 요소 value가 change 변경될 때 react state hook 의 함수로 상태를 변경 -> 앱에 리렌더링
//아래는 key 를 눌렀을 때 발생하는 이벤트
//keyDown 이벤트 발생 시 아래 화살표 함수 arrow function 를 등록 binding
//addTodo가 아니라 addTodo() 로 명시한 이유는 엔터키 이벤트가 발생 시에 바로 호출 즉 실행되어 todo 를 추가하기 위해 즉 구현부 내에서는 실행을 해야 하므로 반드시 () 를 명시해야 됨
//즉 addTodo() = 실행 내용 (구현부 내)
onKeyDown={(e) => e.key === "Enter" && addTodo()} //key를 눌렀을 때 발생하는 이벤트
/>
{/* 아래는 버튼이 클릭되어지면 (클릭 이벤트) 실행될 함수를 binding 등록 */}
{/* onClick 의 addTodo의 구현부 (if() ~)를 실행하라고 등록함 */}
{/* 함수를 호출하는 게 아니라 호출될 함수 객체를 등록하는 것임 */}
<button onClick={addTodo}>추가</button>
{/* onChange={(event) => setNewTodo(event.target.value)} */}
</div>
{/* 리스트 렌더링 - map() 메서드 사용 */}
<ul className="todo-items">
{todos.map((todo) => (
//리액트 리스트에서는 key를 반드시 설정해야 함 -> 오류 방지 및 성능 향상
<li
key={todo.id}
className={`todo-item ${todo.completed ? "completed" : undefined}`}
>
<span className="todo-text">{todo.text}</span>
{/* 삭제 버튼이 클릭되면(클릭이벤트 발생 시) 실행될 화살표함수를 등록
이후 삭제 버튼을 클릭하면 함수 구현부에서 deleteTodo 함수를 실행하여 to do item을 삭제
*/}
<button className="delete-btn" onClick={() => deleteTodo(todo.id)}>
삭제
</button>
{/* const deleteTodo = (id) => {
console.log("삭제할 to do id " + id);
};
*/}
</li>
))}
</ul>
{/* to do list 가 비어 있을 때 아래를 보이도록 조건 처리한다 */}
{todos.length === 0 && <p className="empty-message">할 일이 없습니다!</p>}
<div className="debug">
<p>총 할 일: {todos.length}개</p>
</div>
</div>
);
}
export default TodoList;
function TodoList() {
//로직 : javascript
// To do list 상태 관리 - react hook useState
// to do list 상태 정보를 저장하는 todos 변수, to do list 상태를 변화시키기 위한 함수 setTodos
// 아래 useState(초기값) 은 실제로는 API Server 연동을 통해 확보하지만 지금은 직접 입력한다 (JSON Array)
const [todos, setTodos] = useState([
{ id: 1, text: "점심 먹기", completed: false },
{ id: 2, text: "카드 놀이", completed: true },
]);
//새로운 to do 입력 상태
const [newTodo, setNewTodo] = useState("");
//할 일 to do 추가 함수
const addTodo = () => {
if (newTodo.trim()) {
const todo = {
id: Date.now(), //시간정보를 이용해 고유 아이디 생성
text: newTodo,
completed: false,
};
setTodos([...todos, todo]); //기존 배열에 새 항목 추가한 새 배열 생성해 할당
setNewTodo(""); //입력창 초기화
}
return ( ... );
};
예제
import { useState } from "react";
import CoreConcept from "./components/CoreConcept.jsx";
import Header from "./components/Header/Header.jsx";
import TabButton from "./components/TabButton.jsx";
import { CORE_CONCEPTS } from "./data/data.js";
import { EXAMPLES } from "./data/data.js";
function App() {
// state 상태 관리 : useState Hook 을 이용
// selectedTopic : 사용자가 어떤 탭을 선택했는지 탭제목을 저장,
// setSelectedTopic : 사용자가 탭을 선택했을 때 React 에 탭 정보 변경을 알려서 리렌더링 되도록 하는 함수
const [selectedTopic, setSelectedTopic] = useState();
//탭 버튼 클릭시 호출되는 함수 (탭버튼 이벤트 핸들러)
//탭버튼 정보 : components, jsx, props, state 중 하나
//(data.js 내의 json 정보)
function handleSelect(selectedButton) {
//react state hook 함수를 호출해 화면 리렌더링을 한다
setSelectedTopic(selectedButton);
}
console.log("Component rendering");
let tabContent = <p>주제를 선택하세요</p>;
//선택된 토픽이 있으면 해당 내용을 표시
if (selectedTopic) {
tabContent = (
<div id="tab-content">
<h3>{EXAMPLES[selectedTopic].title}</h3>
<p>{EXAMPLES[selectedTopic].description}</p>
<pre>{EXAMPLES[selectedTopic].code}</pre>
</div>
);
}
return (
<div>
{/* Header 컴포넌트 렌더링 */}
<Header />
<main>
{/* ===== 섹션 1: Core Concepts ===== */}
<section id="core-concepts">
<h2>Core Concepts</h2>
<ul>
{/* map()을 사용한 동적 리스트 렌더링 */}
{/* key prop은 React가 각 항목을 추적하는데 필요
{CORE_CONCEPTS.map((conceptItem) => {
return (
<CoreConcept
key={conceptItem.title}
image={conceptItem.image}
title={conceptItem.title}
description={conceptItem.description}
/>
);
})}
*/}
{/* 스프레드 연산자로 효율적으로 props 전달, 화살표 함수를 간략하게 표현 {}가 아니라 () 형태로
{...conceptItem} 표현은 image={conceptItem.image} title={conceptItem.title}
description={conceptItem.description} 와 동일한 표현임
화살표 함수 { } return 시에 return keyword 를 명시
화살표 함수 ( ) 리턴 생략 => 화살표 함수 축약 형태이고 실행문이 한 문장일 때 사용할 수 있다.
*/}
{CORE_CONCEPTS.map((conceptItem) => (
<CoreConcept key={conceptItem.title} {...conceptItem} />
))}
</ul>
</section>
{/* ===== 섹션 2: Examples ===== */}
<section id="examples">
<h2>Examples</h2>
<menu>
{/* 각 TabButton에 이벤트 핸들러와 선택 상태 전달 */}
<TabButton
isSelected={selectedTopic === "components"}
onSelect={() => handleSelect("components")}
>
Components
</TabButton>
<TabButton
isSelected={selectedTopic === "jsx"}
onSelect={() => handleSelect("jsx")}
>
jsx
</TabButton>
<TabButton
isSelected={selectedTopic === "props"}
onSelect={() => handleSelect("props")}
>
props
</TabButton>
<TabButton
isSelected={selectedTopic === "state"}
onSelect={() => handleSelect("state")}
>
state
</TabButton>
</menu>
{/* 조건부로 렌더링되는 컨텐츠 */}
{tabContent}
</section>
</main>
</div>
);
}
export default App;
/*
CoreConcept 컴포넌트
- Props : 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달, 자식 컴포넌트에서는 구조분해할당 destructuring 받아서 처리
- 컴포넌트 재사용성 연습을 위해 CoreConcept 컴포넌트를 정의
*/
function CoreConcept({ image, title, description }) {
return (
<li>
<img src={image} alt={title} />
<h3>{title}</h3>
<p>{description}</p>
</li>
);
}
export default CoreConcept;
3. React LifeCycle
3-1. React LifeCycle
마운트 시에만 실행 (의존성 빈 배열) : componentDidmount
api 호출, 초기 데이터 설정 등에 사용
useEffect cleanup : 언마운트 시에 실행할 작업을 화살표 함수로 정의해 리턴
useEffect(() => {
//API 호출, 초기 데이터 설정 등에 사용
console.log("1번 useEffect:컴포넌트가 마운트되었습니다");
//useEffect cleanup : 언마운트 시에 실행할 작업을 화살표 함수로 정의해 리턴한다
return () => {
console.log(
"1번 useEffect : 컴포넌트가 언마운트됩니다! 정리 작업 cleanup을 수행합니다"
);
};
}, []); //빈 의존성 배열 -> 마운트 시에 1회만 실행
모든 렌더링마다 실행 (의존성 배열 없음)
거의 사용하지 않는다. (성능 이슈)
useEffect(() => {
//거의 사용하지 않음 - 성능 이슈 주의
console.log("2번 useEffect:렌더링이 일어났습니다");
}); //의존성 배열을 넣지 않은 상태
특정 상태 변경 시 실행 : componentDidUpdate
state or props 가 변경 시 렌더링될 때 실행
useEffect(() => {
//state or props 가 변경 시 렌더링될 때 실행
console.log(`3번 useEffect:count가 변경됨: ${count}`);
}, [count]); //의존성 배열을 넣은 상태
//즉 count가 바뀌면 실행됨