[정보통신산업진흥원] AI 웹개발 취업캠프 - 유데미 필수 강의 후기/기록 (한입 크기로 잘라 먹는 리액트(React.js)-(2))

Alicia·2023년 8월 20일
0

AI_Web_nipa

목록 보기
21/31
post-thumbnail

React에서 배열 사용하기 [조회 / 추가 / 삭제 / 수정]

이번주는 다이어리 기능의 핵심은 내용을 조회하고, 추가하고, 삭제하고 수정하는 로직을 어떻게 구현하는 지 알아보는 시간이었다.


조회

dummyList를 DiaryList 컴포넌트에 props로 전달하여 조회 목록에 표시할 데이터를 넘긴다.
하지만 아무런 배열도 받지 않으면 에러가 나는데 ....

이 에러를 미리 막아줄 메서드가 존재한다 그것이 바로 defaultProps

DiaryList.defaultProps = {
  	diaryList: []
};

undefined로 전달될거 같은 props들을 기본값으로 설정할 수 있는, DiaryList 컴포넌트의
diaryList 프로퍼티가 전달되지 않았을 때 기본적으로 빈 배열([])로 설정되도록 하기 위해 사용된다.
이렇게 기본값을 설정함으로써, 부모 컴포넌트에서 diaryList를 전달하지 않아도
기본적으로 빈 배열이 사용되게 된다.

추가

state끌어올리기, 단방향 데이터흐름, 역방향 이벤트흐름 : 자식 컴포넌트끼리 데이터를주고 받을 수 없기때문에 부모에게 state를 끌어올린후 설정한 state를 editor에겐 setDate 상태변화로 event호출, 조회list 컴포넌트에겐 Data를 전달해주는 형식

DiaryEditor에게 props로 onCreate함수를 넘겨주면 추가할때 onCreate 함수로 author, content,emotion데이터와 함께 newItem객체를 하나 만들어 배열에 추가하는 방식

삭제


App컴포넌트에서 dataState를 업데이트시켜야 한다.

따라서 전달하여 지울 수 있도록 onDelete함수를 만들어 DiaryList에게 전달하고 또 DiaryList는 DiaryItem에게 전달해서 onDelete(id) 지정; -> 이 함수명은 후에 onRemove로 변경됨

수정






함수형 컴포넌트에게 업데이트 조건을 걸자 react.memo

실습예제

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

const CounterA = React.memo(({ count }) => {
  useEffect(() => {
    console.log(`CountA Update - count : ${count}`);
  });
  return <div>{count}</div>;
});

const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`CountB Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  if (prevProps.obj.count === nextProps.obj.count) {
    return true;
  }
  return false;
};

const MemoizedCounterB = React.memo(CounterB, areEqual);

const OptimizeTest = () => {
  const [count, setCount] = useState(1);
  const [obj, setObj] = useState({
    count: 1
  });

  return (
    <div style={{ padding: 50 }}>
      <div>
        <h2>Counter A</h2>
        <CounterA count={count} />
        <button onClick={() => setCount(count)}>A Button</button>
      </div>
      <div>
        <h2>Counter B</h2>
        <MemoizedCounterB obj={obj} />
        <button onClick={() => setObj({ count: 1 })}>B Button</button>
      </div>
    </div>
  );
};

export default OptimizeTest;

CounterA는 버튼 클릭 시 불필요한 리렌더링이 발생하지만, CounterB는 React.memo와 areEqual 함수로 인해 obj.count가 변경되지 않으면 리렌더링이 일어나지 않음

성능 최적화 중요성: 
React.memo를 사용하여 컴포넌트의 성능을 향상시킬 수 있다는 것을 배웠다.

Memoization 개념: 
React.memo는 내부적으로 Memoization 기법을 사용하여 컴포넌트의 이전 결과를 저장하고, 프로퍼티가 변경되지 않으면 저장된 결과를 사용하여 리렌더링을 방지한다. 이 개념을 이해하고 활용하여 컴포넌트 업데이트를 효율적으로 관리할 수 있다.

areEqual 함수 활용: 
React.memo는 두 프로퍼티 값이 동일한 경우에만 리렌더링을 방지한다. 이때 areEqual 함수를 제공하여 언제 리렌더링을 하거나 방지할지를 정의할 수 있다.

최적화 전략 선택: 
모든 컴포넌트에 React.memo를 사용하는 것은 항상 최적화에 도움이 되지는 않는다. 최적화가 필요한 컴포넌트와 상황을 판단하여 적절하게 활용하는 능력을 배웠다.

컴포넌트 트리에 데이터 공급하기; Context API

Context API는 React 애플리케이션에서 전역적인 상태를 관리하고 컴포넌트 간에 데이터를 공유하기 위한 도구이다. 전역 상태를 중앙에서 관리하여 프롭스 전달의 번거로움을 줄이고, 컴포넌트 간에 데이터를 더 쉽게 전달할 수 있어 props drilling을 해결할 수 있는 장점이 있다.

Provider 컴포넌트

Context API에서 핵심 역할을 하는 것이 Provider 컴포넌트인데 이 Provider는 상위 컴포넌트에서 생성되며, 하위 컴포넌트에서 해당 컨텍스트에 접근할 수 있도록 한다.

최적화 완성!

import React, {
  useCallback,
  useEffect,
  useMemo,
  useReducer,
  useRef,
  createContext
} from "react";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
import "./App.css";

export const DiaryStateContext = createContext(null);
export const DiaryDispatchContext = createContext(null);

const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...action.data,
        created_date
      };
      return [newItem, ...state];
    }
    case "REMOVE": {
      return state.filter((it) => it.id !== action.targetId);
    }
    case "EDIT": {
      return state.map((it) =>
        it.id === action.targetId
          ? {
              ...it,
              content: action.newContent
            }
          : it
      );
    }
    default:
      return state;
  }
};

const App = () => {
  const [data, dispatch] = useReducer(reducer, []);
  const dataId = useRef(0);
  const getData = async () => {
    setTimeout(async () => {
      const res = await fetch(
        "https://jsonplaceholder.typicode.com/comments"
      ).then((res) => res.json());

      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++
        };
      });

      dispatch({ type: "INIT", data: initData });
    }, 2000);
  };

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

  const onCreate = useCallback((author, content, emotion) => {
    dispatch({
      type: "CREATE",
      data: { author, content, emotion, id: dataId.current }
    });
    dataId.current += 1;
  }, []);

  const onRemove = useCallback((targetId) => {
    dispatch({ type: "REMOVE", targetId });
  }, []);

  const onEdit = useCallback((targetId, newContent) => {
    dispatch({
      type: "EDIT",
      targetId,
      newContent
    });
  }, []);

  const memoizedDispatch = useMemo(() => {
    return { onCreate, onRemove, onEdit };
  }, []);

  const getDiaryAnalysis = useMemo(() => {
    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100.0;
    return { goodCount, badCount, goodRatio };
  }, [data.length]);

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

  return (
    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider value={memoizedDispatch}>
        <div className="App">
          <DiaryEditor />
          <div>전체 일기 : {data.length}</div>
          <div>기분 좋은 일기 개수 : {goodCount}</div>
          <div>기분 나쁜 일기 개수 : {badCount}</div>
          <div>기분 좋은 일기 비율 : {goodRatio}%</div>
          <DiaryList />
        </div>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );
};

export default App;

List.js

const diaryList = useContext(DiaryStateContext);

  return (
    <div className="DiaryList_container">
      <h2>일기 리스트</h2>
      <h4>{diaryList.length}개의 일기가 있습니다.</h4>
      <div>
        {diaryList.map((it, idx) => (
          <DiaryItem key={`diaryitem_${it.id}`} {...it} />
        ))}
      </div>
    </div>

Item.js

import React, { memo, useContext, useEffect, useRef, useState } from "react";
import { DiaryDispatchContext } from "./App";

const DiaryItem = ({ id, author, content, emotion, created_date }) => {
  const { onRemove, onEdit } = useContext(DiaryDispatchContext);

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

  const [localContent, setLoclContent] = useState(content);
  const localContentInput = useRef(null);

  const handleRemove = () => {
    if (window.confirm(`${id}번 째 일기를 삭제하시겠습니까?`)) {
      onRemove(id);
    }
  };

  const handleQuitEdit = () => {
    setIsEdit(false);
    setLoclContent(content);
  };

  const handleEdit = () => {
    if (localContent.length < 5) {
      localContentInput.current.focus();
      return;
    }
    if (window.confirm(`${id}번 째 일기를 수정하시겠습니까?`)) {
      onEdit(id, localContent);
      toggleIsEdit();
    }
  };

  return (
    <div className="DiaryItem_container">
      <div className="info">
        <span className="author_info">
          | 작성자 : {author} | 감정점수 : {emotion} |
        </span>
        <br />
        <span className="date">{new Date(created_date).toLocaleString()}</span>
      </div>

      <div className="content">
        {isEdit ? (
          <textarea
            ref={localContentInput}
            value={localContent}
            onChange={(e) => setLoclContent(e.target.value)}
          />
        ) : (
          content
        )}
      </div>
      {isEdit ? (
        <div>
          <button onClick={handleQuitEdit}>수정 취소하기</button>
          <button onClick={handleEdit}>저장하기</button>
        </div>
      ) : (
        <div>
          <button onClick={handleRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </div>
      )}
    </div>
  );
};

export default memo(DiaryItem);

React의 useSearchParams 훅을 활용한 URL 쿼리 파싱과 관리

useSearchParams란?

useSearchParams는 React Router의 hooks 중 하나로, URL의 쿼리 파라미터를 다루기 위한 훅입니다. 이를 사용하면 URL 쿼리를 손쉽게 파싱하고 업데이트할 수 있습니다.

import { useSearchParams } from 'react-router-dom';



const MyComponent = () => {
  const [searchParams, setSearchParams] = useSearchParams();


//쿼리 파라미터 업데이트하기
  const updateQuery = (newValue) => {
    setSearchParams({ query: newValue });
  };
  
  return (
    <div>
      <input
        type="text"
        value={searchParams.get('query')}
        onChange={(e) => updateQuery(e.target.value)}
      />
    </div>
  );
};

본 후기는 정보통신산업진흥원(NIPA)에서 주관하는 <AI 서비스 완성! AI+웹개발 취업캠프 - 프론트엔드&백엔드> 과정 학습 기록으로 작성 되었습니다.

0개의 댓글