-
개발공부 40,41일차 [유데미 리엑트 강의]개인공부 2023. 3. 10. 10:57
리엑트 Lifecycle 제어하기 - useEffect
Class React Component Only(아래 함수들은 class형 컴포넌트에서만 사용가능)
Mount
- ComponentDidMount
- 컴포넌트가 태어나는 순간에 어떤걸 수행할 수 있는 함수
Update
- ComponentDidUpdate
- 컴포넌트가 변화하는 순간에 사용할 수 있는 함수
Unmount
- ComponentWillUnmount
- 컴포넌트가 사라지기 이전에 호출 할 수 있는 함수
원래 state같은 기능들은 함수형 컴퍼넌트는 사용이 불가한데
React Hooks로 use키를 불러와서 함수처럼 사용할 수 있게 된다.
Class형 컴포넌트의 길어지는 코드 길이 문제가 생기고 복잡하고 중복코드가 많아져
가독성의 문제등등으로 인해 React Hooks가 나왔다.
Lifecycle제어
//Lifecycle.js import React, { useEffect, useState } from "react"; const Lifecycle = () => { const [count, setCount] = useState(0); const [text, setText] = useState(""); useEffect(() => { console.log("Mount!"); }, []) //useEffect함수가 컴퍼넌트가 마운트가 될때만 작동되려면 두번째 인자인 댑스에 빈배열을 넣어주고 콜백함수에 하고싶은 일을 넣어주면 된다. useEffect(() => { console.log("Update!") }) //리렌더링됬을때마다 콜백함수가 실행되게 하려면 두번째 인자를 넣지말고 실행하고싶은 일만 넣어주면 된다. useEffect(() => { console.log(`count is update : ${count}`) if (count > 5) { alert("count가 5를 넘었습니다. 따라서 1로 초기화합니다."); setCount(1); //count가 5를 넘으면 팝업창이 뜨고 count가 1로 바뀐다. } }, [count]) //count가 업데이트 됬을때 useEffect가 실행이 되도록 한다. useEffect(() => { console.log(`text is update : ${text}`) }, [text]) //text 업데이트 됬을때 useEffect가 실행이 되도록 한다. return ( <div style={{ padding: 20 }}> <div> {count} <button onClick={() => setCount(count + 1)}>+</button> </div> <div> <input value={text} onChange={(e) => setText(e.target.value)} /> </div> </div> ) } export default Lifecycle;
Unmount될때 제어하기
import React, { useEffect, useState } from "react"; const UnmountTest = () => { useEffect(() => { console.log("mount!") return () => { //Unmount 시점에 실행되게 됨. console.log("unmount!") } }, []) return <div>Unmount Testing Component</div> //mount될때 보여진다. } const Lifecycle = () => { const [isVisible, setIsVisible] = useState(false) const toggle = () => setIsVisible(!isVisible) return ( <div style={{ padding: 20 }}> <button onClick={toggle}>on/off</button> {isVisible && <UnmountTest />} {/* 단락회로 평가를 통해 isVisible이 true이면 UnmountTest가 실행이되고 false이면 UnmountTest도 실행이 되지 않는다. */} </div> ) } export default Lifecycle;
React에서 API 호출하기
일기데이터를 가져올 무료 API서비스 이용
https://jsonplaceholder.typicode.com/
//App.js 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, //(Math.random() * 5) -> 0부터 4까지의 랜덤난수를 호출하게되는데 정수만 불러오는게 아닌 // 소수점자리수가 나올 수 있어 Math.floor를 사용해서 소수점을 버려준다. //+1을 해줌으로써 1~5까지 나올 수 있다. created_date: new Date().getTime(), id: dataId.current++ } }) setData(initData) } useEffect(() => { getData(); }, [])
1. getData라는 상수를 만들어서 fatch로 Api를 불러온다.
const getData = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/comments').then((res) => res.json())
2. 가지고온 데이터가 500개이기 때문에 20개만 잘라서 가져와서 가공 후
setData함수에 넣어줘서 일기데이터의 초기값으로 사용 할 수 있게 된다.
const initData = res.slice(0, 20).map((it) => { return { author: it.email, content: it.body, emotion: Math.floor(Math.random() * 5) + 1, //(Math.random() * 5) -> 0부터 4까지의 랜덤난수를 호출하게되는데 정수만 불러오는게 아닌 // 소수점자리수가 나올 수 있어 Math.floor를 사용해서 소수점을 버려준다. //+1을 해줌으로써 1~5까지 나올 수 있다. created_date: new Date().getTime(), id: dataId.current++ } }) setData(initData) }
3. useEffect를 사용해서 mount되는 시점에 getData가 실행되게 하면
브라우저를 열었을때 getData가 실행되어 위에 넣어준 초기값이 보이게 된다.
useEffect(() => { getData(); }, [])
React Developer Tools
현재 가지고있는 데이터, props등을 쉽게 확인 할 수 있다.
현재 진행중인걸 확인 할 수 있는 기능이다.
최적화1 - 연산 결과값을 재사용 하는 방법
Memoization
이미 계산 해 본 연산 결과를 기억 해 두었다가
동일한 계산을 시키면, 다시 연산하지 않고 기억 해 두었던 데이터를 반환 시키게 하는 방법
마치 시험을 볼 때 이미 풀어본 문제는 다시 풀어보지 않아도 답을 알고 있는 것 과 유사함
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 onEdit={onEdit} onRemove={onRemove} diaryList={data} /> </div> );
이렇게 되면 콘솔에 일기 분석 시작 함수가 2번동작하는걸 볼 수 있다.
App.js가 처음 실행이 될때 초기값을 빈배열로 지정했기 때문에 한번 실행이되고
const [data, setData] = useState([])
이전 API 호출했을때 getData함수가 실행이 되고 setData의 값이 바뀌게 된다.
useEffect(() => { getData(); }, [])
리렌더링되면 App.js안에 함수들이 다시 실행이 되고
const { goodCount, badCount, goodRatio } = getDiaryAnalysis()
getDiaryAnalysis() 함수가 다시 실행이 되면서 총 2번 호출이된다.
리스트안에 일기를 수정을 하면 App컴퍼넌트가 리렌더링이 되고 함수가 다시 실행이 되어서
getDiaryAnalysis함수가 다시 호출이 된다.
다만 우리가 수정하는건 content이지 감정점수를 수정하는게 아니기 때문에
getDiaryAnalysis함수가 다시 수행하는건 좋지 않다.
useMemo 사용하기
Memoization하고싶은 함수를 감싸주면 된다.
다만 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]); const { goodCount, badCount, goodRatio } = getDiaryAnalysis; //getDiaryAnalysis 호출한걸 객체로 비구조화 할당으로 받는다.
data의 갯수가 변하지 않고서는 getDiaryAnalysis는 실행되지 않는다.
최적화2 - 컴포넌트 재 사용
React.memo
https://ko.reactjs.org/docs/getting-started.html
React.memo는 고차 컴포넌트이다.
고차컴포넌트란 ? 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.
const MyComponent = React.memo(function MyComponent(props) { /* props를 사용하여 렌더링 */ });
다만 부모컴포넌트에 의한 리렌더링은 막을 수 있지만 자기자신의 리렌더되면 리렌더링한다.
import { useEffect, useState } from "react"; 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> } const OptimizeTest = () => { const [count, setCount] = useState(1) const [text, setText] = useState("") return <div style={{ padding: 50 }}> <div> <h2>count</h2> <CountView count={count} /> <button onClick={() => setCount(count + 1)}>+</button> </div> <div> <h2>text</h2> <TextView text={text} /> <input value={text} onChange={(e) => setText(e.target.value)} /> </div> </div> }; export default OptimizeTest
이렇게 되면 count가 변할때도 Textview가 렌더링된다.
필요없는 상황에서도 렌더링 되는걸 막기 위해 React.memo를 사용한다.
import React, { useEffect, useState } from "react"; const TextView = React.memo(({ text }) => { useEffect(() => { console.log(`update :: Text : ${text}`) }) return <div>{text}</div> }); const CountView = React.memo(({ count }) => { useEffect(() => { console.log(`update :: count : ${count}`) }) return <div>{count}</div> }); const OptimizeTest = () => { const [count, setCount] = useState(1) const [text, setText] = useState("") return <div style={{ padding: 50 }}> <div> <h2>count</h2> <CountView count={count} /> <button onClick={() => setCount(count + 1)}>+</button> </div> <div> <h2>text</h2> <TextView text={text} /> <input value={text} onChange={(e) => setText(e.target.value)} /> </div> </div> }; export default OptimizeTest
mount - html이 다 그려지고 나서 그 다음시점
자바스크립트는 객체의 값이 같아도 객체의 주소가 다르면 같지 않다고 판단한다.
다만
let a = {count: 1} let b = a;
이렇게 b = a 일때는 같은 값도 가지고 자바스크립트는 둘이 같다 라고 판단한다.
function MyComponent(props) { /* props를 사용하여 렌더링 */ } function areEqual(prevProps, nextProps) { /* nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환 */ } export default React.memo(MyComponent, areEqual);
이 메서드는 오직 성능 최적화 를 위하여 사용됩니다. 렌더링을 “방지”하기 위하여 사용하지 마세요. 버그를 만들 수 있습니다.
const areEqual = (prevProps, nextProps) =>{ return true // 이전 프롭스와 현재 프롭스가 같다 ->리렌더링을 일으키지 않게 된다. return false // 이전 과 현재가가 다르다. -> 리렌더링을 일으킨다. }
<React.memo 최종 예제코드 >
더보기import React, { useEffect, useState } from "react"; const CounterA = React.memo((({ count }) => { useEffect(() => { console.log(`CounterA update = count : ${count}`) }) return <div>{count}</div> })) const CounterB = (({ obj }) => { useEffect(() => { console.log(`CounterB update = obj : ${obj.count}`) }) return <div>{obj.count}</div> }) const areEqual = (prevProps, nextProps) => { return prevProps.obj.count === nextProps.obj.count //위 조건이 맞으면 true(리렌더링되지 않음)false면 리렌더링이 된다. } const MemoizedCounterB = React.memo(CounterB, areEqual) //위에 areEqual함수는 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> {/* setCount(count)는 카운터는 1로 지정을 했기때문에 setCount도 1이된다. */} </div> <div> <h2>Counter B</h2> <MemoizedCounterB obj={obj} /> <button onClick={() => setObj({ count: obj.count, })}>B button</button>q </div> </div>) }; export default OptimizeTest
최적화 3 - useCallback
React.memo
만든 파일 전체를 감쌀때엔 상단에 React도 넣어주고
import React, { useEffect, useState, useRef } from "react";
하단에 DiaryEditor를 감싸서 내보내주면 DiaryEditor파일자체를 React.memo로 감쌀 수 있다.
export default React.memo(DiaryEditor);
useEffect로 잘 렌더링 되고있는지 확인해보면
useEffect(() => { console.log("DiaryEditor 렌더") })
콘솔에 “DiaryEditor 렌더”가 두번 찍히는걸 볼 수 있다.
두번찍히는 이유는 App.js를 봤을때
const [data, setData] = useState([])
data가 초기값이 빈배열이기 때문에 App컴포넌트가 한번 렌더링이 일어나고
DiaryEditor도 빈배열인 상태에서 한번 렌더링이 일어난다.
두번째는 컴포넌트가 마운트된 시점에 getData를 호출해서
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, //(Math.random() * 5) -> 0부터 4까지의 랜덤난수를 호출하게되는데 정수만 불러오는게 아닌 // 소수점자리수가 나올 수 있어 Math.floor를 사용해서 소수점을 버려준다. //+1을 해줌으로써 1~5까지 나올 수 있다. created_date: new Date().getTime(), id: dataId.current++ } }) setData(initData) } useEffect(() => { getData(); }, [])
마지막에 setData함수에 결과를 전달해서 data의 값이 바뀌게 되어 또 렌더링이 일어난다.
setData(initData)
이렇게되면 마운트 되자마자 두번의 렌더링이 되게 된거다.
이때문에 DiaryEditor컴포넌트가 전달받는 onCreate 함수도
//DiaryEditor.js const DiaryEditor = ({ onCreate }) => {
App컴포넌트가 렌더링되면서 같이 계속 렌더링 되게 된다.
//App.js const onCreate = (author, content, emotion) => { //author,content,emotion를 파라미터로 받아온다. const created_date = new Date().getTime(); const newItem = { author, content, emotion, created_date, id: dataId.current } dataId.current += 1 //dataId의 초기값으로 0을 줬고 새로운 Item마다 +1이되어 아이디가 부여된다. setData([newItem, ...data]) //원래 있던 data위로 새로운 newItem이 들어오게된다. }
onCreate함수에 내용은 리렌더링이 된다고해도 똑같다.
다만 비원시타입 자료형의 비교는 얕은비교로 일어나기 때문에 DiaryEditor컴포넌트가
props으로 가지고있는 onCreate 함수가 App컴포넌트가 렌더링이 될때마다 다시 만들어져서
onCreate 함수 때문에 DiaryEditor컴포넌트가 계속 만들어지고 있다고 생각 할 수 있다.
결론은 onCreate 함수가 재생성 되지 않아야만 DiaryEditor컴포넌트를 React.memo와 함께 최적화 할 수 있다.
그러려면 onCreate함수가 재생성 되지 않게 해야하는데 여기에 usememo는 사용하면 안된다.
이유는 usememo는 값을 반환하기 때문에 onCreate함수는 값을 반환하는게 아닌 원본그대로 DiaryEditor로 보내줘야하기 때문인다.
useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
메모이제이션된 콜백을 반환합니다.
두번째 인자로 전달한 값이 변화하지 않으면 첫번째 인자로 전달한 콜백함수를 계속 재사용 할 수 있다.
const onCreate = useCallback((author, content, emotion) => { //author,content,emotion를 파라미터로 받아온다. const created_date = new Date().getTime(); const newItem = { author, content, emotion, created_date, id: dataId.current } dataId.current += 1 //dataId의 초기값으로 0을 줬고 새로운 Item마다 +1이되어 아이디가 부여된다. setData([newItem, ...data]) //원래 있던 data위로 새로운 newItem이 들어오게된다. }, [])
useCallback을 사용해서 두번째 인자를 빈배열로 지정을해서 마운트 되는 시점에 한번만 렌더링이 되고 그이후로는 그다음부터는 첫번째인자로 저장했던 함수를 사용할 수 있도록 했다.
이렇게되면 리스트를 삭제해도 DiaryEditor가 렌더링 되지 않는다.
그런데 DiaryEditor에 일기를 쓰고 일기리스트에 추가를하면 원래있던 일기들은 삭제가되고 추가한 일기만 남게된다.
이유는 onCreate함수에 useCallback를 넣었을때 두번째 인자에 빈배열을 넣었기 때문이다.
onCreate함수는 컴포넌트가 마운트되는 시점에 한번만 사용되기 때문에 그당시에
onCreate함수가 마지막으로 실행될당시 빈배열이였기 때문에 이런현상이 발생하게 된거다.
함수는 컴포넌트가 재실행될 때 다시 생성되는 이유가 있습니다. 현재의 상태 값을 참조해야 한다.
하지만 onCreate 함수는 콜백 안에 갖혀 있어서 deps를 빈 배열로 전달했기 때문에 onCreate 함수 값은 계속 빈 배열이다.
빈배열에 추가했기때문에 저장한 1개의 값만 남게되는거다.
deps는 배열의 값이 변경되면 함수를 재생성 하게된다.
그런데 onCreate 함수가 재생성되지 않으면 최신의 데이터상태의값을 참고할 수 없다.
이걸위해 함수형 업데이트가 필요하다.
함수형 업데이트란 ?
상태변환 함수에 함수를 전달하는것
setData((data)=>[newItem, ...data])
인자로 data를 받아서 Item을 추가한 data를 retun하는 콜백함수를 setData함수에다 전달한다.
이렇게되면 useCallback의 두번째 인자를 [] 빈배열로 두어도 항상 최신의 값을
setData의 data라는 인자를 통해서 참고할 수 있게된다.
프로젝트 최적화 완
Item 1개를 삭제했을때 전체 Item을 전체 다 삭제 된다.
1. DiaryItem컴포넌트 전체를 React.memo로 묶어준다.
export default React.memo(DiaryItem);
2. React를 임포트한다.
import React, { useEffect, useRef, useState } from "react"
3. useEffect로 어떤 item들이 리렌더링이 진행되는지 확인한다.
useEffect(() => { console.log(`${id}번 째 아이템 렌더!`) })
React.memo로 감싼다고해도
onEdit, onRemove,
두개는 함수이기 때문에 데이터의 값이 변화하면 재생성 될 수 밖에 없다.
4. App.js 에 onRemove함수를 useCallback에 감싼다.
const onRemove = useCallback((targetId) => { const newDiaryList = data.filter((it) => it.id !== targetId) //targetId가 아닌것만 필터링해서 배열로 바꾼다. setData(newDiaryList) //필터링된 배열을 setData에 넣어서 data의 상태를 변화시킨다. }, [])
5. data를 최신의 값으로 줘야해서 아래처럼 변경 해줘야 한다.
const onRemove = useCallback((targetId) => { setData(data => data.filter((it) => it.id !== targetId)) //필터링된 배열을 setData에 넣어서 data의 상태를 변화시킨다. }, [])
6. onEdit함수도 useCallback을 이용해서 동일하게 변경 해 준다.
const onEdit = useCallback((targetId, newContent) => { //수정하는ID, 수정하는 Content를 받는다. setData((data) => data.map((it) => it.id === targetId ? { ...it, content: newContent } : it) )//map내장함수를 이용해서 모든요소를 순회하면서 새로운 배열을 만들어서 setData에 전달한다. //모든요소에 수정대상이라면 (it.id가 선택한 id라면)모든 배열에 content를 newContent로 교체한다. 대상이 아니라면 원래있던 데이터를 리턴해준다. }, [])
결과는 아이템하나를 삭제하더라도 전체가 렌더링이 되지 않고, 아이템을 추가했을때 그 아이템만 렌더링 되는걸 확인 할 수 있다.
온보딩커리큘럼에 기본언어 공부한다고 ㅠㅠ 쪼끔씩 하다보니까 블로그에 올릴 타이밍을 못잡아서 계속 미루다 겨우 올렸다
최근에 다시 리엑트 강의를 보면서 느낀건 내가 메서드 컴포넌트 등 단어의 정확한 정의를 몰라서 더 이해를 못하는것같다
어려운것도 어려운거지만.. 기본이 없음을 다시 느꼈다
일단 이건 완주하는데까지 힘쓰겠지만 기초단어를 다시 공부하고 2회강하는 목표를 가져야 될것같다.
요새 이것도 봐야하고 저것도 봐야하는 욕심에 시간은 부족하고 마음만 조급해지니까 다 못하겠고 싶긴한데
으쌰 해봐야지
'개인공부' 카테고리의 다른 글
CSS(position, 텍스트 관련 속성, display&border) (3) 2023.03.22 CSS(시맨틱마크업,선택자) (3) 2023.03.22 개발공부 35일차 [유데미 리엑트 강의] (1) 2023.03.03 개발공부 34일차 [유데미 리엑트 강의] (0) 2023.02.27 개발공부 32일차 [유데미 리엑트 강의] (0) 2023.02.27