그동안 코드를 작성할때, 페이지 단위로 작업을 했다면
컴포넌트란?
하나의 특정한 기능을 수행하는 코드의 단위
하나의 컴포넌트는 하나의 기능만 수행한다.
즉 기능단위로 작업을 한다.
이렇게 상향식으로 앱을 만들면, 테스트가 쉽고 확장성이 좋다.
컴포넌트 단위로 나누었으면 그 다음에는 트리구조로 나타내본다.
그리고 데이터를 어디에 위치시킬지 결정해야 한다.
컴포넌트는 props를 이용해 데이터를 전달인자나 속성처럼 전달받을 수 있다.
즉 데이터를 전달하는 주체는 부모 컴포넌트가 된다.
리액트의 중요한 키워드 중 하나는 '단방향 데이터 흐름' 이다.
state는 최소화하는 것이 좋다.
state가 많을수록 복잡해지기 때문이다.
state가 아닌 경우
v 부모로부터 props를 통해 전달된다.
v 시간이 지나도 변하지 않는다.
v 컴포넌트 안의 다른 state나 props를 가지고 계산이 가능하다.
해당 state data가 필요한 컴포넌트가 둘 이상이라면 두 컴포넌트의 공통 소유 컴포넌트(공통의 부모)에 state를 위치시킨다.
역방향 데이터 흐름이 발생하는 경우가 있다.
새로운 글을 추가하는 경우, 버튼을 클릭하면 부모의 상태를 변화시켜야한다.
이때 사용하는 것이 state 끌어올리기(Lifting state up)이다.
상태를 변경시키는 함수를 하위 컴포넌트에 props로 전달해서 해결한다.
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다
function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = () => {
setValue("보여줄게 완전히 달라진 값");
};
return (
<div>
<div>값은 {value} 입니다</div>
<ChildComponent handleButtonClick={handleChangeValue} /> //handleChangeValue 실행
</div>
);
}
function ChildComponent({ handleButtonClick }) { //handleBtnClick 전달
const handleClick = () => {
// Q. 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
// A. 인자로 받은 상태 변경 함수를 실행하자!
handleButtonClick() //상태변경함수 실행
}
return (
<button onClick={handleClick}>값 변경</button>
)
}
function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = (newValue) => {
setValue(newValue);
};
// ...생략...
}
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
handleButtonClick('넘겨줄게 자식이 원하는 값')
}
return (
<button onClick={handleClick}>값 변경</button>
)
}
import React, { useState } from "react";
import "./styles.css";
const currentUser = "김코딩";
function Twittler() {
//1. Twittler라는 컴포넌트. tweets라는 데이터를 state로 가지고 있다.
const [tweets, setTweets] = useState([
//tweets라는 데이터에는 배열 형태로 두가지의 객체가 들어있다.
{
uuid: 1,
writer: "김코딩",
date: "2020-10-10",
content: "안녕 리액트"
},
{
uuid: 2,
writer: "박해커",
date: "2020-10-12",
content: "좋아 코드스테이츠!"
}
]);
const addNewTweet = (newTweet) => { //새로운 트윗데이터가 들어오면 tweets에 추가해준다.
setTweets([...tweets, newTweet]);
//tweets 상태를 변화시키는 함수로 기존 tweets를 펼쳐서 깊은 복사를 한 후 그 위에 newTweet을 넣어준다.
}; // 이 상태 변경 함수가 NewTweetForm에 의해 실행되어야 합니다.
return (
<div>
<div>작성자: {currentUser}</div>
<NewTweetForm onButtonClick={addNewTweet} />
//NewTweetForm이라는 두번째 컴포넌트에 onButtonClick이라는 함수를 만들어서 addNewTweet 함수를 전달한다.
<ul id="tweets">
{tweets.map((t) => (
<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
{t.content}
</SingleTweet>
))}
</ul>
</div>
);
}
function NewTweetForm({ onButtonClick }) {
//2. 두번째 컴포넌트. 새로운 트윗데이터를 생성한다.
const [newTweetContent, setNewTweetContent] = useState("");
const onTextChange = (e) => {
setNewTweetContent(e.target.value);
//textarea에 입력된 내용들을 변수에 할당한다.
};
const onClickSubmit = () => { //버튼을 누르면 실행되는 함수.
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent
}; //새 트윗 데이터를 객체형태로 담는다.
// TDOO: 여기서 newTweet이 addNewTweet에 전달되어야 합니다.
onButtonClick(newTweet);
//부모 컴포넌트로부터 받은 온버튼클릭 함수에 새로운 트윗객체를 보낸다.
};
return (
<div id="writing-area">
<textarea id="new-tweet-content" onChange={onTextChange}></textarea>
<button id="submit-new-tweet" onClick={onClickSubmit}>
새 글 쓰기
</button>
</div>
);
}
function SingleTweet({ writer, date, children }) {
return (
<li className="tweet">
<div className="writer">{writer}</div>
<div className="date">{date}</div>
<div>{children}</div>
</li>
);
}
export default Twittler;
이해가 잘 안돼서 하나하나 찬찬히 뜯어보았다.
이제 좀 이해가 되는 것 같다...
리액트에서는
1. 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오거나
2. 이벤트를 활용해 DOM을 조작할 때
Side Effect가 발생했다고 말한다.
함수의 입력만이 함수의 결과에 영향을 주는 함수
입력으로 전달된 값을 수정하지 않는다.
그렇기 때문에 side effect가 발생하지 않는다.
AJAX, LocalStorage, 타이머와 같이 리액트와 상관없는 API사용시 side effect가 발생한다.
컴포넌트 내에서 Side Effect를 실행할 수 있게 하는 Hook이다.
첫번째인자는 함수이다. 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.
- 컴포넌트 생성
- state 변경
- 새로운 props 전달
두번째 인자는 배열이다. 이 배열은 어떤 값의 목록이 들어간다. 이 배열을 특별히 종속성 배열이라고 부른다. 배열 내의 종속성1, 종속성2의 값이 변할 때 첫 번째 인자의 함수가 실행된다.
배열 내의 어떤 값이 변할 때에만 함수가 실행된다.
https://ko.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
최상위에서만 Hook을 호출해야 한다.
반복문, 조건문 혹은 중첩된 함수 내에서 훅을 호출하지 말아야 한다. 그래야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 훅이 호출된다.
React함수 내에서 Hook을 호출해야 한다.
훅을 일반적인 자바스크립트 함수에서 호출하면 안된다. 리액트 함수 컴포넌트에서 훅을 호출하거나, Custom Hook에서 호출해야 한다. 그래야 모든 상태 관련 로직을 소스코드에서 명확하게 보이도록 할 수 있다.
단 한번만 실행되는 Effect함수
- 빈 배열 넣기
useEffect(함수, [])
컴포넌트가 처음 생성될 때에만 effect함수가 실행된다.- 아무것도 넣지 않기
useEffect(함수)
컴포넌트가 처음 생성되거나, props가 업데이트되거나, state가 업데이트 될 때 함수가 실행된다.
장점 | 단점 | |
---|---|---|
컴포넌트 내부에서 처리 | HTTP 요청의 빈도를 줄일 수 있다 | 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다 |
컴포넌트 외부에서 처리 | 클라이언트가 필터링 구현을 생각하지 않아도 된다 | 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다 |
처음 단 한번 외부 API로부터 명언 목록을 받아오고 filter함수 이용
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";
export default function App() {
const [proverbs, setProverbs] = useState([]);
const [filter, setFilter] = useState("");
useEffect(() => {
console.log("언제 effect 함수가 불릴까요?");
const result = getProverbs();
setProverbs(result);
}, []);
const handleChange = (e) => {
setFilter(e.target.value);
};
return (
<div className="App">
필터
<input type="text" value={filter} onChange={handleChange} />
<ul>
{proverbs
.filter((prvb) => {
return prvb.toLowerCase().includes(filter.toLowerCase());
})
.map((prvb, i) => (
<Proverb saying={prvb} key={i} />
))}
</ul>
</div>
);
}
function Proverb({ saying }) {
return <li>{saying}</li>;
}
검색어가 바뀔때마다 외부 API 호출
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";
export default function App() {
const [proverbs, setProverbs] = useState([]);
const [filter, setFilter] = useState("");
const [count, setCount] = useState(0);
useEffect(() => {
console.log("언제 effect 함수가 불릴까요?");
const result = getProverbs(filter);
setProverbs(result);
}, [filter]);
const handleChange = (e) => {
setFilter(e.target.value);
};
const handleCounterClick = () => {
setCount(count + 1);
};
return (
<div className="App">
필터
<input type="text" value={filter} onChange={handleChange} />
<ul>
{proverbs.map((prvb, i) => (
<Proverb saying={prvb} key={i} />
))}
</ul>
<button onClick={handleCounterClick}>카운터 값: {count}</button>
</div>
);
}
function Proverb({ saying }) {
return <li>{saying}</li>;
}
//엔드포인트 : http://서버주소/proverbs 라고 가정
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
});
}, [filter]);
로딩화면을 구현하기 위해 상태처리가 필요하다.
const [isLoading, setIsLoading] = useState(true);
// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}
//fetch 요청의 전후로 setIsLoading을 설정한다.
useEffect(() => {
setIsLoading(true); //로딩
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
setIsLoading(false); //로딩
});
}, [filter]);