💡 이번에 배운 내용
- Section2.
서버와 통신이 가능한 구조적인 Web App을 만들 수 있다.- Unit9. React 클라이언트 Ajax 요청: React 데이터 흐름에 대해 복습하고, Effect Hook과 Ajax를 사용해 서버로부터 데이터를 받아오는 방법에 대해서 학습한다.
내용 정리에 상당한 시간이 걸렸지만 그만큼 정리하면서 공부가 되어 좋은 기회였다. 이번 내용도 실습해보니 상당히 쉽지 않다. 역시 네트워크가 관련된 실습은 (특히 비동기) 쉽지않다.
괜히 비동기 데이터를 받아올 때 함수 내에서 변수에 담아가며 동기적으로 작업하여 값을 리턴했다.
하지만 간단하게 바로 fetch와 then을 연결시킨 값을 리턴하니까 동기적으로 쓸 필요도 없었다...
비동기 작업시 리턴 시점도 참 중요하구나... 잊지말자🥲
역방향 데이터 흐름, state 끌어올리기, 부수 효과(side Effect), 순수 함수(pure Function), Effect Hook, React에서의 AJAX 요청, loading indicator, loading placeholder
이번 학습내용은 지난 번 학습했던 내용을 복습했고, 거기에 개념을 추가하는 방식이었다.
지난번 학습내용은 아래와 같다.
위의 내용을 종합해보면 하향식 데이터 흐름, 단방향 데이터 흐름이 중요하며 state, props를 통해 컴포넌트를 효과적으로 연결할 수 있다.
그런데 위의 내용과 연결하여 역방향 데이터 흐름도 존재한다.
function Parent() {
const [parentState, setParentState] = useState("기본값");
const handleChangeState = (newState) => {
setParentState(newValue);
};
return (
<div>
<div>현재 state값은 {parentState} 입니다</div>
<Child handleChangeState={handleChangeState} />
</div>
);
}
function Child({ handleChangeState }) {
const clickEventHandle = () => {
handleChangeState("자식이 보내는 값");
}
return (
<button onClick={clickEventHandle}>값 변경</button>
)
}
관련 내용: 🔗React 공식문서 | 5.state와 생명주기 - 데이터는 아래로 흐릅니다
위의 복습한 내용과 역방향 데이터 흐름에 대해 정리하자면 리액트 애플리케이션의 설계 방법은 다음과 같다.
자세한 내용은 공식문서에 나와있다.
🔗React 공식문서 | React로 사고하기
부수 효과란 함수 내부의 구현이 함수 외부에 영향을 주는 경우를 의미한다.
특히 리액트에서의 부수 효과는 다음과 같다.
순수 함수란 함수의 입력에 따라 같은 결과가 나오며 함수 내부에서만 영향을 주는 함수를 의미한다.
즉 반대로 입력 외의 값이 함수 결과에 영향을 미치거나 부수 효과를 발생시킬 때 순수 함수라고 할 수 없다.
정리하자면
순수 함수의 조건
예시)
공식문서에 따르면 Hook은 JavaScript 함수이나 사용할 때 다음과 같이 주의해야 한다.
Effect Hook은 컴포넌트 내에서 부수 효과를 실행할 수 있게 하는 Hook으로 useEffect가 있다.
사용법은 아래와 같다.
useEffect(함수, [조건을담은배열])
인자에 따라 useEffect의 첫번째 인자의 함수 실행 조건이 달라진다.
useEffect(함수)
useEffect(함수, [])
useEffect(함수, [요소1, 요소2])
검색 결과 등 검색하려는 데이터가 있는지 검색하기위해 데이터를 요청해서 접근해야 한다. 이 때
데이터를 한 번에 받아서 접근할지,
그때마다 데이터를 요청하여 접근할지
선택할 수 있다.
첫번째는 클라이언트에서, 두 번째는 매번 서버에 요청해야 한다.
두 방식은 아래와 같이 차이가 있다.
이를 예제로 통해 확인해볼 수 있다.
input태그에 어떤 값을 입력할 때마다 그 문자열이 들어간 도시명을 나타내는 예제가 있다.
이 예제를 살펴보면 처음 언급한 데이터 처리 방식이 어떤식으로 이뤄지는지 좀 더 쉽게 알 수 있다.
import { useEffect, useState } from "react";
//외부의 도시 데이터: stringify를 이용해 저장한다.
localStorage.setItem(
"city",
JSON.stringify([
"Seoul",
"Paris",
"New York",
"Milano",
"Tokyo"
])
);
//외부의 데이터 처리하기
function getCity(searchingValue = "") {
const jsonData = localStorage.getItem("city");
const cities = JSON.parse(jsonData) || [];
return cities.filter((el) => el.toLowerCase().includes(searchingValue.toLowerCase())
);
}
export default function App() {
const [cities, setCities] = useState([]);
const [searching, setSearching] = useState("");
const [count, setCount] = useState(0);
//1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
useEffect(() => {
const result = getCity(searching);
setCities(result);
});
const handleChange = (e) => {
setSearching(e.target.value);
};
return (
<div className="App">
도시명을 검색하세요
<input type="text" value={searching} onChange={handleChange} />
<ul>
{/* 2. 데이터를 불러오는 방식에 따라 이 부분이 달라진다 */}
</ul>
</div>
);
}
function City(props) {
return <li>{props.city}</li>;
}
이 예제는 위 데이터 처리 방식에 따라 1번 부분, 2번 부분을 다르게 구현해볼 수 있다.
컴포넌트 내부에서 처리하기
1번 부분
//1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
useEffect(() => {
const result = getCity();
setCities(result);
}, []); //클라이언트 내부에 모든 데이터를 처음 한 번만 받아와 실행한다.
2번 부분
//내부에서 데이터를 받아 직접 필터링 처리한다.
<ul>
{cities
.filter((el) => {
return el.toLowerCase().includes(searching.toLowerCase());
})
.map((el, i) => (
<City city={el} key={i} />
))}
</ul>
컴포넌트 외부에서 처리하기
1번 부분
//1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
useEffect(() => {
const result = getCity(searching);
setCities(result);
}, [searching]); //searching값이 변할때마다 함수가 실행된다.
2번 부분
//값이 변할 때마다 필터링되어 저장되므로 렌더링만 한다.
<ul>
{cities.map((el, i) => (
<City city={el} key={i} />
))}
</ul>
위의 예제를 fetch API를 써서, 임의의 서버(http://서버주소/cities
)에 요청한다고 가정하자.
그리고 예제를 바꿔보면 아래와 같다.
useEffect(() => {
fetch(`http://서버주소/cities?q=${searching}`)
.then(resp => resp.json())
.then(result => {
setCities(result);
});
}, [searching]);
보통 AJAX 요청이 느릴 경우를 대비해 로딩중이라는 의미의 화면을 구현한다.
이 로딩 화면에는 크게 두가지가 있다.
이 로딩 인디케이터를 불러올 때
state를 사용해 미리 구현해둔 Loading indicator 컴포넌트를 불러올 수 있다.
const [isLoading, setIsLoading] = useState(true);
//LoadingIndicator: 로딩 인디케이터 컴포넌트
//Content: 로딩 완료 후 나타날 화면 컴포넌트
export default function App(){
return {isLoading ? <LoadingIndicator /> : <Content />}
}
그리고 fetch API를 활용해 로딩 인디케이터를 구현할 수 있다.
useEffect(() => {
setIsLoading(true); //
useEffect(() => {
fetch(`http://서버주소/cities?q=${searching}`)
.then(resp => resp.json())
.then(result => {
setCities(result);
setIsLoading(false); //
});
}, [searching]);
1. props와 구조분해 할당
컴포넌트에서 부모로부터 전달받은 props를 사용할 때 구조분해할당을 사용할 수 있다.
본문의 도시 검색 예제를 보면 아래와 같이 props를 사용한 것을
function City(props) {
return <li>{props.city}</li>;
}
아래처럼 구조분해할당을 사용해 표현할 수 있다.
function City({city}) {
return <li>{city}</li>;
}
구조분해할당이므로 원래 prop가 있던 자리의 중괄호는 props가 객체이기에 사용한 중괄호이다. 때문에 중괄호 안의 키 이름은 props에 부모가 지정한 속성이름으로 저장된 키 이름이여야 한다.