한 입 크기로 잘라먹는 리액트 6강

이안이다·2023년 5월 29일
0

React

목록 보기
5/7
post-thumbnail

자 드디어 배운 내용들을 토대로 감성 일기장을 만드는 실습을 하면서 본격적인 React 공부를 시작한다.


사전 준비

먼저 'simplediary'라는 폴더를 만들어놓고 VScode에 띄운 후에 다음과 같이 준비를 해준다.

프로젝트 생성을 끝냈으면 simplediary폴더에 들어가서 그 안에 또 simplediary폴더가 생기고 그 안에 각종 js,md,json파일들이 있을텐데 걔네를 잘라내서 simplediary폴더가 이중으로 들어가지 않게끔 설정한다. 그리고 필요 없는 파일들을 삭제한다 (App.test.js, logo.svg, setupTest.js, reportWebVitals.js등등).

자 터미널에 npm start 쳐서 React웹 열어주고 실습을 시작하자.


감성일기장 1. 사용자 입력 처리하기


이런 일기장을 만들거다.

import { useState } from "react";

리액트의 useState기능을 이용할거니까 미리 추가해준다.

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

이렇게 함수를 구현할거다.

강의를 따라서 코드를 다 만들면

import { useState } from "react";

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

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = () => {
    console.log(state);
    alert("저장 성공!");
  };

  return (
    <div className="DiaryEditor">
      <h2>오늘의 일기</h2>
      <div>
        <input
          value={state.author}
          onChange={handleChangeState}
          name="author"
          placeholder="작성자"
          type="text"
        />
      </div>
      <div>
        <textarea
          value={state.content}
          onChange={handleChangeState}
          name="content"
          placeholder="일기"
          type="text"
        />
      </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;

이런 코드를 만들 수 있다. 이번 강의에서 기억에 남는 내용들이 좀 있는데..
일단 그냥 input창을 만들었을 때 내가 거기에 입력하는게 그 input창에 그대로 뜨게 하는 게 이렇게 어려운 건 줄 몰랐다. const[state, setState] 써서 상태변화 함수 만들어서 실시간으로 적용되게 해줘야한다는 거 좀 인상적이었다. 내가 각각 다른 input창에 입력하는 값에 따라서 각각 다른 콜백함수가 실행된다는 점 ! 그리고 스프레드 연산자를 활용하면 조금 더 간단히 할 수 있다는 거 !

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

이렇게 ...state처럼 스프레드 연산자를 쓰면 예전 강의에서 배웠던 내용처럼, 저 tate라는 객체가 가지고 있는 프러퍼티들을 저 부분에 펼쳐준다. ...state가 author값, content값을 쫙쫙 펼쳐주는 것. 사실 스프레드 연산자 기억 안나서 구글링 하고 옴.

마지막으로 스타일링을 해주면 !!!

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

.DiaryEditor input,
textarea {
  margin-bottom: 20px;
  width: 500px;
}

.DiaryEditor input {
  padding: 10px;
}
.DiaryEditor textarea {
  padding: 10px;
  height: 150px;
}

.DiaryEditor select {
  width: 300px;
  padding: 10px;
  margin-bottom: 20px;
}

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


완성 ~ 30분짜리 강의였는데 왜 두시간이나 걸릴까ㅎㅎ


감성 일기장 2. React에서 DOM 조작하기 - useRef

이번에는 사용자가 일기의 내용을 너무 적게 입력했거나 이상한 값이 들어왔을 때 처리하는 방법에 대해 공부할거다.

  if(state.content.length < 5) //일기 내용이 5글자 이하면
  {
    alert("일기 본문 내용이 너무 적다 임마 !!!");
  }

사실 그냥 이렇게 처리해도 된다. 그치만 요즘 trendy한 웹페이지들은 이런 alert를 최대한 사용하지 않기 때문에 간지에 살고 간지에 죽는 우리도 alert를 쓰지 않고, 문제가 발생한 구역에 포커싱을 하는 방법으로 처리하자.

    if (state.content.length < 5) {
      contentInput.current.focus();
      return;
    }

이렇게 가능 !!!


감성 일기장 3. React에서 배열 사용하기 1 - 리스트 렌더링

이번엔 작성한 일기들을 리액트의 배열에다가 저장하는 방법에 대해 공부하자.

렌더링한다 == 화면에 표시한다

const dummyList = [
  {
    id: 1,
    author: "김이안",
    content: "아브라카다브라",
    emotion: 5,
    created_date: new Date().getTime(),
  }
]

이해를 위해 이렇게 배열을 만들었다. 딱봐도 허접하지만 배열 사용의 연습을 위해 이렇게만ㅎㅎ.. 일단 new Date()를 쓰면 현재 시간을 가져온다. 그리고 getTime()은 시간을 받아서 밀리세컨드로 반환해준다.

자 그리고 DiaryList.js라는 파일을 새로 만들어서 저장된 일기들을 관리할건데

const DiaryList = ({ diaryList }) => {
  return (
    <div className="DiaryList">
      <h2>일기 리스트</h2>
      <h4>{diaryList.length}개의 일기가 있습니다.</h4>
      <div>
        {diaryList.map((it) => ( //여기 (it) 주목 !!!!
          <DiaryItem key={`diaryitem_${it.id}`} {...it} />
        ))}
      </div>
    </div>
  );
};

이 코드를 보면 diaryList.map((it)) => 쓰는게 좀 인상적이다. 저 it에는 뭐가 들어있냐면

아까 app.js에 작성한 저 리스트 속 id와 author, content 등등의 정보들이 it이라는 객체에 담겨 있는거고, 위에서 diaryitem_${it.id} 코드를 보면 그 it에 담긴 정보 중에서 id에 접근하겠다는 걸 알 수 있다. 이런 부분들 하나씩 공부하면서 느끼는건데 진짜 리액트는 참 자유분방한 것 같다. 공부하기는 어렵지만 잘만 익히면 진짜 능수능란하게 활용할 수 있을 것 같다..

근데 만약에 it에 들어가있는 객체들이 id처럼 값을 가지고 있지 않아서 접근하기 어려운 경우도 있을 수 있다. 그럴 때는 파이썬 배열에서 index에 접근하는 거랑 비슷하게 idx를 사용해서 접근할 수도 있다

diaryList.map((it,idx) => (
  ...
))

이런식으로!

근데 배열에 인덱스를 사용하다보면 나중에 객체들을 새로 추가하거나 삭제하거나 그럴 때 순서가 뒤바뀌는 참사가 발생할 수도 있어서 엔간하면 인덱스로 접근하지말고 고유한 id로 키를 지정해서 쓰는게 현명하다 !

 <span className="date">{new Date(created_date).toLocaleString()}</span>

이렇게 만들면 우리가 알아보기에 이쁜 형식으로 날짜와 시간을 렌더링할 수 있다. (2023. 5. 29. 오후 3:24:57)

css를 살짝 꾸며주면 아래와 같은 일기장으로 진화한다 !



감성 일기장 4. 리스트에 데이터 추가하기


자 이렇게 컴포넌트 트리를 그려가면서 코딩하는 게 좋음.


1. 리액트로 만든 컴포턴트들은 이렇게 트리 구조를 가진다.
2. 데이터는 위에서 아래로만 흐르는 단방향 데이터가 된다.
3. 추가,수정,삭제 같은 이벤트들은 아래에서 위로 올라간다.
-> props로 전달

그러면 App.js 파일에 onCreate라고 하는 함수를 만들어준다. 일기 데이터를 추가할 수 있는 함수이다.

  const dataId = useRef(0);
  const onCreate = (author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current, //id는 useRef()를 사용해서 만듦. 처음은 0
    };
    dataId.current += 1; //그리고 1씩 늘려서 다음 번호 매기기
    setData([newItem, ...data]);

어려운 건 없는데 주의해야하는건 작성자명이나 내용, 감정점수 등은 몰라도 id는 숫자고 우리가 사용자로부터 입력 받아오는 정보가 아니다. 따라서 위 코드 최상단처럼 useRef()를 활용해서 초기값을 받아오고 1씩 더하게끔해서 몇번째 일기인지 나타내는 기능을 구현한다. 그리고 맨 마지막 코드는 가장 마지막에 추가된 일기장이 일기장 리스트의 가장 위에 기록될 수 있게 해준다.

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

그리고 만든 onCreate를 이렇게 prop으로 넘겨준다.

입력하면
저장성공 뜨고
매우만족

이렇게 배열에 데이터를 추가할 수 있다.
recap해보자 !!!


이 App 컴포넌트가 DiaryEditor 컴포넌트와 DiaryList 컴포넌트가 함께 사용할 일기 데이터를 하나 가지고 있다. useState()로. 근데 이건 빈 배열에서 출발한다. 일기가 없는 상태에서부터 시작하는거니깐. 그리고 일기 상태변화 함수인 setData까지 들어있다.

자 그러면 여기서 DiaryList는 현재 App컴포넌트가 가진 일기의 state를 넘겨받기만 하면 된다. 그러면 App컴포넌트가 가진 data가 바뀌면 알아서 DiaryList의 data도 바뀌게 되고 (추가,삭제 등) 그걸 렌더링하면 되는거다.


감성 일기장 5. 리스트 데이터 삭제하기

추가하는 거 잘 만들었으니까 이번에는 삭제하는 기능을 구현한다.
HTML에 가서 버튼을 만드는 게 아니라 DiaryItem.js에서 html에 접근할 수 있게 지금까지 만들어온 거 처럼 만들면 된다. 자꾸 웹 상에 버튼이나 input을 만들려고 하면 나도 모르게 html을 찾아 키려고 한다 ,,,

DiaryList.js에다가

  <button
    onClick={() => {
      if (window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)) {
        onDelete(id);
      }
    }}
  >
    삭제하기
  </button>

이렇게 해놓고

다음으로 App.js에도 삭제 기능 넣어주고 !

  const onDelete = (targetId) => {
    const newDiaryList = data.filter((it) => it.id !== targetId);
    setData(newDiaryList);
  };


여기에 onDelete도 prop으로 받아주고 !!!

삭제하려했더니

날 미치게 한다..

TypeError: onDelete is not a function
음...

하하하하하하하하 한참 머리 뜯고 있었는데 DiaryItem.js 파일에 제대로 수정하지 않은 부분이 있었다. 그러니까 js는 이게 onDelete가 함수가 맞냐고 에러띄운 거였고 ㅋㅋ아 열받네 진짜..... 이걸 왜 ....

      <div>
        {diaryList.map((it) => (
          <DiaryItem key={it.id} {...it} onDelete={onDelete} />
        ))}
      </div>


이제 정상적으로 삭제되는 걸 확인할 수 있다 !


감성 일기장 만들기 6. 리스트 데이터 수정하기

강사의 번복으로 인해.. onDelete함수를 모두 onRemove로 수정해주고 시작한다.

수정해주는 기능은 onEdit()으로 구현하고 각각의 파일에 코드를 넣어주자.

App.js에는

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

아래에서 다시 설명하겠지만 이 코드 매우 요

DairyList.js

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

DiaryItem.js

      {isEdit ? (
        <>
          <button onClick={handleQuitEdit}>수정 취소</button>
          <button onClick={handleEdit}>수정 완료</button>
        </>
      ) : (
        <>
          <button onClick={handleClickRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </>
      )}

이렇게 각각 코드를 넣어주고!!!


이렇게 수정하기 버튼이 잘 구현됐고 잘 되는지 확인해보자.


잘 수정 된다 !!!

수정하기를 누르면 내가 이전에 작성했던 원문 내용을 그대로 띄우고 거기에서부터 수정할 수 있어야하는데 수정하기를 누르면 빈 입력창이 나오고 거기에 입력한 내용이 그냥 원문을 덮어씌워 버리는 방식이 되면 안되니까

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

useState("");에서 공백 대신에 그냥 content를 넣어줌으로써 원문 내용을 가져올 수 있도록 했다.

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

그리고 수정하다가 수정 취소를 누르고, 다시 수정하기에 들어갔을 때 이전에 수정하던 내용이 그대로 뜨는 문제가 발생한다. 내가 원하는 거는 수정 취소하면 수정하던 내용은 날려버리고 다시 수정하기에 들어오면 다시 그냥 원본 contents만 보여주고 싶기 때문에 이렇게 handleQuitEdit함수를 만들어서 DiaryItem.js에 넣어줬다.


감성 일기장 7. React Lifecycle 제어하기 - useEffect


생애주기 !


리액트의 생애주기 ! 이렇게 보니까 어려우니까 쉽게 다시 설명해준대.


리액트의 생애주기는 이렇게 마운트, 업데이트, 언마운트로 나뉜다.

그럼 우리가 각각 어떤 작업들을 할 수 있을지 예를 들어보자.

뭐 어렵지 않다. 오케이 ~

다음은 React Hooks라는 개념이 있다.

원래 State같은 기능들은 함수형 컴포넌트들은 쓸 수 없다. class형 컴포넌트들만 쓸 수 있었다. 근데 React Hooks라는게 개발되면서 use를 붙여서 useState, useEffect, uesRef이렇게 갖다 붙여서 함수처럼 사용할 수 있게 됐다.

두 개의 파라미터 중에서 저 뎁스 배열에 자꾸 변화하는 어떤 값을 넣어놓으면, 저 콜백 함수가 그 값이 변화할 때마다 수행이 된다는 뜻이다.

LifeCycle.js라는 파일을 실험용으로 만들어서 실습을 진행해보자.

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

const UnMountTest = () => {
  useEffect(() => {
    console.log("Sub Component Mount");
    return () => {
      console.log("Sub Component Unmount");
    };
  }, []);
  return <div>UN MOUNT TEST</div>;
};

const LifeCycle = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const [isVisible, setIsVisible] = useState(false);
  const toggle = () => setIsVisible(!isVisible);

  useEffect(() => {
    console.log("Mount!");
  }, []);

  useEffect(() => {
    console.log("Update!");
  });

  useEffect(() => {
    console.log(`count is update : ${count}`);
  }, [count]);

  useEffect(() => {
    console.log(`text is update : ${text}`);
  }, [text]);

  return (
    <div>
      <div>
        {count}
        <button onClick={() => setCount(count + 1)}>count up</button>
      </div>
      <div>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
      </div>
      <button onClick={toggle}>ON/OFF BUTTON</button>
      {isVisible && <UnMountTest />}
    </div>
  );
};

export default LifeCycle;

감성 일기장 8. React에서 API불러오기

fetch로 api불러오는 거는 어제 니콜라스 바닐라JS 챌린지 졸업 작품 만들어서 제출할 때 다시 공부했던거라 편하게 공부할 수 있겠다 !!

강의 초반부에 공부했던 async, await 사실 다 까먹고 있었다.
아래 코드에서 주석 표시한 부분들을 추가해보자.

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

const App = () => {
  const [data, setData] = useState([]);
  const dataId = useRef(0);

  const getData = async () => { //요기 !!! api url가져오는 함수
    const res = await fetch(
      "https://jsonplaceholder.typicode.com/comments"
    ).then((res) => res.json());

    const initData = res.slice(0, 20).map((it) => { //요기 !!! 문자열 slice해서 필요한 만큼 가져오고 이 작업들을 해주면 코드 아래 사진과 같이 작동 가능
      return {
        author: it.email,
        content: it.body,
        emotion: Math.floor(Math.random() * 5) + 1,
        created_date: new Date().getTime() + 1,
        id: dataId.current++,
      };
    });

    setData(initData);
  };

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

  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 onRemove = (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 onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
};
export default App;

이렇게 App.js에 API를 불러와서 사용할 수 있게끔 코드를 수정하면

이렇게 된다. 솔직히 이게 무슨 의미가 있나...싶기는 하지만 생각보다 짧게 끝나버려서 다음 강의에 알려주지 않을까 싶다..


감성 일기장 9. React developer tool

이번 강의에선 api 추가설명을 기대했지만 일단 코딩 실습 없이 개꿀팁 하나를 소개해주겠다고 한다.

구글에 검색하고

추가해주자.


잘 됐다.

이제 설정>도구>확장프로그램에서

이렇게 활성화 시켜주고, 마지막으로 권한 !!

권한까지 이렇게 설정해주면 끝이다.

자 그래서 얘는 뭐하는 녀석이냐면,
그동안 뭐 이벤트에 대해서 props로 보내주고 뭐하고 되게 복잡했는데 그런 것들을 개발자에게 가시적으로 제공해주는 기능을 일단 제공한다. 어떤 파일의 어떤 함수에서 어떤 prop을 받아오고 있는지 등에 대한 정보를 보여준다.

이런식으로.

우리가 리스트로 렌더링한 것들의 키값들도 다 알려주고 어떤 state인지 등등도 다 알려주는 기능이 있다.

그리고 또 하나가 있는데 내가 작성하고 있는 부분 즉, 리액트 입장에서 리렌더링 하고있는 부분들에 노란색 테두리로 포커싱을 해준다.

앞으로 요긴하게 써먹자 !
(설치 후에 크롬을 다 종료했다가 다시 vscode에서 npm start로 다시 열어줘야 한다는데 귀찮아서 난 안했다 ㅎㅎ)


감성 일기장 10. 최적화 1 - 연산 결과 재탕

벌써 이번 단원 10번째 강의다. 오전에 일어나서 시작했는데 벌써 저녁 식사 시간이 다 돼간다.

이번엔 연산 결과를 재탕하는 방법에 대해 공부한다.

이전에 풀어놨던 문제의 정답을 기억해놨다가 나중에 비슷한 문제가 주어졌을 때 기억했던 답을 재탕하는 것. 이것이 Memoization 메모이제이션 이름 귀엽다.

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

이렇게 작성자가 선택한 감정 점수에 따라 각각의 일기 개수를 카운트 해주는 뼈대를 만들어보자.

  const getDiaryAnalysis = useMemo(() => {
    if (data.length === 0) {
      return { goodcount: 0, badCount: 0, goodRatio: 0 };
    }
    console.log("일기 분석 시작");

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

기능은 이렇게 구현을 해준다.

요러케.

근데 하나 생각해봐야 할 부분이 생겼다. 저장된 일기를 수정했을 때도 App컴포넌트가 다시 한 번 리렌더링 되게끔 코딩을 했다. 근데 이건 무의미하다. 수정하기를 눌러서 내가 수정할 수 있는 건 원문내용이지 감정 점수를 수정할 수는 없기 때문 ! 이거 완전 어플 '썸원'이랑 비슷하다. 거기서도 커플 문답에서 감정은 한 번 고르면 못 바꾼다 하고 원문 내용은 수정할 수 있는데 개발자가 Memoization에 대한 공부를 안했나보다 ^^

우리는 메모이제이션 기법을 이용해서 최적화해보자. useMemo()라는 함수를 쓸건데 이 함수로 우리가 최적화하고 싶은 부분을 감싸주기만 하면 된다.

  const getDiaryAnalysis = useMemo(() => {
    if (data.length === 0) {
      return { goodcount: 0, badCount: 0, goodRatio: 0 };
    }
    console.log("일기 분석 시작");

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

like this ! 자 보면 코드 맨 뒤에 배열 하나가 붙는다. 이 배열에 data.length를 넣어놓으면 얘가 변화할 때만 useMemo 함수의 첫번째 인자로 전달한 이 callback함수가 다시 수행되게 된다. 그러니까 ! 다른 곳에서 아무리 getDiaryAnalysis()를 호출한다고 하더라도 [data.length] 얘가 변하지 않는 이상은 그냥 똑같은 리턴을 계산하지 않고 반환한다는 뜻이다. 즉, data.length가 변하지 않으면 다시는 위 과정을 다시 계산하지 마라는 뜻 !

자 근데 useMemo()를 처음 쓰는 사람들이 항상 실수해서 TypeError를 겪는데

바로 요거.. 왜 발생했냐면 useMemo()로 어떤 함수를 감싸고 디펜덴시 배열을 전달해서 최적화를 하면 걔는 더 이상 함수가 아니다. 따라서 위에서 const getDiaryAnalysis 얘는 useMemo()로부터 값을 그냥 리턴 받게 되는 것이다. 그러니까 함수가 아니라 그냥 하나의 값으로만 사용해야 한다. 이 점을 주의해야한다 !!!


감성 일기장 11. 최적화 2 - 컴포넌트 재탕

이번에는 연산결과에 이어서 컴포넌트를 재탕하는 방법에 대해서 다룬다.
자 이쯤에서 당연하고 기본적인거지만 자꾸 등장하는 컴포넌트에 대한 recap.

리액트 컴포넌트(component)는 리액트로 만들어진 앱을 이루는 최소한의 단위로서 가장 작은 조각이라고 이해하면 된다. 컴포넌트는 property(props), state, context 이렇게 세 가지의 구성 요소로 이루어진다. 그리고 두 가지의 종류가 있는데 이게 중요하다. 하나는 함수 컴포넌트이다. 말 그대로 JS의 function기반 컨포넌트이다. JS에서 함수를 선언하듯이 function으로 컴포넌트를 정의하고, return문에 JSX코드를 반환한다. 다른 하나는 클래스 컴포넌트이다. 클래스 컴포넌트는 JS의 class기반 컴포넌트로, class로 정으하고 render() 함수에서 jsx코드를 반환한다.
(출처 및 참고 : https://life-with-coding.tistory.com/508)

자 다시 강의로 돌아와서 최적화가 어쩌구 렌더링이 저쩌구는 두 번 들어도 잘 모르겠고..

React.memo에 대해서나 빠삭하게 이해해보자.

React공식 홈페이지에서 React.memo에 대해서 설명해주는 내용이다. 사실 리엑트에 대해 공부할 때 이 공식 홈페이지를 참고하는게 가장 좋다. 여기서 맞다고 하면 맞는 것이고, 틀리다고 하면 틀린 것이다. 자 근데 위 사진에서 시작부터 모르겠는 단어가 나온다. 고차 컴포넌트 ...


고차 컴포넌트에 대해 react 공식 홈페이지에서 제공해주는 정보이다. 정리해보자면 고차 컴포넌트 HOC는 컴포넌트를 가져와 새 커모넌트를 반환하는 함수이다! 로 정의할 수 있다.

마치 가죽을 주면 구두로 만들어주겠다 ! 이런 느낌이다.
고차 컴포넌트가 뭐하는 놈인지는 대충 이해했으니 다시 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;


react.memo를 사용해서 똑같은 프러퍼티에 대해서 리렌더링을 하지 않는 것을 알 수 있다.

그럼 문제 하나 !!

이 두개를 비교했을 때, a랑 b는 같다고 나올까 다르다고 나올까?


결과는 다르다고 나온다.
이유는 JS가 객체나 함수 등을 비교할 때 값에 의한 비교가 아닌 주소에 의한 비교 이른바, 얕은비교를 하기 때문이다. 즉 비교 대상이 서로 같은 값을 가졌느냐로 비교하는 것이 아니라 같은 주소를 가지고 있느냐로 비교한다는 뜻이다. 따라서 설령 값이 같더라도 주소가 다르다면 다르다고 판단하는 것이다.


이렇게 비교하면 Equal, 같다는 값이 반환된다.

이 areEqual()이 기존의 얕은 비교를 하게 하지 않고, 우리가 저 주석 부분에 깊은 비교를 할 수 있는 코드를 넣었을 때 True를 반환하면 리렌더링을 하지 않고 (동일한 값이니까), False를 리턴했을 때만 다시 렌더링하게 한다.

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

위 코드에서 areEual부분을 다시 따왔다. 이를 통해서 우리가 리렌더링을 할 부분과 안 할 부분을 true와 false로 판단 할 수 있게 된다.


감성 일기장 12,13 최적화 완료

지금까지 만든 일기장에서 저장된 일기를 삭제하면 일기 저장하기도 깜빡이는 현상을 볼 수 있는데 그럴 필요는 없다. 따라서 최적화가 필요하다. 일기를 작성하는 폼이 다시 렌더링 될 필요는 없기 때문이다.

컴포넌트가 리렌더링 되는 경우는 어떤 경우들이 있을까?

1. 본인이 가진 state에 변화가 생겼을 때

2. 부모 컴포넌트에 리렌더링이 일어나거나

3. 자신이 가진 prop이 변경되는 경우

이렇게 3가지가 있다.

최적화하는 과정은 결국엔 내가 리렌더링 하고 싶은 부분을 리렌더링 했는데 만약 다른 부분에서도 리렌더링이 일어나면 그건 낭비가 되는거잖아? 그걸 잡아서 낭비되는 부분들에는 리렌더링이 일어나지 않게끔 코드를 수정하는 과정이 최적화다. 단순히 이 일기장으로 봐서는 와닿지 않을 수 있어도, 유튜브나 인스타처럼 사진이나 영상처럼 큰 용량을 차지하는 요소들이 하나하나의 동작마다 불필요하게 다 리렌더링 된다면 그 페이지는 엄청 느려질거다. 이렇게 생각하니까 최적화의 중요성이 와닿는다!!

그리고 그러기 위해서 가장 대표적으로 많이 사용할 수 있는게 앞서 배운 useMemo(), React.memo(), useCallBack()인거고. 음음 좋아.


감성 일기장 14. 복잡한 상태 변화 로직 분리


리액트에는 굉장히 많은 상태 변화 함수가 존재한다.


이번 강의에서는 이 useReducer이라는 친구에 대해 배운다.
이 친구를 사용하면

이렇게 여러개의 상태변화 함수를 이렇게 사용할 수 있게 해준다.

App.js

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

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: //switch문에는 반드시 default case도 적어줘야함. 까먹지 마 !
      return state;
  }
};

const App = () => {
  const [data, dispatch] = useReducer(reducer, []);
  const dataId = useRef(0);
  const getData = 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 });
  };

  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 memoizedDiaryAnalysis = 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 } = memoizedDiaryAnalysis;

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

export default App;

길어서 거부감이 들긴 하지만 실습을 위해 따라친 코드를 천천히 보면 reducer의 역할을 이해할 수 있다. swtich문에서 default case를 까먹지 말고 적어야 한다는 점, switch문에서 각각 INIT, CREATE, REMOVE 등등의 경우에 대해서 어떤 action을 취할지 정해서 연결해주는 부분들! dispatch로 호출하는거까지 ! 그리고 reducer가 데이터를 받아서 새로운 item에 프러퍼티들 연결해주는거까지 지금 당장 봐도 완벽히 익히지는 못했지만 나중에 들었을 때 아 한 번 공부했었던거구나~ 정도까지는 할 수 있을 것 같다.


감성 일기장 16(최종). 컴포넌트 트리에 데이터 공급하기

드디어 마지막 강의다 ~!
먼저 props drilling이라는 개념이 나온다.

내가 안쓰는 prop들이랑 중복돼서 문제가 발생할 수 있는데 마치
props가 땅을 파고 들어가듯 드릴링하는 것 같다고 해서 붙은 이름이다.

쓸데없이 프로퍼티들로 연결하지 않고 대표 provider 한놈에게 직접 데이터들을 공급받게끔 하는 방식으로 보완할 수 있다.


리액트에서 Context를 생성하는 방법이다.
위 사진 속 두 번째 코드처럼 children을 이용해서 컴포넌트들을 넣어주면 된다.

직접 코드로 만들어보자 !

export const DiaryStateContext = createContext(null);
export const DiaryDispatchContext = createContext(null);
  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>
  );

이렇게 만들었던 Context.Provider을 App()에 추가해줘야 정상적으로 사용할 수 있다.

그리고 중요한 부분 !!

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

useMemo()써서 불필요한 리렌더링 핸들링 해주는 거 기억하자.

const DiaryEditor = () => {
  const { onCreate } = useContext(DiaryDispatchContext);

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

그리고 DiaryEditor.js에서도 이렇게 비구조화 할당을 이용해야한다는 점 기억하기 !!


context의 onCreact() 함수로부터 이렇게 내가 입력한 내용이 잘 불러와진다는 것까지 확인 완료!

자 여기까지 일단 고생많았어 내 자신 ,,,
이제 7강에서 스타일링 하면서 배포 준비까지 마지막 빠이팅 해보면 되겠다.
7강 강의 슬쩍 엿보기 했는데 나눔글씨체로도 꾸미고 하는 게 재밌어 보인다.
근데 강의 시간이 좀 ...
아자아자 !

profile
경제와 개발을 곁들인 기획자..!

3개의 댓글

comment-user-thumbnail
2023년 6월 3일

사진이 많으니까 보기 편하네요!
이제 택배는 왔습니까

답글 달기
comment-user-thumbnail
2023년 6월 4일

목차별로 정리되어있어서 좋아요.
잘 보고 갑니다. 감사합니다.

답글 달기

제 느낀점은요..! 말투 때문인지 뭔가 정리된 형식적인 글을 읽는다기 보다 직접 설명해주고 있는 듯한 기분이 드네욤! 읽으면서 다시 복습하는 기분이었어여! 수고하셨습니당~

답글 달기