React 데이터 흐름
React 개발 방식의 가장 큰 특징은 페이지 단위가 아닌, 컴포넌트 단위로 시작한다는 점이다.
따라서 먼저 컴포넌트를 만들고, 다시 페이지를 만드는 상향식(bottom-up) 방법을 사용한다.
이것의 가장 큰 장점은 테스트가 쉽고 확장성이 좋다는 점이다.
이러한 컴포넌트를 활용한 설계 방식은 이전 시간에 배웠다.
이제는 데이터를 어디에 둘지 생각해야 한다.
컴포넌트는 바깥에서 props를 이용해서 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달 받을 수 있다.
데이터를 전달하는 주체는 부모 컴포넌트가 된다.
이는 데이터 흐름이 하향식(top-down)임을 뜻한다.
이 원칙은 단방향 데이터 흐름(one-way data flow)라는 키워드로 통한다.
그리고 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못한다.
이벤트에 따라 변하는 데이터를 state로 둔다는 사실을 이미 알 것이다.
하지만 이 state를 최소화하기 위해서 다음 3가지 질문을 통해 판단해야 한다.
- 부모로부터 props가 전달되는가?
- 시간이 지나도 변하지 않는가?
- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가?
위 조건에 해당한다면 state가 아니다.
State 끌어올리기 (Lifting state up)
하위 컴포넌트에서의 클릭 이벤트가 부모의 상태를 바꾸어야만 하는 상황이 있다.
이를 해결할 수 있는 키워드가 바로 "State 끌어올리기"이다.
state를 끌어올리면 상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있다.
이는 마치 콜백 함수를 사용하는 방법과 비슷하다.
아래 코드는 State를 끌어올리는 간단한 예시 코드이다.
import React, { useState } from "react";
export default function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = () => {
setValue("보여줄게 완전히 달라진 값");
};
return (
<div>
<div>값은 {value} 입니다</div>
<ChildComponent handleButtonClick={handleChangeValue} />
</div>
);
}
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
handleButtonClick();
};
return <button onClick={handleClick}>값 변경</button>;
}
위 예제에서 자식 컴포넌트는 마치 고차함수가 인자로 받은 함수를 실행하듯, props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다.
다음은 이전에도 살펴봤던 Twittler에서 새로운 트윗을 추가하는 예제이다.
import React, { useState } from "react";
import "./styles.css";
const currentUser = "김코딩";
function Twittler() {
const [tweets, setTweets] = useState([
{
uuid: 1,
writer: "김코딩",
date: "2020-10-10",
content: "안녕 리액트",
},
{
uuid: 2,
writer: "박해커",
date: "2020-10-12",
content: "좋아 코드스테이츠!",
},
]);
const addNewTweet = (newTweet) => {
setTweets([...tweets, newTweet]);
};
return (
<div>
<div>작성자: {currentUser}</div>
<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 }) {
const [newTweetContent, setNewTweetContent] = useState("");
const onTextChange = (e) => {
setNewTweetContent(e.target.value);
};
const onClickSubmit = () => {
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent,
};
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;
Effect Hook
Side Effect and Pure Function
함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우, 해당 함수는 Side Effect가 있다고 이야기한다.
반면에 오직 함수의 입력만이 함수의 결과에 영향을 주고, 입력에 전달된 값을 수정하지 않는 함수를 Pure Function이라고 이야기한다.
순수 함수에는 네트워크 요청과 같은 Side Effect가 없다.
그리고 순수 함수는 예측 가능한 함수이다.
위 특징들을 미루어 보았을 때, Math.random() 함수는 예측 가능한 함수가 아니기 때문에 순수 함수가 아니다.
fetch API 같은 경우에도, 네트워크 요청은 서버 데이터에 Side Effect를 일으킬 수 있고, 클라이언트나 서버 처리 과정 중에 에러가 발생할 수도 있으므로 역시 예측 가능한 함수가 아니다.
React의 함수 컴포넌트
앞서 살펴보았던 React의 함수 컴포넌트는 props가 입력으로, JSX Element가 출력으로 나간다.
이는 순수 함수로 작동한다.
하지만 React 애플리케이션을 작성할 때, AJAX 요청이 필요하거나 LocalStorage 또는 타이머와 같은 React와 상관 없는 API를 사용하는 경우가 발생할 수 있다.
이는 React의 입장에서는 전부 Side Effect이다.
그렇기 때문에 React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.
다음은 useEffect를 활용한 예시 코드이다.
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const proverbs = [
"좌절감으로 배움을 늦추지 마라",
"Stay hungry, Stay foolish",
"Memento Mori",
"Carpe diem",
"배움에는 끝이 없다",
];
const [idx, setIdx] = useState(0);
const handleClick = () => {
setIdx(idx === proverbs.length - 1 ? 0 : idx + 1);
};
return (
<div className="App">
<button onClick={handleClick}>명언 제조</button>
<Proverb saying={proverbs[idx]} />
</div>
);
}
function Proverb({ saying }) {
useEffect(() => {
document.title = saying;
});
return (
<div>
<h3>오늘의 명언</h3>
<div>{saying}</div>
</div>
);
}
useEffect의 첫번째 인자는 함수이다.
해당 함수 내에서 Side Effect를 실행하면 된다.
useEffect의 실행 시점
- 컴포넌트 생성 후 처음 화면에 렌더링
- 컴포넌트에 새로운 props가 전달되며 렌더링
- 컴포넌트에 state가 바뀌며 렌더링
이와 같이 컴포넌트가 새롭게 렌더링될 때 Effect Hook이 실행된다.
Hook을 사용할 때 주의할 점
- 최상위에서만 Hook을 호출한다. (반복문 / 조건문 / 중첩된 함수 X)
- React 함수 내에서 Hook을 호출한다. (JavaScript 함수에서 호출 X)
조건부 effect 발생 (dependency array)
useEffect의 두번째 인자는 배열이다.
이 배열은 조건을 담고 있다.
여기서의 조건은 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미한다.
따라서 해당 배열엔 바로 그 어떤 값의 목록이 들어간다.
이 배열을 종속성 배열이라고 부른다.
다음은 이전에 살펴보았던 명언 제조기 앱에서 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("");
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>;
}
보다 완전한 이해를 위해서 storageUtil.js 파일도 복사해보았다.
localStorage.setItem(
"proverbs",
JSON.stringify([
"좌절감으로 배움을 늦추지 마라",
"Stay hungry, Stay foolish",
"Memento Mori",
"Carpe diem",
"배움에는 끝이 없다",
])
);
export function getProverbs(filterBy = "") {
const json = localStorage.getItem("proverbs");
const proverbs = JSON.parse(json) || [];
return proverbs.filter((prvb) =>
prvb.toLowerCase().includes(filterBy.toLowerCase())
);
}
단 한번만 실행되는 Effect 함수
두번째 인자에 빈 배열을 넣는 경우에는 두번째 인자를 아예 넘기지 않는 것과 어떻게 다를까?
앞서, 두번째 인자를 생략할 경우, 컴포넌트가 렌더링될 때 effect 함수가 실행된다는 점을 배웠다.
하지만 두번째 인자에 빈 배열을 넣었을 경우에는 컴포넌트가 처음 생성될 때만 effect 함수가 실행된다.
이러한 로직은 대표적인 예시로, 처음 단 한번, 외부 API를 통해 리소스를 받아오고 더이상 API 호출이 필요하지 않을 때 사용할 수 있다.
Data Fetching
목록 내 필터링을 구현하기 위해서 다음과 같은 두가지 접근이 있을 수 있다.
- 컴포넌트 내에서 필터링 : 전체 목록 데이터를 불러오고, 목록을 검색어로 filter
- 컴포넌트 밖에서 필터링 : 컴포넌트 외부로 API 요청을 할 때, 필터링한 결과를 받아오는 방법 (검색)
컴포넌트 내에서 필터링
처음 단 한번, 외부 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>;
}
컴포넌트 밖에서 필터링하는 예시는 '조건부 Effect 발생'에서 확인한 코드와 동일하다.
두 방식의 차이점
처리 방법 | 장점 | 단점 |
---|---|---|
컴포넌트 내부에서 처리 | HTTP 요청 빈도를 줄임 | 브라우저의 메모리에 많은 데이터를 갖게 되어서 클라이언트의 부담이 늘어남 |
컴포넌트 외부에서 처리 | 필터링 구현을 생각하지 않아도 됨 | 빈번한 HTTP 요청으로 서버의 부담이 늘어남 |
AJAX 요청 보내기
LocalStorage API인 storageUtil.js 대신 fetch API를 사용해서 서버에 요청한다면 effect 함수는 다음과 같이 작성할 수 있을 것이다.
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then((resp) => resp.json())
.then((result) => {
setProverbs(result);
});
}, [filter]);
로딩 화면 (Loading indicator)
모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다.
외부 API 접속이 느릴 경우를 감안하여, 로딩 화면의 구현은 필수적이다.
로딩 화면의 구현에도 상태 처리가 필요하다.
이를 간단하게 표현하면 다음과 같다.
const [isLoading, setIsLoading] = useState(true);
// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}
useEffect(() => {
setIsLoading(true);
fetch(`http://서버주소/proverbs?q=${filter}`)
.then((resp) => resp.json())
.then((result) => {
setProverbs(result);
setIsLoading(false);
});
}, [filter]);
Debounce and Throttle
클라이언트가 서버에 요청을 덜 보내기 위해서 Debounce와 Throttle을 사용한다.
아래에 간단하게 실험해볼 수 있는 예제들과 깔끔하게 잘 정리된 설명이 있는 블로그 글이 있다.
Debounce와 Throttle, 그리고 차이점
'JavaScript > React' 카테고리의 다른 글
React의 이벤트 처리 (0) | 2021.08.08 |
---|---|
State & Props (0) | 2021.08.08 |
SPA & Router (0) | 2021.08.08 |
React 기본 개념 (0) | 2021.08.08 |