블로깅 주제
- [HTTP/네트워크] 실습
1. 지금 현재, 당신의 기분이나 느낌을 표현해 주세요.
- 올해의 마지막 공휴일이 지났다 ㅠㅠ 왜 11월, 12월은 공휴일이 없는 것인가!!!!!!안돼!!! ㅠㅠㅠ 여하튼 이제 섹션2도 2/3 정도가 끝나간다.. 벌써 이렇게 시간이 흘렀다는게 믿기지가 않는다. 최선을 다해보자.
2. 오늘 무엇을 학습한 내용 중 지금 떠올릴 수 있는 단어를 모두 나열해 주세요.
- React 데이터 흐름, state 끌어올리기, Effect Hook
3. 2에서 작성한 단어를 가지고, 오늘의 학습 내용을 설명해 보세요.
- React 데이터 흐름
▶ React 데이터 흐름
React의 개발 방식의 가장 큰 특징은 페이지 단위가 아닌, 컴포넌트 단위로 시작한다는 점이 가장 큰 특징이다.
먼저 컴포넌트를 만들고, 다시 페이지를 조립해나간다. 즉, 상향식(bottom-up)으로 앱을 만든다.
이것의 가장 큰 장점은 테스트가 쉽고 확장성이 좋다는 것이다.
컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 전달인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있다.
즉 데이터를 전달하는 주체는 부모 컴포넌트가 된다. 이는 데이터 흐름이 하향식(top-down)임을 의미한다.
전체 트윗 목록은, Tweets에서 필요로 하는 데이터이다.
그런데, 새 글을 추가하는 이벤트가 발생할 경우, 이때 전체 트윗 목록에 새로운 트윗 객체를 추가할 수 있어야 한다. 즉, 두 컴포넌트 모두 트윗 목록에 의존한다.
두 컴포넌트의 부모는 Twittler이므로 전체 트윗 목록 상태는 여기에 위치한다.
NewTweetForm은 그저 버튼이 눌린 후 완성된 하나의 트윗 객체를 전체 트윗 목록에 전달하기만 하면 된다. 입력에 따라 실시간으로 다른 컴포넌트가 변한다면 모를까, 여기에서는 그렇지 않으므로 다른 컴포넌트와 공유할 필요가 없다. "작성 중인 트윗 내용"이라는 상태는 NewTweetForm에 두는 것으로 충분하다.
상태 위치를 전부 정하고 나서 생각해보니, 부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 것을 발견할 수 있다. 바로 새로운 트윗 추가가 대표적인 예시이다. 버튼을 통한 이 액션은, 부모의 상태를 변화시켜야 한다.
이를 해결할 수 있는 키워드는 바로 "State 끌어올리기(Lifting state up)"이다.
▶ 상태 끌어올리기
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다.
function ParentComponent() {
const [value, setValue] = useState("날 바꿔줘!");
const handleChangeValue = (newValue) => {
setValue(newValue);
};
return (
<div>
<div>값은 {value} 입니다</div>
<ChildComponent handleButtonClick={handleChangeValue} />
</div>
);
}
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
handleButtonClick('넘겨줄게 자식이 원하는 값')
}
return (
<button onClick={handleClick}>값 변경</button>
)
}
상태를 변경하는 함수는 handleChangeValue 이다. 전달은 props를 이용하자. 하위 컴포넌트가 버튼 클릭 이벤트에 따라 상태를 변경하려고 하므로 이름은 handleButtonClick이라고 지어주자.
<ChildComponent> 는 마치 고차 함수가 인자로 받은 함수를 실행하듯, props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다. "상태 변경 함수"는 버튼이 클릭할 때 실행되기를 원하므로, 해당 부분에 콜백 함수를 실행하자.
필요에 따라 설정할 값을 콜백 함수의 인자로 넘길 수도 있다.
Action Item: 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]);
}; // 이 상태 변경 함수가 NewTweetForm에 의해 실행되어야 합니다.
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
};
// 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;
참고 문서)
https://ko.reactjs.org/docs/thinking-in-react.html
과제: StateAirline Client part1
Search.js
import { useState } from 'react';
function Search({onSearch}) {
const [textDestination, setTextDestination] = useState('');
const handleChange = (e) => {
setTextDestination(e.target.value.toUpperCase());
};
const handleKeyPress = (e) => {
if (e.type === 'keypress' && e.code === 'Enter') {
handleSearchClick();
}
};
const handleSearchClick = () => {
console.log('검색 버튼을 누르거나, 엔터를 치면 search 함수가 실행됩니다');
// TODO: 지시에 따라 상위 컴포넌트에서 props를 받아서 실행시켜 보세요.
onSearch({departure: "ICN", destination: textDestination});
};
return (
<fieldset>
<legend>공항 코드를 입력하고, 검색하세요</legend>
<span>출발지</span>
<input id="input-departure" type="text" disabled value="ICN"></input>
<span>도착지</span>
<input
id="input-destination"
type="text"
value={textDestination}
onChange={handleChange}
placeholder="CJU, BKK, PUS 중 하나를 입력하세요"
onKeyPress={handleKeyPress}
/>
<button id="search-btn" onClick={handleSearchClick}>
검색
</button>
</fieldset>
);
}
export default Search;
Main.js
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { getFlight } from '../api/FlightDataApi';
import FlightList from './component/FlightList';
import LoadingIndicator from './component/LoadingIndicator';
import Search from './component/Search';
import Debug from './component/Debug';
// 후반 테스트를 진행할 때 아래 import를 삭제합니다.
import json from '../resource/flightList';
export default function Main() {
// 항공편 검색 조건을 담고 있는 상태
const [condition, setCondition] = useState({
departure: 'ICN',
});
const [flightList, setFlightList] = useState(json);
// 주어진 검색 키워드에 따라 condition 상태를 변경시켜주는 함수
const search = ({ departure, destination }) => { //상태 변경함수가 객체로 값을 받음
if (
condition.departure !== departure ||
condition.destination !== destination
) {
console.log('condition 상태를 변경시킵니다');
console.log({ departure, destination });
// TODO: search 함수가 전달 받아온 '항공편 검색 조건' 인자를 condition 상태에 적절하게 담아보세요.
setCondition({ departure, destination });
}
};
const filterByCondition = (flight) => {
let pass = true;
if (condition.departure) {
pass = pass && flight.departure === condition.departure;
}
if (condition.destination) {
pass = pass && flight.destination === condition.destination;
}
return pass;
};
global.search = search; // 실행에는 전혀 지장이 없지만, 테스트를 위해 필요한 코드입니다. 이 코드는 지우지 마세요!
// TODO: Effeck Hook을 이용해 AJAX 요청을 보내보세요.
// TODO: 더불어, 네트워크 요청이 진행됨을 보여주는 로딩 컴포넌트(<LoadingIndicator/>)를 제공해보세요.
// useEffect(() => {
// }, [])
// TODO: 테스트 케이스의 지시에 따라 search 함수를 Search 컴포넌트로 내려주세요.
return (
<div>
<Head>
<title>States Airline</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>여행가고 싶을 땐, States Airline</h1>
<Search onSearch = {search}/>
<div className="table">
<div className="row-header">
<div className="col">출발</div>
<div className="col">도착</div>
<div className="col">출발 시각</div>
<div className="col">도착 시각</div>
<div className="col"></div>
</div>
<FlightList list={flightList.filter(filterByCondition)} />
</div>
<div className="debug-area">
<Debug condition={condition} />
</div>
<img id="logo" alt="logo" src="codestates-logo.png" />
</main>
</div>
);
}
- Effect Hook
▶ Side effect(부수 효과)
1. Side effect(부수 효과)
함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기한다. React에서는 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오거나 이벤트를 활용해 DOM 직접 조작할 때 Side Effect가 발생했다고 말한다.
let foo = 'hello';
function bar() {
foo = 'world';
}
bar(); // bar는 Side Effect를 발생시킵니다!
2. pure function(순수 함수)
순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 의미한다. 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없다. 또한 순수 함수는, 입력으로 전달된 값을 수정하지 않는다.
function upper(str) {
return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않습니다 (Immutable)
}
upper('hello') // 'HELLO'
순수 함수에는 네트워크 요청과 같은 Side Effect가 없다. 순수 함수의 특징 중 하나는, 어떠한 전달 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장한다. 그래서 예측 가능한 함수이기도 하다.
- 순수함수 조건
- 동일한 인자가 들어갈 경우 항상 같은 값이 나와야 한다.
- 부수적인 효과가 일어나면 안 된다.
- return 값으로만 소통한다.
질문
- Math.random()은 순수 함수가 아닙니다. 왜일까요?
==> Math.random은 어떤 결과값이 나올지 예측이 불가능하기 때문에 순수 함수가 아니다. - 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다고 가정해 봅시다. 이 함수는 순수 함수가 아닙니다. 왜일까요?
==> 네트워크 상황, 서버상태에 따라 응답코드가 달라지기 때문에 예측이 불가능하다.
3. React의 함수 컴포넌트
우리가 앞서 배운 React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력으로 나간다. 여기에는 그 어떤 Side Effect도 없으며, 순수 함수로 작동한다.
function SingleTweet({ writer, body, createdAt }) {
return <div>
<div>{writer}</div>
<div>{createdAt}</div>
<div>{body}</div>
</div>
}
하지만 보통 React 애플리케이션을 작성할 때에는, AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다. 이는 React의 입장에서는 전부 Side Effect 이다. React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.
React 컴포넌트에서의 Side Effect
- 타이머 사용 (setTimeout)
- 데이터 가져오기 (fetch API, localStorage)
★ Hook 의 기능
Hook은 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해주는 기능이다.
useState와 useEffect를 사용하여 특징적으로 state와 lifecycle과 같은 기능을 사용 가능하게 해준다.
- State Hook - useState
- Effect Hook - useEffect
▶ Effect Hook 기본
//API
useEffect(함수)
useEffect의 첫 번째 인자는 함수이다. 해당 함수 내에서 side effect를 실행하면 된다.
언제 실행되나요?
- 컴포넌트 생성 후 처음 화면에 렌더링(표시)
- 컴포넌트에 새로운 props가 전달되며 렌더링
- 컴포넌트에 상태(state)가 바뀌며 렌더링
이와 같이 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.
Hook을 쓸 때 주의할 점
- 최상위에서만 Hook을 호출
- React 함수 내에서 Hook을 호출
예시) useEffect 사용하여 Title 바꾸기
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>
);
}
▶ Effect Hook 조건부 실행
//API
useEffect(함수, [종속성1, 종속성2, ...])
useEffect의 두 번째 인자는 종속성 배열이다. 각 종속성은 표현식이 아닌 어떤 값으로 할당한다. 배열 내의 종속성1, 또는 종속성2의 값이 변할 때, 첫 번째 인자의 함수가 실행된다.
배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행된다.
예시) 외부 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,count]);
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>;
}
단 한 번만 실행되는 Effect 함수
// 컴포넌트가 처음 생성되거나, props가 업데이트 되거나, state가 업데이트 될 때마다 실행됩니다.
useEffect(() => {
console.log(몇 번 호출될까요?)
})
//컴포넌트가 처음 생성될 때만 함수가 실행됩니다.
//ex) 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할 수 있습니다.
useEffect(() => {
console.log(몇 번 호출될까요?)
},[])
//dep이 업데이트 될 때마다 실행됩니다.
useEffect(() => {
console.log(몇 번 호출될까요?)
},[dep])
▶ 컴포넌트 내에서의 Ajax 요청
목록 내 필터링을 구현하기 위해서는 다음과 같은 두 가지 접근이 있을 수 있다.
1. 컴포넌트 내에서 필터링: 전체 목록 데이터를 불러오고, 목록을 검색어로 filter 하는 방법
https://codesandbox.io/s/filter-by-client-vyzdc?from-embed=&file=/src/App.js
2. 컴포넌트 외부에서 필터링: 컴포넌트 외부로 API 요청을 할 때, 필터링 한 결과를 받아오는 방법 (보통, 서버에 매번 검색어와 함께 요청하는 경우가 이에 해당)
https://codesandbox.io/s/useeffect-2-oute9?from-embed
두 방식의 차이점
지금은, 우리가 storageUtil.js를 이용해 외부 API를 직접 구현했지만(LocalStorage API를 이용했습니다), 이는 서버 요청으로 대체할 수 있다. 만일 서버에서 수십만 개의 명언을 제공한다고 가정해 보자.
다음의 표는 HTTP를 이용한 서버 요청을 가정할 때, 두 방식의 차이점을 설명하고 있다.
장점 | 단점 | |
컴포넌트 내부에서 처리 | HTTP 요청의 빈도를 줄일 수 있다 | 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다 |
컴포넌트 외부에서 처리 | 클라이언트가 필터링 구현을 생각하지 않아도 된다 | 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다 |
Ajax 요청을 보내자!
임의로 구현한 storageUtil.js 대신, fetch API를 써서, 서버에 요청한다면?
명언을 제공하는 API의 엔드포인트가 http://서버주소/proverbs 라고 가정해 보자.
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
});
}, [filter]);
AJAX 요청이 매우 느릴 경우?
모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다. 외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현은 필수적이다.
기본적으로, Loading indicator의 구현에도 상태 처리가 필요하다.
const [isLoading, setIsLoading] = useState(true);
// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}
fetch 요청의 전후로 setIsLoading을 설정해 주어 보다 나은 UX를 구현할 수 있다.
useEffect(() => {
setIsLoading(true);
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
setIsLoading(false);
});
}, [filter]);
'코드스테이츠 SEB FE 41기 > Section 별 내용 정리' 카테고리의 다른 글
section2/Unit10/[Web Server] 기초(10/13) (0) | 2022.10.13 |
---|---|
section2/unit9/[React] 클라이언트 Ajax 요청(10/12) (0) | 2022.10.12 |
section2/unit8/[HTTP/네트워크] 실습(10/7) (0) | 2022.10.07 |
section2/unit8/[HTTP/네트워크] 실습(10/6) (2) | 2022.10.06 |
section2/unit7/[HTTP/네트워크] 기초(10/5) (1) | 2022.10.05 |