🥹

아좌잣 홧팅이닷!

토독토독..💻

개발새발🐶🐾🐥🐾/React

[React Project: 간단한 일기장(6)] 최적화1 - useMemo

SU_VIN 2023. 1. 5. 17:33
반응형

 

 

 

한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 - 인프런 | 강의

개념부터 독특한 프로젝트까지 함께 다뤄보며 자바스크립트와 리액트를 이 강의로 한 번에 끝내요. 학습은 짧게, 응용은 길게 17시간 분량의 All-in-one 강의!, - 강의 소개 | 인프런...

www.inflearn.com

 

 

 

 

 

[React Project: 간단한 일기장(5)] 배열사용해서 데이터 조작하기3- 데이터 삭제및 수정

한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 - 인프런 | 강의 개념부터 독특한 프로젝트까지 함께 다뤄보며 자바스크립트와 리액트를 이 강의로 한 번에 끝내요. 학습은 짧게, 응

su-vin25.tistory.com

이어서...

 


지금까지의 코드 ↓

더보기

App.js

import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
import './App.css';
import { useEffect, useRef, useState } from 'react';

function App() {
  
  const [data,setData]=useState([]);

  const dataId = useRef(0);

  const getData= async()=>{
    const res = await fetch("https://jsonplaceholder.typicode.com/comments").then((res)=>res.json());
    console.log(res);

    const initData= res.slice(0,20).map((it)=>{
      return{
        author: it.email,
        content: it.body,
        emotion: Math.floor(Math.random()*5)+1,
        created_date : new Date().getTime(),
        id: dataId.current++,
      }
    })

    setData(initData);
  }

  useEffect(()=>{
    getData();
  },[]);

  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])
  }

  const onDelete = (targetId)=>{
    console.log(`${targetId}가 삭제되었습니다`);
    const newDiaryList= data.filter((it)=>it.id!==targetId);
    setData(newDiaryList);
  }

  const onEdit = (targetId,newContent)=>{
    setData(
      data.map((it)=>it.id===targetId?{...it,content:newContent}:it)
    )
  }

  return (
    <div className="App">
    <DiaryEditor onCreate={onCreate}/>
    <DiaryList diaryList={data} onDelete={onDelete} onEdit={onEdit}/>
    </div>
  );
}

export default App;

DiaryEditor.js

import React, { useRef,useState } from "react";

const DiaryEditor=({onCreate})=>{
    
    const authorInput = useRef();
    const contentInput = useRef();

    const [state,setState]= useState({
        author:"",
        content:"",
        emotion:1,
    });

    const handleChangeState=(e)=>{
        setState({
            ...state,
            [e.target.name]:e.target.value,
        })
    };
    
    const handleSubmit = ()=>{
        if(state.author.length<1){
            authorInput.current.focus();
            return;
        }
        if(state.content.length<5){
            contentInput.current.focus();
            return;
        }
        onCreate(state.author,state.content,state.emotion);
        alert("저장 성공!");
        setState({
            author:"",
            content:"",
            emotion:1
        })
    }


    return(
        <div className="DiaryEditor">
        <h2>오늘의 일기</h2>
        <div>
            <input 
                ref={authorInput}
                name="author" 
                value={state.author} 
                onChange={handleChangeState}/>
        </div>
        <div>
            <textarea
                ref={contentInput} 
                name="content" 
                value={state.content} 
                onChange={handleChangeState}/>
        </div>
        <div>
        <span>오늘의 감정점수 : </span>
            <select name="emotion" value={state.emotion} onChange={handleChangeState}>
                <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

import DiaryItem from "./DiaryItem";

const DiaryList = ({diaryList,onDelete,onEdit})=>{
    return(
        <div className="DiaryList">
        <h2>일기 리스트</h2>
        <h4>{diaryList.length}개의 일기가 있습니다</h4>
        <div>
            {diaryList.map((it)=>(
                <DiaryItem key={it.id} {...it} onDelete={onDelete}
                onEdit={onEdit}/>
            ))}
        </div>
        </div>
    )
}

DiaryList.defaultProps={
    diaryList: [],
}


export default DiaryList;

DiaryItem.js

import { useRef, useState } from "react";

const DiaryItem=({
    author,
    content,
    emotion,
    created_date,
    id,
    onDelete,
    onEdit,
})=>{

    const localContentInput = useRef(); 

    const [isEdit,setIsEdit]=useState(false);
    const toggleIsEdit=()=>setIsEdit(!isEdit);


    const [localContent,setLocalContent] = useState(content);

    const handelDelete =()=>{
        if(window.confirm(`${id}번째 일기를 삭제하시겠습니까?`)){
            onDelete(id);
        }
    }
    const handleQuitEdit = ()=>{
        setIsEdit(false);
        setLocalContent(content);
    }
    
    const handleEdit = ()=>{
        if(localContent.length<5){
            localContentInput.current.focus();
            return;
        }

        if(window.confirm(`${id}번째 일기를 수정하시겠습니까?`)){
            onEdit(id,localContent);
            toggleIsEdit();
        }
        
    }

    return( 
        <div className="DiaryItem">
            <div className="info">
                <span>작성자 :{author} | 감정점수 :{emotion}</span>
                <br/>
                <span className="date">{new Date(created_date).toLocaleDateString()}</span>
            </div>
            <div className="content">
                {isEdit?<>
                    <textarea ref={localContentInput}value={localContent} onChange={(e)=>setLocalContent(e.target.value)}/>
                </>:<>{content}</>}
            </div>
            <div>
                {isEdit?<><button onClick={handleQuitEdit}>수정 취소</button>
                        <button onClick={handleEdit}>수정 완료</button></>:
                        <><button onClick={handelDelete}>삭제하기</button>
                        <button onClick={toggleIsEdit}>수정하기</button></>}

            </div>


        </div>
    )
}

export default DiaryItem;

App.css

.DiaryEditor{
    border: 1px solid gray;
    text-align: center;
    padding: 20px;
}

.DiaryEditor input,textarea{
     margin-bottom: 20px;
     width: 300px;
     padding: 5px;
}

.DiaryEditor textarea{
    padding: 20px;
}

.DiaryEditor select{
    width: 200px;
    padding: 5px;
    margin-bottom: 10px;
}

.DiaryEditor button{
    width: 200px;
    padding: 10px;
    cursor: pointer;
}

/* LIST */
.DiaryList{
    border:  1px solid gray;
    padding: 20px;
    margin-top: 20px;
}

.DiaryList h2 {
    text-align: center;
}
/* ITEM */
.DiaryItem{
    background-color: rgb(240, 240, 240);
    margin-bottom: 10px;
    padding: 20px;
}

.DiaryItem .info{
    border-bottom: 1px solid gray;
    padding-bottom:10px ;
    margin-bottom: 10px;
}

.DiaryItem .date{
    color: gray;
}

.DiaryItem .content {
 font-weight: bold;
 margin-bottom: 30px;
 margin-top: 30px;
}

이제 어느 정도 원하는 바를 다 갖춘 일기장을 완성하였다!

하지만 여기서 코드 최적화를 조금 더 진행해 볼 것이다.

 


useMemo?

연산 결괏값들을 재사용한다

Memoization?

말 그대로 이미 계산해본 연산 결과를 기억해 두었다가 동일한 계산을 시키면 다시 연산하지 않고 
기억해두웠던 데이터를 반환시키게 한다

이 개념들을 이용해 최적화를 한번 해볼 것이다.

 


우리는 일기에 감정점수를 가지고 있다 이 점수를 이용해 

1,2는 기분 나쁨 3,4,5는 기분 좋음으로 판단하여 

기분 좋은 일기의 개수, 기분 나쁜 일기의 개수, 기분 좋은 일기의 비율을 화면에 띄어 볼 것이다

 

  const getDiaryAnalysis = ()=>{
    console.log("일기 분석 시작");

    const goodCount = data.filter((it)=>it.emotion>=3).length;
    const badCount = data.length-goodCount;
    const goodRatio = (goodCount/data.length)*100;
    return {goodCount,badCount,goodRatio}; //객체로 리턴
  );
  
  const {goodCount,badCount,goodRatio}=getDiaryAnalysis(); //객체비구조화할당

getDiaryAnalysis라는 일기 분석 함수를 하나 만들어주고 그 값들을 객체로 리턴을 해주자

지역 변수들이기 때문에 객체 비구조화 할당으로 값을 받아주자

 

  return (
    <div className="App">
    <DiaryEditor onCreate={onCreate}/>
    <div>전체일기 : {data.length}</div>
    <div>기분 좋은 일기 개수: {goodCount}</div>
    <div>기분 나쁜 일기 개수: {badCount}</div>
    <div>기분 좋은 일기 개수 비율: {goodRatio}</div>
    <DiaryList diaryList={data} onDelete={onDelete} onEdit={onEdit}/>
    </div>
  );

DiaryEditor 컴포넌트와 DiaryList사이에 넣어주자

잘 뜬다 근데 여기서 문제는 아니지만 불편한 점이 있다

 

일기를 수정하면 전체일기가 늘어나지 않으므로 좋은 일기, 나쁜 일기 분석한 값이 바뀌지 않지만

렌더링이 되는 걸 볼 수 있다.. 

그럼 일기의 길이가 변경되어 즉 추가나 삭제가 되어 비율이 바뀔 때만 getDiaryAnalysis이 렌더링 되게 해 보자

 

이를 도와주는 게 useMemo이다

  const getDiaryAnalysis = useMemo(
    ()=>{
    console.log("일기 분석 시작");

    const goodCount = data.filter((it)=>it.emotion>=3).length;
    const badCount = data.length-goodCount;
    const goodRatio = (goodCount/data.length)*100;
    return {goodCount,badCount,goodRatio}; //객체로 리턴
  },[data.length]
  );

리턴을 하는 함수가 있을 때 리턴까지의 값을 최적화하고 싶다면 

해당 함수를 useMemo로 감싸주자 그럼 이 함수의 기능이 useMemo의 콜백함수처럼 들어가게 된다

그리고 두 번째 인자로는 useEffect처럼 defendency array를 넣어준다 행동도 똑같다 이 배열의 값이

바뀌며 callback함수가 실행되는 것이다

 

그럼 우리는 일기의 개수가 변화할 때만 렌더를 바라므로 저 배열에 data.length를 넣어주며 되는 것이다

그럼 이렇게 실행해 보자

 

Uncaught TypeError: getDiaryAnalysis is not a function

어라 getDiaryAnalysis가 더 이상 함수가 아니라는 에러메세지가 뜬다 그렇다 useMemo를 사용하면 그 함수는 더이상 함수가 아니다

왜냐하면 useMemo 기능은  함수를 전달받아서 그 callback함수가 리턴하는 값을 리턴하기 때문에 

getDiaryAnalysis는 더 이상 함수가 아닌 값인 것이다

  const {goodCount,badCount,goodRatio}=getDiaryAnalysis;

그러니 함수가 아닌 값으로 사용해야 한다

 

그럼 내가 원했던 것처럼 수정 시에는 렌더가 일어나지 않지만 일기개수가 변화한다면 리렌더가 일어나는 걸 볼 수 있다!

useMemo를 사용해 함수의 연산 최적화를 해보았다!

 


..💬

useEffect와 굉장히 비슷하다 생각해서 차이점을 찾아보았다

useMemo의 경우 "생성"함수에 관련된 기능으로 생성자 함수가 고비용(처리 시간이 오래 걸리는 등)인 경우 렌더링마
다 계산하는 것은 처리 시간이 오래 걸리므로 값을 기억해놓고 의존성이 변경되었을 경우에만 다시 계산해주는 기능이다

useEffect의 경우는 api 호출, 타이머 등 렌더링 과정에서 한 번만 호출해도 될 기능들이 렌더링 되어 실행되거나, 호출과정에서 렌더링에 영향을 끼칠 수 있는 것을 모아서 따로 처리하기 위한 기능이다
 
둘의 가장 큰 차이점은 useEffect는 해당 컴포넌트의 렌더링이 완료된 후에 실행되지만, useMemo는 렌더링 중에 실행되어진다

오.. 이런 기능 들을 프로젝트의 필요한 곳에 알맞게 사용해야 한다는 생각이 들었다.. 어렵댜..

 

또 useMemo를 사용하면 그 함수는 더 이상 함수가 아닌 값! 이라는 점!

반응형