DEVELOP
article thumbnail

인프런 이정한님의 한입크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지 ] 강의 수강 후 강의의 내용을 정리하며 공부한 것을 쓴 게시글입니다.

 

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

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

www.inflearn.com

- 소스코드 : https://github.com/cjy921004/reactDiary

 

GitHub - cjy921004/reactDiary

Contribute to cjy921004/reactDiary development by creating an account on GitHub.

github.com


React Lifecycle 제어하기 - useEffect 

# Lifecycle (생애주기)

: 일반적으로 시간에 흐름에 따라, 탄생부터 죽음에 이르는 순간에 따른 단계적인 과정 

- React 컴포넌트도 이런 생명주기 Lifecycle을 갖는다. (탄생 - 변화 - 죽음)

- 탄생 (Mount)  : 화면에 나타나는 것  (ex:초기화 작업)

- 변화 (Update) : 업데이트 (리렌더)   (ex:예외 처리 작업)

- 죽음 (UnMount) : 화면에서 사라짐   (ex:메모리 정리 작업)

react lifecycle

# React Hooks 

: 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해준다. 

- ex : useState, useRef, useEffect 등 

- 2019.06 정식 출시된 기능 

-  클래스형 컴포넌트의 길어지는 코드 길이 문제, 중복 코드, 가독성 문제 등을 해결하기 위해 등장함 

 

#useEffect

- 두개의 파라미터  callback 함수와 Dependency Array(의존성 배열)을 전달

- Dependency Array(의존성 배열)에 들어있는 값 중 하나라도 바뀌면 callback 함수가 실행 

- 변화를 감지하고 싶은 값만 의존성 배열에 넣어 콜 백 함수를 수행할 수 있음 

import { useEffect } from "react";

useEffect(()=>{
    //todo...
   }, []);

 

# useEffect로 변화단계(Update) 구현하기 

- + 버튼 클릭 마다 count 값이 증가되는데, useEffect의 의존성 배열에 count 값을 넣었으므로, count 값이 증가할 때 마다 해당 콜백 함수 호출됨 

  useEffect(() => {
    console.log(`count is update : ${count}`);
    if (count > 5) {
      alert("count가 5를 넘었습니다. 1로 초기화합니다.");
      setCount(1);
    }
  }, [count]);

 

# useEffect로 탄생 단계(Mount)와 죽음 단계(UnMount) 구현하기 

- ON/OFF 버튼 = Mount/UnMount 버튼 

- mount :  useEffect의 의존성 배열에 빈 배열을 전달하면, 콜백함수가 실행 됨 

- unmount : 콜백함수 안에 리턴문을 작성함 

const UnmountTest = () => {
  useEffect(() => {
    console.log("Mount!");

    return () => {
      //Unmount 시점에 실행되게 됨
      console.log("Unmount! ");
    };
  }, []);

  return <div>Unmount Testion Component</div>;
};
const LifeCycle = () => {
  const [isVisible, setIsVisible] = useState(false);
  const toggle = () => setIsVisible(!isVisible);

  return (
    <div sytle={{ padding: 20 }}>
      <button onClick={toggle}>ON/OFF</button>
      {isVisible && <UnmountTest />}
    </div>
  );
};


React에서 API 사용하기 

- 무료 json API 공유하는 사이트 : https://jsonplaceholder.typicode.com/

 

JSONPlaceholder - Free Fake REST API

{JSON} Placeholder Free fake API for testing and prototyping. Powered by JSON Server + LowDB. Tested with XV. As of Oct 2022, serving ~1.7 billion requests each month.

jsonplaceholder.typicode.com

- 초기 다이어리 데이터는 빈 배열이므로, 초기 일기 리스트를 대체할 데이터를 위 사이트의 comments resources 에서 뽑아오도록 한다. 

- comments는 아래와 같은 형식의 데이터를 갖고 있는 객체 배열이다.

  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
  },
  {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
  },

 

# App.js - getData 함수 선언 

- comment 중 slice로 20개의 데이터만 뽑아서 map으로 하나씩 순회한다. 

- comments 의 email을 작성자로, body를 일기의 본문으로하고, 감정 점수는 1부터 5까지의 숫자 중 랜덤으로 설정하고, 작성 시간은 현재 시간으로 하며, id는 dataId 값을 하나씩 증가한 값으로 설정해 initData 배열을 생성한다. 

- 생성된 배열 initData를 현재 다이어리 데이터인 Data에 저장하기 위해  setData 함 

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.ceil(Math.random() * 5),
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });

    setData(initData);
  };

 

#  App.js 

- 처음 실행될 때만 API 를 호출하기 위해서, useEffect의 콜백 함수로 getData를 호출하고, 의존성 배열 deps에 빈 배열을 넣는다. 

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

API 호출 후 초기 실행 시 일기 리스트 출력 화면


React Developer tools 

- chrome 웹 스토어에서 react developer tools 다운로드하기

: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi

 

React Developer Tools

Adds React debugging tools to the Chrome Developer Tools. Created from revision 47f63dc54 on 12/6/2022.

chrome.google.com

- 개발자도구(F12)의 컴포넌트 탭에서 컴포넌트 계층 구조와 각각의 컴포넌트가 어떤 데이터와, props, state 등을 가지고 있는지 쉽게 확인 가능 

- components - view settings - Highlight updates when components render 를 체크하면 현재 리렌더링중인 컴포넌트에  박스로 강조표시가 됨 

일기 작성 중일때 DiaryEditor에 노란색 박스로 강조

일기 작성 중일때 DiaryEditor 컴포넌트에 노란색 박스로 강조 

최적화 1 - 연산 결과 재사용

# Memoization

: 이미 계산 해 본 연산 결과를 기억해 두었다가, 동일한 계산을 시키면 다시 연산하지 않고 기억해 두었던 데이터를 반환 시키게 하는 방법 

 - 마치 시험을 볼 때, 이미 풀어본 문제는 다시 풀어보지 않아도 답을 알고 있는 것과 유사함 

 

# useMemo 

: 렌더링 최적화를위해 사용되는 React Hooks 

- 첫번쨰 인자로 콜백 함수를, 두번째 인자로 의존성 배열(deps)을 받는다.

- 의존성 배열 안의 값이 변경될 때에만 콜백 함수를 호출한다.

 

예제 ) 감정 점수가 3이상일때, 감정이 good, 이하일 때 bad라고 하면, 감정이 good인 일기의 개수와 bad인 일기의 개수를 구해 good인 일기의 비율을 구하는 예제 

  •  useMemo를 사용하지 않으면 기존 일기의 내용을 수정하는 등 감정이 good인 일기의 비율이 변화가 없음에도 해당 함수가 실행됨 
  •  useMemo를 사용하여 의존성 배열안에 전체 일기의 개수인 data.length를 넣어놓으면, 일기가 추가되거나 삭제되는 등ㅇ의 일기의 개수가 변화가 있을때만 해당 함수를 실행함 
  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]);

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

각각 useMemo를 사용하지 않을 때와 useMemo를 사용할 경우, 일기 본문 수정했을 때


최적화2 - 컴포넌트 재사용 

# React.memo

: 컴포넌트에서 리렌더링이 필요한 상황에서만 해주도록 설정할 수 있는 기능 

- props가 변경되었는지를 체크하여 같은 props가 들어오면 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용함 

- 함수형 컴포넌트에게 업데이트 조건을 걸 수 있음 

- props로 객체가 주어졌을 때 객체에 대해 얕은 비교를 하기 때문에 객체 안의 프로퍼티 값이 그대로여도 리렌더링이 될 수 있음 

- 다른 비교 동작을 위해서 React.memo의 두번째 인자로 별도의 함수를 제공해주면 됨 

 

 

예제) text와 count를 각각 props로 갖는 두 컴포넌트 TextView와 CountView의 부모 컴포넌트가 리렌더링하는 예제 

  • React.memo 를 사용하지 않을 경우에는 text, count값 중 하나만 변경되어도 두 컴포넌트 모두 리렌더링 됨
  • React.memo 를 사용할 경우에는 text 값이 변하면, TextView 컴포넌트만 리렌더링이 되고, count 값이 변하면 CountView 컴포넌트만 리렌더링이 됨 
const TextView = ({ text }) => {
  useEffect(() => {
    console.log(`Update :: text : ${text}`);
  });
  return <div>{text}</div>;
};

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

React.memo를 사용하지 않을 떄와 React.memo를 사용할 때 리렌더링 차이 

예제) 프로퍼티로 count 값을 가지고 있는 객체를 props로 갖는 컴포넌트에 버튼을 눌러도 그 전 count 값으로 그대로 유지되는 예제

  • React.memo의 두번째 파라미터로 별도의 비교함수를 주지 않을 경우 프로퍼티의 값이 변경되지 않아도 컴포넌트가 계속 리렌더링 됨 
         - 객체 비교시 객체의 고유 메모리 주소를 비교하는 얕은 비교를 하기 때문에 객체 안의 프로퍼티의 값이
          같을지라도 다른 객체라고 판단하여 리렌더링이 됨 
  • React.memo의 두번째 파라미터로 프로퍼티를 비교하는 함수를 줄 경우 프로퍼티의 값이 변경되지 않으므로 컴포넌트가 리렌더링되지 않음 
  const [obj, setObj] = useState({
    count: 1,
  });
        <button onClick={() =>setObj({count: obj.count,})}>B button</button>
const CounterB = ({ obj }) => {
  useEffect(() => {
    console.log(`Counter B Update - count : ${obj.count}`);
  });
  return <div>{obj.count}</div>;
};

const areEqual = (prevProps, nextProps) => {
  return prevProps.count === nextProps.count;
};

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

별도 비교함수를 주지 않았을 경우에는 계속 리렌더링되고, 비교함수 주어지면 리렌더링되지 않음  


최적화3 - useCallback활용

# useCallback

: 메모이제이션된 콜백을 반환 

- useMemo는 특정 결과값을 재사용할 때 사용하는 반면, useCallback은 특정 함수를 재생성하지 않고, 재사용하고 싶을 떄 사용 

- 한 번 만든 함수를 필요할 때만 새로 만들고 재사용하는 것이 최적화에 중요하기 때문 

- 의존성 배열 안의 값이 변경되었을 때만 콜백 함수를 실행 

 

- App.js 의 onCreate 함수를 useCallback으로 감싸줌 

  const onCreate = useCallback((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]);
  }, []);

-> 문제점 : 일기를 추가하면 기존 20개의 일기들이 삭제되고, 최근 추가한 일기만이 출력됨 

 : onCreate를 초기에 만들면서 setData를 할 때, 초기의 반 배열 data로 setState되고 그대로 유지되기 때문 

-> 해결방법 : 함수형 업데이트 활용

초기 일기 리스트에 새 일기를 추가했을 때 생기는 문제점

 

# 함수형 업데이트

: setState의 인수를 새로운 상태값이 아닌 콜백함수를 전달하는 방식 

- setState에 콜백함수로 전달된 함수는 어떤 상황이든 항상 매개변수로 최신의 state를 제공받음 

 

- onCreate 함수 내부의 setData를 아래와 같이 함수형 업데이트 해 줌

    setData((data) => [newItem, ...data]);

최적화4 - 최적화 완성

- 기존 문제점 : 하나의 일기를 삭제하면 모든 일기들이 리렌더링 됨 

- 이유 : DiaryItem의 props에 있는 onRemove, onEdit 함수가 계속 재생성되기 때문 

- 해결방안 : DiaryItem을 React.memo로 감싸고, onRemove와 onEdit을 useCallback으로 감싸고, setData를 함수형 업데이트로 바꿔준다. 

  const onRemove = useCallback((targetId) => {
    setData((data) => data.filter((it) => it.id !== targetId));
  }, []);

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

복잡한 상태 관리 로직 분리하기 - useReducer

# useReducer 

: useState의 대체 함수. 컴포넌트의 state를 밖으로 분리하여 관리 가능 

const [상태객체, dispatch함수] = useReducer(reducer 함수, 초기값);

- dispatch 함수 

   : 상태를 변화시키는 액션을 발생시키는 함수 

   - 인자로 객체를 받는데, type 프로퍼티가 꼭 있어야 함 

 

- reducer 함수

   : 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수 

   - switch-case문으로 작성하고 return 해주는 값이 새로 바뀌는 상태 값 

   - 인자로 state와 action을 받음 

   - state는 현재 값이고 action은 dispatch 함수에서 넘겨준 값 

  const [data, dispatch] = useReducer(reducer, []);
const reducer = (state, action) => {
  switch (action.type) {
    case "INIT": {
      return action.data; // 이 값으로 새로운 state가 됨
    }

    case "CREATE": {
      const created_date = new Date().getTime();
      const newItem = {
        ...action.data,
        created_date,
      };
      return [newItem, ...state];
    }

    case "EDIT": {
      return state.map((it) =>
        it.id === action.targetId ? { ...it, content: action.newContent } : it
      );
    }

    case "REMOVE": {
      return state.filter((it) => it.id !== action.targetId);
    }

    default:
      return state; //그대로 전달
  }
};
   const initData = res.slice(0, 20).map((it) => {
      return {
        author: it.email,
        content: it.body,
        emotion: Math.ceil(Math.random() * 5),
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });
    dispatch({ type: "INIT", data: initData });
  };

  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 });
  }, []);

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

기존 단방향 트리와 Context를 사용한 트리 

- 기존 데이터 전달할 때 props를 통해 데이터를 전달함 

- 컴포넌트가 매우 많고 깊을 때, 여러 컴포넌트를 거쳐 전달해야하므로 복잡하고 관리 어려움  

-> context 사용으로 해결 

 

# Context 

: 리엑트 컴포넌트 간에 어떤 값을 공유할 수 있게 해주는 기능 

- props가 아닌 또 다른 방식으로 컴포넌트 간에 데이터를 전달하는 방법 

- React.createContext로 context 생성 

- context 객체 안 provider 컴포넌트에 공유하고자 하는 데이터를 value 값으로 지정하면 자식 컴포넌트에서 해당 데이터에 접근 가능 

- 자식 컴포넌트에서는 useContext로 데이터에 접근 가능

export const DiaryStateContext = React.createContext();
export const DiaryDispatchContext = React.createContext();

- 상태 관리 함수들은 memoizedDispatches에 따로 넣음 

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

- 최상위 provider로 DiaryStateContext를 통해 다이어리 data를 value 값으로 넘겨주고, 바로 아래에 DiaryDispatchContext 를 통해 위에 선언한 상태관리 함수들을 value 값으로 넘겨줌 

  return (
    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider value={memoizedDispathces}>
        <div className="App">
          <DiaryEditor onCreate={onCreate} />
          <DiaryList onRemove={onRemove} onEdit={onEdit} />
        </div>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );

- 하위 컴포넌트들은 useContext를 통해 값들을 전달받을 수 있음 

  const { onEdit, onRemove } = useContext(DiaryDispatchContext);

 

profile

DEVELOP

@JUNGY00N