working_helen
[React 일기장 프로젝트] 데이터 추가, 삭제, 수정 구현 본문
강의명 : 한입 크기로 잘라 먹는 리액트(React.js) - 섹션 5. React 기본 - 간단한 일기장 프로젝트
리액트 흐름에 따라 일기장 데이터 추가, 삭제, 수정 구현하기
1. Event & Data 흐름
2. 데이터 추가 : onCreate()
3. 데이터 삭제 : onDelete()
4. 데이터 수정 : isEdit, localContent, handleQuitEdit, handleEdit, onEdit
1. Event & Data 흐름
- 역방향 이벤트 흐름 : Event는 아래에서 위로
- 단방향 데이터 흐름 : Data는 위에서 아래로
- 공통 부모 App.js에서 [State 변수, State 변경 함수] 생성
- DiaryEditor는 App에서 props로 전달한 setData 함수 호출
- DiaryList는 App에서 전달받는 props의 값이 변화되어 리렌더링
전체 State(일기장 배열)를 변경하는 함수
==> 부모 컴포넌트 App.js에서 구현 + 자식 컴포넌트에게 props로 전달
DiaryEditor에서 새로운 일기 작성
→ App에서 props로 전달했던 setData가 호출
→ App의 일기장 배열에 새로운 데이터가 추가 = App의 state 변화 = DiaryList로 전달되는 props 변화
→ DiaryList 리렌더링
2. 데이터 추가 : onCreate()
1) App.js에 [State, setState], onCreate 함수 생성
- data : 일기장 배열 State 변수, Array 타입
- useState([ ]) : 빈 배열로 초기화
- onCreate : 새로운 일기 item을 생성하고, setData를 이용해 data를 변경
- 일기장마다 서로 다른 고유한 id를 부여하기 위해 id reference 변수 생성
- 일기장 추가될 때마다 id 값을 1씩 증가
2) DiaryEditor.js에 onCreate 함수를 props로 전달
- 일기장 입력을 받는 컴포넌트에, State 변경 함수를 props로 전달
- DiaryEditor.js에서 새롭게 생성된 일기장을 저장할 때, onCreate 함수 실행
- 실행 결과가 App.js에 있는 data State를 변화시킴 (역방향 이벤트 흐름)
3) DiaryList.js에 State 변수를 props로 전달
- App.js에서 data State가 변화할 때, DiaryList도 리렌더링
- 새롭게 data에 추가된 일기장 원소가 DiaryList의 화면 출력 결과에도 반영
[ App.js 코드 ]
import './App.css';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
import { useState, useRef } from 'react';
function App() {
// State 변수 생성
// 빈 배열을 초기값으로
const [data, setData] = useState([]);
// 일기장 id reference 변수 생성
// dataId의 초기값을 0으로 설정
const dataId = useRef(0);
// 새로운 일기 item을 추가하는 함수
const onCreate = (author, content, emotion)=>{
const created_date = new Date().getTime();
const newItem = {
author, content, emotion, created_date,
id: dataId.current, // 현재 id값 접근
}
dataId.current +=1; // id값 1 증가
// 기존 data 배열 앞에, newItem 추가
setData([newItem, ...data])
};
return (
<div className="App">
<DiaryEditor onCreate={onCreate}/>
{/* 새로운 일기장을 생성하는 함수를 props로 전달 */}
{/* html 태그 리턴 */}
<DiaryList diaryList={data}/>
{/* html 태그 리턴 */}
</div>
);
}
export default App;
[ DiaryEditor.js 코드 ] (일부)
// onCreate 함수를 props로 전달받음
const DiaryEditor = ({onCreate}) => {
...
const handleSubmit = () => {
if(state.author.length<1){
authorInput.current.focus();
return;
}
if(state.content.length<5){
alert("본문을 5글자 이상 적어주세요.");
contentInput.current.focus();
return;
}
// 새롭게 생성된 일기장 저장하는 곳에서 onCreate 호출
onCreate(state.author, state.content, state.emotion);
alert("저장 성공");
}
3. 데이터 삭제 : onDelete()
1) App.js에 onDelete 함수 생성
- 일기 item을 제거 = 기존 일기장 배열을 원소 하나가 삭제된 배열로 업데이트
- 배열.filter((원소) => 조건) : 조건을 만족하는 원소들만 걸러 새로운 배열로 반환
- 일기장 배열에서 targetId에 해당하는 일기장만 제외한 새로운 일기장 배열 생성
- setData로 현재 data를 새로운 배열로 대체
2) DiaryList.js → DiaryItem.js에 onDelete 함수를 props로 전달
- DiaryItem에 '삭제하기' button 컴포넌트 생성
- window.confirm(문구) : 문구와 함께 윈도우 확인창 띄움, '확인' 누르면 true 반환
- '삭제하기' → '확인' 하면, 현재 일기장의 id가 onDelete에 targetId로 전달되어 일기장 삭제
[ App.js 코드 ]
import './App.css';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
import { useState, useRef } from 'react';
function App() {
// State 변수 생성
const [data, setData] = useState([]);
// 일기장 id reference 변수 생성
const dataId = useRef(0);
// 새로운 일기 item을 추가하는 함수
const onCreate = (author, content, emotion)=>{
const created_date = new Date().getTime();
const newItem = {
author, content, emotion, created_date,
id: dataId.current,
}
dataId.current +=1;
setData([newItem, ...data])
};
// 일기 item을 제거하는 함수
const onDelete = (targetId)=>{
const newData = data.filter((item) => item.id != targetId);
setData(newData); // data 배열을 새로운 배열로 업데이트
};
return (
<div className="App">
<DiaryEditor onCreate={onCreate}/>
{/* 새로운 일기장을 생성하는 함수를 props로 전달 */}
{/* html 태그 리턴 */}
<DiaryList diaryList={data} onDelete={onDelete}/>
{/* 일기장을 삭제하는 함수를 props로 전달 */}
{/* html 태그 리턴 */}
</div>
);
}
export default App;
[ DiaryList.js 코드 ] 일부
// onDelete 함수도 props로 받고
const DiaryList = ({diaryList, onDelete}) =>{
...
// 다시 props로 전달
<DiaryItem key={item.id} {...item} onDelete={onDelete}/>
[ DiaryItem.js 코드 ]
// props = {author, content, created_id, emotion, id, onDelete}
const DiaryItem = (props) => {
// DiaryItem 리턴
return (
<div className = 'DiaryItem'>
<div className="info">
<span>작성자 : {props.author}</span>
<span className="date"> | {new Date(props.created_date).toLocaleDateString()}</span>
<div>감정 점수: {props.emotion}</div>
</div>
<div className="content">
<div>{props.content}</div>
</div>
{/* 삭제하기 button 생성 */}
<button
onClick={()=>{
if(window.confirm(`${props.id}번째 일기를 삭제하시겠습니까?`)){
props.onDelete(props.id);
}
}}>
삭제하기</button>
</div>
);
};
export default DiaryItem;
4. 데이터 수정 : isEdit, localContent, handleQuitEdit, handleEdit, onEdit
1) DiaryItem.js에 수정여부를 의미하는 State 변수 생성
- 수정하는 상황인지 아닌지에 따라 다른 UI 화면을 구성하기 위해, 각 일기장마다 수정여부 값을 저장할 State 변수 생성
- 수정은 각 일기장마다 일어나는 것이기 때문에 DiaryItem.js에서 State 생성
- 수정여부 false로 초기화, 수정여부의 true/false 값을 반전시키는 toggleIsEdit 함수 구현
const [isEdit, setIsEdit] = useState(false);
const toggleIsEdit = () => setIsEdit(!isEdit);
2) 수정할 text input을 받을 State 변수 생성
- <input> 태그 - State 형태
- 기존의 content로 초기화 = 기존의 content를 시작으로 수정할 수 있도록
const [localContent, setLocalContent] = useState(props.content);
3) 수정여부에 따른 일기장 text 구성
- 수정X : 일기장의 content를 보여주는 기존 화면
- 수정O : 사용자가 글을 작성할 수 있도록 <textarea> 태그 이용
- onChange = setLocalContent 함수로 localContent 값을 변화, 그 결과를 textarea에 표시
{/* 수정 여부에 따라 다른 창이 나오도록 */}
<div className="content">
{isEdit ? (
<>
<textarea
// 입력받은 내용으로 textarea 채우기
ref={localContentRef}
value = {localContent}
onChange = {(e)=>setLocalContent(e.target.value)}
/>
</>
) : (<>{props.content}</>)}
</div>
4) 수정여부에 따른 일기장 button 구성
- 수정X : '삭제하기', '수정하기' button
- 수정O : '수정 취소', '수정 완료' button
{/* 수정 여부에 따라 다른 버튼이 나오도록 */}
{isEdit ? (<>
<button onClick={handleQuitEdit}>수정 취소</button>
<button onClick={handleEdit}>수정 완료</button>
</>) : (<>
<button onClick={handleDelete}>삭제하기</button> // 데이터 삭제 수행
<button onClick={toggleIsEdit}>수정하기</button> // 수정여부 true, flase를 변경
</>)}
5) 각 button의 onClick 함수 구현
handleQuitEdit : content 수정을 취소하는 함수
- 수정여부 false로 변경
- setLocalContent 함수를 이용해 localContent를 수정 전 기존 content로 원상복구
// 수정 취소 버튼 click 했을 때 함수
const handleQuitEdit = ()=>{
// 수정 여부 변경하고, 기존 content로 복구
toggleIsEdit();
setLocalContent(props.content);
};
handleEdit : content를 수정하는 함수
- 전체 일기장 배열을 수정해야 → App.js에 일기장 배열 State 변경 함수를 정의
- onEdit : targetId에 해당하는 일기장의 content를 newContent로 변경하는 함수
// [ App.js 코드 ]
// 일기 content를 수정하는 함수 = 새로운 일기장 배열 리턴
const onEdit = (targetId, newContent)=>{
setData(
data.map((item)=>
// targetId에 해당되면 content를 업데이트한 객체를 반환
// targetId가 아니면 원래 item 그대로 반환
item.id === targetId ? {...item, content: newContent} : item
)
);
- onEdit(현재 일기장 id, 입력한 수정 후 content) 실행해서 현재 일기장의 content 내용을 수정
// [ DiaryItem.js 코드 ]
// focus를 위해 reference 객체 만들기
// focus할 <textarea> 태그에 ref로 추가
const localContentRef = useRef();
const handleEdit = () =>{
// 수정 후 입력이 올바른지 확인
if (localContent.length < 5){
localContentRef.current.focus();
return;
}
if(window.confirm(`${props.id}번째 일기를 수정하시겠습니까?`)){
// 현재 id 일기장의 content를 localContent로 바꾸기
// 수정여부 false로 변경
props.onEdit(props.id, localContent);
toggleIsEdit();
}
※ 최종 코드
[ App.js 코드 ]
import './App.css';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
import { useState, useRef } from 'react';
function App() {
// State 변수 생성
// data = 일기장 배열 State 변수
// 빈 배열을 초기값으로
const [data, setData] = useState([]);
// -----------------------------------------------------
// 일기장 id reference 변수 생성
// dataId의 초기값을 0으로 설정
const dataId = useRef(0);
// 새로운 일기 item을 추가하는 함수
const onCreate = (author, content, emotion)=>{
const created_date = new Date().getTime();
const newItem = {
author, content, emotion, created_date,
id: dataId.current, // 현재 id값 접근
}
dataId.current +=1; // id값 1 증가
// 기존 data 배열 앞에, newItem 추가
setData([newItem, ...data])
};
// -----------------------------------------------------
// 일기 item을 제거하는 함수
const onDelete = (targetId)=>{
const newData = data.filter((item) => item.id != targetId);
setData(newData);
};
// -----------------------------------------------------
// 일기 content를 수정하는 함수
// 새로운 일기장 배열 리턴
const onEdit = (targetId, newContent)=>{
setData(
data.map((item)=>
// targetId에 해당되면 content를 업데이트한 객체를 반환
// targetId가 아니면 원래 item 그대로 반환
item.id === targetId ? {...item, content: newContent} : item
)
);
};
// -----------------------------------------------------
// App의 리턴
return (
// App.css 적용을 위해 className을 'App'으로 지정
<div className="App">
<DiaryEditor onCreate={onCreate}/>
{/* 새로운 일기장을 생성하는 함수를 props로 전달 */}
{/* html 태그 리턴 */}
<DiaryList diaryList={data} onDelete={onDelete} onEdit={onEdit}/>
{/* 일기장을 삭제하는 함수를 props로 전달 */}
{/* html 태그 리턴 */}
</div>
);
}
export default App;
[ DiaryEditor.js 코드 ]
// 새로운 일기장 입력을 받는 컴포넌트
import { useRef, useState } from "react";
// onCreate 함수를 props로 전달받음
const DiaryEditor = ({onCreate}) => {
//State 변수 생성 : 실행에 따라 값이 변하는 변수들 객체로 모으기
const [state, setState] = useState({
// State 초기값 지정
author:"",
content:"",
emotion: 5,
});
//Reference 객체 생성
const authorInput = useRef();
const contentInput = useRef();
// 일기장 입력값을 실시간으로 UI에 표현하는 함수
// event e에 대해 하나의 state만 변화
const handleChangeState = (e) => {
setState({
// 전체 State 객체를 Spread
// 필요한 key만 value 변경
...state,
[e.target.name]: e.target.value,
});
};
// 일기장 저장 함수
const handleSubmit = () => {
// 잘못된 입력에 focus 주기
if(state.author.length<1){
authorInput.current.focus();
return;
}
if(state.content.length<5){
alert("본문을 5글자 이상 적어주세요.");
contentInput.current.focus();
return;
}
// 새롭게 생성된 일기장 저장할 때, onCreate 호출
onCreate(state.author, state.content, state.emotion);
alert("저장 성공");
}
// DiaryEditor의 리턴
return (
<div className="DiaryEditor">
<h2>오늘의 일기</h2>
{/* 입력 받기 */}
<div>
<input
ref = {authorInput} //focus 함수의 reference 지정
name = "author" //변수명
value={state.author} //변수값
onChange={handleChangeState}
/>
</div>
<div>
<textarea
ref = {contentInput}
name = "content"
value={state.content}
onChange={handleChangeState}
/>
</div>
<div>
오늘의 감정 점수 :
<select
name="emotion"
value={state.emotion}
onChange={handleChangeState}>
{/* select 값 후보들 */}
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
<option value={5}>5</option>
</select>
</div>
{/* 저장하기 */}
<div>
<button onClick={handleSubmit}>일기 저장하기</button>
</div>
</div>
)
};
export default DiaryEditor;
[ DiaryList.js 코드 ]
// 저장된 일기장 배열을 UI 화면에 표현해주는 컴포넌트
import DiaryItem from './DiaryItem.js'
const DiaryList = ({diaryList, onDelete, onEdit}) =>{
// DiaryList의 리턴
return (
<div className = 'DiaryList'>
<h2>일기 리스트</h2>
<h4>{diaryList.length}개의 일기가 있습니다.</h4>
<div>
{diaryList.map((item)=>(
// map : 배열의 각 원소마다 함수를 적용한 결과 새로운 배열 반환
// DiaryItem으로 개별 일기장을 props 전달
<DiaryItem key={item.id} {...item} onDelete={onDelete} onEdit={onEdit}/>
// html 태그 리턴
))}
</div>
</div>
);
};
// defualt Props 정의
DiaryList.defaultProps = {
diaryList: [],
}
export default DiaryList;
[ DiaryItem.js 코드 ]
// 일기장 하나하나를 구현하는 컴포넌트
import { useState, useRef } from 'react';
// props = {author, content, created_id, emotion, id, onDelete, onEdit} 받음
const DiaryItem = (props) => {
// 삭제하기 버튼 click 했을 때 함수
const handleDelete = ()=>{
if(window.confirm(`${props.id}번째 일기를 삭제하시겠습니까?`)){
props.onDelete(props.id);
}
};
// -----------------------------------------------------
// 수정 여부를 저장하는 State 변수 생성
// false로 초기화
const [isEdit, setIsEdit] = useState(false);
const toggleIsEdit = () => setIsEdit(!isEdit);
// 수정할 text input을 받을 State 변수 생성
// 수정 전엔 기존 content로 초기화
const [localContent, setLocalContent] = useState(props.content);
// 수정 취소 버튼 click 했을 때 함수
const handleQuitEdit = ()=>{
// 수정 여부 변경하고, 기존 content로 복구
toggleIsEdit();
setLocalContent(props.content);
};
// focus를 위해 reference 객체 만들기
// focus할 요소의 태그에 ref로 추가
const localContentRef = useRef();
// 수정 완료 버튼 click 했을 때 함수
const handleEdit = () =>{
// 수정 후 입력이 올바른지 확인
if (localContent.length < 5){
localContentRef.current.focus();
return;
}
if(window.confirm(`${props.id}번째 일기를 수정하시겠습니까?`)){
// 현재 id 일기장의 content를 localContent로 바꾸기
// 수정 폼 종료
props.onEdit(props.id, localContent);
toggleIsEdit();
}
};
// -----------------------------------------------------
// DiaryItem 리턴
return (
<div className = 'DiaryItem'>
<div className="info">
<span>작성자 : {props.author}</span>
<span className="date"> | {new Date(props.created_date).toLocaleDateString()}</span>
<div>감정 점수: {props.emotion}</div>
</div>
<div className="content">
{/* 수정 여부에 따라 다른 창이 나오도록 */}
{isEdit ? (
<><textarea
// 입력받은 내용으로 textarea 채우기
// textarea를 focus할 것이므로 여기에 reference 변수 추가
ref={localContentRef}
value = {localContent}
onChange = {(e)=>setLocalContent(e.target.value)}
/></>
) : (<>{props.content}</>)}
</div>
{/* 수정 여부에 따라 다른 버튼이 나오도록 */}
{isEdit ? (<>
<button onClick={handleQuitEdit}>수정 취소</button>
<button onClick={handleEdit}>수정 완료</button>
</>) : (<>
<button onClick={handleDelete}>삭제하기</button>
<button onClick={toggleIsEdit}>수정하기</button>
</>)}
</div>
);
};
export default DiaryItem;
'외부 수업 > React 스터디' 카테고리의 다른 글
[React 일기장 프로젝트] API에서 데이터 로드 (0) | 2024.01.16 |
---|---|
[React 일기장 프로젝트] useEffect로 Lifycycle 제어 (0) | 2024.01.16 |
[React 일기장 프로젝트] UI 구성하기 (0) | 2024.01.15 |
[React 기초] State / Props (0) | 2024.01.15 |
[React 기초] Node.js와 React.js 기본개념 (0) | 2024.01.15 |