working_helen

[React 일기장 프로젝트] 데이터 추가, 삭제, 수정 구현 본문

외부 수업/React 스터디

[React 일기장 프로젝트] 데이터 추가, 삭제, 수정 구현

HaeWon_Seo 2024. 1. 16. 15:52

강의명 : 한입 크기로 잘라 먹는 리액트(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;

 

 

일기 저장
일기 수정