[React] Review
💎 npm start
- 폴더 생성
- VSCode에서 해당 폴더로 들어감
- 터미널에서
npx create-react-app 프로젝트 이름
- ctrl+x -> 폴더로 파일들을 이동시킨다.
- npm start를 통해서 열기
💎 파일 살펴보기
결국 src에는 App.css, App.js, index.css, index.js 파일만 남아있고, css 파일의 내용만 지워주면 준비 끝!
💎 컴포넌트 추가하기
🔆 +/- 버튼에 따라 숫자 변경 시키기
// src > Counter.js
import React, { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const onIncrease = () => {
setCount(count + 1);
};
const onDecrease = () => {
setCount(count - 1);
};
return (
<div>
<h2>{count}</h2>
<button onClick={onDecrease}>-</button>
<button onClick={onIncrease}>+</button>
</div>
);
};
export default Counter;
const [count, setCount] = useState(0);
: count를 0으로 초기화 setCount를 통해서 리랜더를 한다.
- 버튼을 누르면 onDecrease/onIncrease 함수가 적용된다.
- 해당 함수에 있는 setCount 를 통해서 count가 변경된다.
- 변경된 count는 useState 로 넘어간다.
- count 값이 변경된다.
🔆 숫자에 따라 짝수, 홀수 나누기.
위에서 만들었던 Counter.js를 사용해서 나온 숫자가 짝수인지 홀수인지를 판별하는 컴포넌트를 만드려고 한다.
💡THINK
Counter.js에 있는 count라는 숫자가 홀수인지 짝수인지를 판별하는 다른 컴포넌트를 만든다고 했을 때, Counter.js에서 OddEvenResult.js로 count를 전달해주어야 한다.
// src > Counter.js
import React, { useState } from "react";
import OddEvenResult from "./OddEvenResult";
const Counter = ({ initialValue }) => {
console.log(initialValue);
const [count, setCount] = useState(initialValue);
const onIncrease = () => {
setCount(count + 1);
};
const onDecrease = () => {
setCount(count - 1);
};
return (
<div>
<h2>{count}</h2>
<button onClick={onDecrease}>-</button>
<button onClick={onIncrease}>+</button>
<OddEvenResult count={count}></OddEvenResult>
</div>
);
};
export default Counter;
count={count}
형식을 통해서 프롭을 전달해주고 거기서 짝수인지 홀수인지 판별한다.
const OddEvenResult = ({ count }) => {
console.log("Render");
return <>{count % 2 === 0 ? "Even" : "Odd"}</>;
};
export default OddEvenResult;
💎 다이어리 만들기
❓input 창에서 name, value의 역할은?
import { useState } from "react";
const DiaryEditor = () => {
const [state, setState] = useState({
author: "",
content: "",
});
return (
<div className="DiaryEditor">
<h2>오늘의 일기</h2>
<div>
<input
name="author"
value={state.author}
onChange={(e) => {
setState({
author: e.target.value,
content: state.content,
});
}}
/>
</div>
<div>
<input
value={state.content}
onChange={(e) => {
setState({
author: state.author,
content: e.target.value,
});
}}
/>
</div>
</div>
);
};
export default DiaryEditor;
먼저 변경될 수 있는 값인 author, content를 모두 useState의 객체 형태로 묶은 것을 확인할 수 있다.
그래서 author이 변경된다면 content는 유지하고, content가 변경된다면 author이 유지되도록 하기 위해서 onChange 이벤트를 사용해서 setState를 통해서 변경시켜준 것을 확인할 수 있다.
근데 여기서 name과 value의 역할은 무엇일까?
🔆 input 태그의 name 속성 역할
Change 이벤트가 발생을 했다면 해당 변경된 값이 무엇을 가리키는 것인지를 알 수 있다.
따라서 e.target.name
은 어떤 부분이 변경되었는지를 알려준다.
Ex. author 부분의 input 값이 변경: e.target.name === “author”
🔆 input 태그의 value 속성 역할
현재 변경된 그 값 을 나타낸다. author 부분에서 변경된 값이 “손수경” 이면 e.target.value는 손수경이 된다. 또는 useState를 통해서 값을 매핑시키는 역할을 하는 것 같다.
❓ 이벤트 처리할 떼 특정 함수 사용하는 방법
import { useState } from "react";
const DiaryEditor = () => {
const [state, setState] = useState({
author: "",
content: "",
});
const handleChangeState = (e) => {
setState({
...state,
[e.target.name]: e.target.value,
});
};
return (
<div className="DiaryEditor">
<h2>오늘의 일기</h2>
<div>
<input
name="author"
value={state.author}
onChange={handleChangeState}
/>
</div>
<div>
<input
name="content"
value={state.content}
onChange={handleChangeState}
/>
</div>
</div>
);
};
export default DiaryEditor;
중괄호를 사용하되 괄호는 붙히지 말고 사용하자 이거 웹프기에서 배웠던 거 같기도 하고,,,,
❓❌Props 가 여러 개인 경우 전달하는 과정?
- App.js: total 관리
- DiaryEditor.js: 다이어리는 쓰고 저장하는 공간
- DiaryList.js: 저장된 다이어리가 리스트 별로 관리하는 공간
- DiaryItem.js: 리스트에 들어있는 하나의 Item을 관리하는 공간
그래서 지금 DiaryList 안에 DiaryItem을 넣을려고 했는데 그러기 위해서는 DiaryList에서의 it (일기 하나)를 DiaryItem으로 전달 해야 된다.
❓근데 문제가 DiaryList에서 프롭을 전달해줄때는
<DiaryItem key={it.id} {...it}></DiaryItem>
이런식으로 전달해주었는데 DiaryItem에서 전달받을 때에는
const DiaryItem = { author, content, created_date, emotion, id };
로 전달을 받는다. 왜,,,? 왜 주는데로 받지않고 지 혼자 프롭을 늘려,,,? => props 변수를 사용해서 …it 값을 다 받아올 수 있다. 하지만 이렇게 된다면 아이템 안에 있는 값들을 사용할 때마다 props.author, props.content,, 이런식으로 작성을 해주어야 하므로 아마도 바로 author, content, emotion 처럼 변수처럼 사용해주기 위해서 직접 프롭을 전달 받은 것 같다.
💎 DiaryList를 리액트 구조에 맞게 데이터 추가하기
❓ 왜 이렇게 코드를 짰을까?
리액트에서 데이터 추가하기 부분!
// src > App.js
import "./App.css";
import { useState, useRef } from "react";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
function App() {
const [data, setData] = useState([]);
const dataId = useRef(0);
const onCreate = ({ author, content, emotion }) => {
const created_data = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_data,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
};
return (
<div className="App">
<h1>일기장</h1>
<DiaryEditor oncreate={setData}></DiaryEditor>
<DiaryList diaryList={data}></DiaryList>
</div>
);
}
export default App;
일단 지금 보면 원래 있던 더미리스트를 지우고 리액트의 특징에 맞게 데이터를 이동시키는 것을 하고자 한다. 리액트의 특징이란 이벤트는 상위 컴포넌트로 전달이 되고 상위 컴포넌트에서 데이터가 전달되는 방식이다. 자세한 사항은 [React] 리액트에서 데이터 추가하기 에서 확인하길,,
🔆 에상 경로 및 코드 작성
- 사용자 입력
- 저장 버튼 클릭 -> DiaryEditor 에서 이벤트 발생
- DiaryEditor.js에 handleSubmit 함수 호출 -> onCreate함수를 호출, alert
- App.js로 매개변수가 전달됨 -> setData함수를 통해서 data로 값 전달
- App.js에서 DiaryList에 매개변수로 data를 전달해주었으므로 그 data를 랜더링함.
이렇게 진행된다고 봤을 때, 우선 클릭을 해서 정보를 가지고 있는 컴포넌트는 DiaryEditor 이다. 그럼 여기서 정보를 App.js로 넘겨주어서 데이터를 화면에 출력을 해야된다.
그러기 위해서는 App.js에서 데이터가 올라왔다고 가정을 하면!
값이 변경되었으므로 useState를 사용해야하고 여기에 있는 data는 일기 리스트르를 넣어놓는 곳이므로 const [data, setData] = useState([]);
로 초기화를 시켜준다.
그리고 해당 데이터는 DiaryList에서 작성이 되어야하므로 <DiaryList diaryList={data}></DiaryList>
로 넘겨주고, setData는 DiaryEdtior에서 진행이 되므로 <DiaryEditor onCreate={onCreate}></DiaryEditor>
를 통해서 데이터를 가져온다.
걍 내 느낌이지만 App.js가 컨트롤을 하고 여기서 컴포넌트들이 움직이는 느낌?
그래서 onCreate 메서드를 통해서 DiaryEditor의 값을 가져왔을 때, App.js 부분에서 추가된 데이터까지 합해서 DiaryList로 전달을 해야된다. data 리스트를 만드는 과정은 기존에 있던 데이터 …data에 새로운 일기의 값들만 객체로 만들어서(newItem) 추가를 해주면 된다.
💎 추가된 데이터 삭제하기
❓ 추가된 함수 또는 값을 전달하는 법?
import DiaryItem from "./DiaryItem";
const DiaryList = ({ diaryList, onDelete }) => {
console.log(diaryList);
return (
<div className="DiaryList">
<h2>일기 리스트</h2>
<h4>{diaryList.length}개의 일기가 있습니다. </h4>
<div>
{diaryList.map((it) => (
<DiaryItem key={it.id} {...it} onDelete={onDelete}></DiaryItem>
))}
</div>
</div>
);
};
DiaryList.defaultProps = {
diaryList: [],
};
export default DiaryList;
여기서 onDelete 라는 함수가 기존 코드에서 추가된 부분인데, 그냥 중괄호 안에 콤마 찍고 추가하기만 하면 된다.
💎 데이터 리스트 수정하기 🌟어렵
isEdit이 false이면 수정하지 않는 상태이고 isEditdl true이면 수정하고 있는 상태이다.
그래서 수정하기 버튼을 클릭하면 toggleIsEdit 함수를 통해서 isEdit의 값이 토글이 된다. 그래서 isEdit이 true라면 변경된 값을 content로 하고, false라면 기존의 content를 랜더한다.
근데 여기서 수정하기를 클릭하면 버튼을 수정 취소, 수정 완료로 바꾸고 싶으니까 버튼 부분의 코드 역시 삼항 연산자를 사용하여서 바꾸어 준다.
- 수정 취소 버튼: 수정을 하다가 수정 취소버튼을 누르면 원래의 content 부분으로 돌아가야되므로 isEdit 부분을 false로 바꾸어주면 된다. setLocalcontent는 수정 상태의 textarea를 말하며 다시 수정하기 버튼을 누르면 작성되어있는 content를 똑같이 띄우기 위해서 필요하다.
- 수정 완료 버튼🌟: 이벤트가 발생하면 새로운 데이터는 App 컴포넌트를 통해서 다시 하위 컴포넌트로 내려와야되므로 App 컴포넌트에서 수정된 내용을 변경시켜준다. 타깃 아이디를 기준으로!
//src > DiaryItem.js
import { useState, useRef } from "react";
const DiaryItem = ({
onRemove,
onEdit,
author,
content,
created_date,
emotion,
id,
}) => {
/* 데이터 리스트에서 데이터 삭제하기 */
const handleRemove = () => {
if (window.confirm(`${id}번 째 일기를 삭제하시겠습니까?`)) onRemove(id);
};
/* 데이터 리스트에서 데이터 수정하기 */
const [isEdit, setIsEdit] = useState(false);
const toggleIsEdit = () => setIsEdit(!isEdit);
const [localContent, setLocalContent] = useState(content);
/* 수정 취소 버튼 */
const handleQuitEdit = () => {
setIsEdit(false);
setLocalContent(content);
};
/* 수정 완료 버튼 */
const localContentInput = useRef(); //이거 왜 필요?
const handleEdit = () => {
if (localContent.length < 5) {
localContentInput.current.focus();
return;
}
if (window.confirm(`${id}번째 일기를 수정하시겠습니까?`));
onEdit(id, localContent);
toggleIsEdit();
};
return (
<div className="DiaryItem">
<div className="info">
<span>
작성자 : {author} | 감정점수 : {emotion} |
</span>
<br />
<span className="date">{new Date(created_date).toLocaleString()}</span>
<div className="content">
{isEdit ? (
<>
<textarea
value={localContent}
ref={localContentInput}
onChange={(e) => {
setLocalContent(e.target.value);
}}
></textarea>
</>
) : (
<>{content}</>
)}
</div>
</div>
{isEdit ? (
<>
<button onClick={handleQuitEdit}>수정 취소</button>
<button onClick={handleEdit}>수정 완료</button>
</>
) : (
<>
<button onClick={handleRemove}>삭제하기</button>
<button onClick={toggleIsEdit}>수정하기</button>
</>
)}
</div>
);
};
export default DiaryItem;
//src > App.js
import "./App.css";
import { useState, useRef } from "react";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
function App() {
const [data, setData] = useState([]);
const dataId = useRef(0);
/* 새로운 리스트 추가 */
const onCreate = (author, content, emotion) => {
console.log("App.js data", author, content, emotion);
const created_data = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_data,
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">
<h1>일기장</h1>
<DiaryEditor onCreate={onCreate}></DiaryEditor>
<DiaryList
diaryList={data}
onRemove={onRemove}
onEdit={onEdit}
></DiaryList>
</div>
);
}
export default App;
계층구조를 토대로 App 컴포넌트에서 만들어진 onEdit 함수는 DiaryList를 거쳐서 DiaryItem으로 와야되므로 DiaryList에도 onEdit 프롭을 전달해준다.
💎 Develop
🔆 객체를 반환하는 방법
/* emotion 점수 카운팅 */
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 };
};
그냥 바로 객체 형태로 리턴을 하면 된다.
🔆 Memoization이 필요한 이유
일기를 수정하면 리랜더링이 되는데, 이때는 감정 점수와는 무관하므로 불필요하게 리랜더링되고 있다. sol -> 최적화 1. useMemo() 사용 -> HOW? : Memoization 하고 싶은 함수를 감싸주면 된다.
/* emotion 점수 카운팅 */
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]);
이때 useEffect() 처럼 어떤 경우에 getDiaryAnalysis를 수행할 것인지 작성해주어야 한다. 이때 감정 점수가 추가되는 시점은 일기가 추가되는 시점이므로 data.length가 변화할 때 작동하도록 해주면 된다.
❓useMemo의 리턴값?
useMemo를 사용하게 되면 getDiaryAnalysis 는 더이상 함수가 아니므로 리턴값을 사용하기 위해서는 함수가 아닌 변수처럼 사용해주어야 한다.
근데 그럼 useMemo는 정확히 어떤 값을 리턴하는데??
그래서 출력을 해보면 [object Object]
가 나오는데 이건 뭐임,,?
useMemo의 구현부가 리턴하는 값 을 리턴한다
❓useMemo외 React.memo의 차이점?
둘다 리랜더를 막는 API아닌가??
맞는데, useMemo의 경우는 특정 경우에만 랜더링이 되도록 함으로써 필요없는 경우 useMemo의 구현부의 랜더링을 막아준다. (useMemo 구현부 외에는 모두 랜더링) 하지만 React.memo의 경우는 값이 변하는 컴포넌트만 랜더링되도록 해준다.
즉, useMemo의 경우는 하나의 컴포넌트보다는 하나의 함수에 대한 불필요한 랜더링을 막는 기능을 하고, React.memo의 경우는 하나의 컴포넌트에 대한 불필요한 랜더링을 막는 기능을 한다.
❓useCallback에서 빈배열을 deps로 준다면?
기본적으로 useCallback의 작동 원리: useMemo와 비슷한 Hook이다.
- useMemo는 특정 결과값을 재사용 할 때 사용한다.
- useCallback은 특정 함수를 새로 만들지 않고 재사용할 때 사용한다.
useCallback은 deps가 변하지 않으면 구현부에 있는 함수를 재사용하는 Hook이다.
// App.js
const onCreate = useCallback((author, content, emotion) => {
const created_date = new Date().getTime();
const newItem = {
author,
content,
created_date,
emotion,
id: dataId.current,
};
dataId.current += 1;
setData([newItem, ...data]);
}, []);
따라서 이렇게 된다면 diaryEditor는 불필요한 랜더를 안할 수 있다. 하지만, 일기를 추가하게 된다면 기존의 일기는 없어지고 방금 추가한 일기만 나타나게 되는데 왜 그럴까? => 빈 배열이라는 것은 처음에 Mount 될 때만 적용이 된다. 그러면 onCreate 함수 구현부만 계속 재사용이 될 것이고, 이때 …data는 [] 빈 배열이기 때문에 발생한 일이다.
💡 sol. setData로 전달해주는 값을 함수로 만들어준다.
/* 새로운 리스트 추가 */
const onCreate = useCallback((author, content, emotion) => {
const created_data = new Date().getTime();
const newItem = {
author,
content,
emotion,
created_data,
id: dataId.current,
};
dataId.current += 1;
setData((data) => [newItem, ...data]);
}, []);
data를 전달받아서 다시 newItem과 data를 data로 전달해주는 형식으로 만들어주면 된다.
댓글남기기