인프런 이정한님의 한입크기로 잘라 먹는 리액트(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 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();
}, []);
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 컴포넌트에 노란색 박스로 강조 |
최적화 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;
최적화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>;
};
예제) 프로퍼티로 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
- 기존 데이터 전달할 때 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);
'FRONTEND > React' 카테고리의 다른 글
[ 인프런 - 한입 크기로 잘라 먹는 React] 3. React 실전 프로젝트 - 감정 일기장 만들기(2) (0) | 2023.01.09 |
---|---|
[ 인프런 - 한입 크기로 잘라 먹는 React] 3. React 실전 프로젝트 - 감정 일기장 만들기(1) (0) | 2023.01.08 |
[ 인프런 - 한입 크기로 잘라 먹는 React] 2. React 기본 - 간단한 일기장 프로젝트 (1) (0) | 2023.01.03 |
[ 인프런 - 한입 크기로 잘라 먹는 React ] 1. React 기초 (0) | 2022.12.30 |
[ 생활코딩 Web2 - React ] 컴포넌트, props, 이벤트, state, create, update (0) | 2022.12.11 |