리액트 강의를 처음 들었는데 내가 잘 모르던 실전 패턴들을 기록하여 정리하면서 들었다. 그래서 사전 느낌으로 나중에 혹시 생각이 잘 안나더라도 찾아볼 수 있게 잘 정리하였고 각 패턴과 연관성이 적은 부분은 추상화하여 표현하였다. 나중에 기억이 안나서 볼 때 조금 더 가독성을 생각하여 정리하였다.
npm init react-app <폴더이름>
npm run start
div
나 <React.Fragment>
태그로 전체를 감싸주면 된다. 또는 빈 태그 <></>
도 사용 가능하다.function Button(){
return <button>{props.children}</div>;
}
...
<Button>안녕 난 버튼이야</Button>
컴포넌트는 웹 페이지를 구성하는 부품과도 같은 것이다. 부품 하나도 작은 부품으로 구성될 수 있듯이 컴포넌트도 더 작은 단위의 컴포넌트로 이루어질 수 있다.
바닐라 자바스크립트로 컴포넌트를 변경해야 하는 경우, 변경이 필요한 모든 DOM요소들을 불러와 각 프로퍼티나 애트리뷰트를 직접 바꿔줘야 한다. 이런 번거로움과 불안정성을 제거하기 위해 리액트는 아예 새로 렌더링을 하는 방식을 택한다.
그런데 이렇게 하면 바뀌지 않는 부분도 다시 렌더링되어서 불필요한 성능 저하가 일어나지 않을까?
이런 것을 방지하기 위해 리액트는 Virtual DOM이란 것을 사용한다.
리액트가 엘리먼트를 렌더링할 때 바로 DOM트리에 반영하는 것이 아니라 Virtual DOM에 먼저 반영한다(이 때 이전 컴포넌트를 전부 지우고 통채로 새로 만들게 됨). 그리고 실제 DOM트리에 반영하기 전에 이전 Virtual DOM과 변화를 반영한 Virtual DOM을 비교하여(일종의 스냅샷 비교) 실제로 변경사항이 있는 부분만 실제 DOM에서 변경한다. 리액트는 이렇게 효율적인 화면변경과 렌더링을 위해 Virtual DOM이라는 자료구조를 사용한다.
이미지 import: 다음과 같이 이미지를 임포트 하면 src안에 임포트한 이미지의 url이 들어간다. 이 때 rock.svg안에서는 export할 필요가 없다.
// 여기에 코드를 작성하세요
import Rock from "./assets/rock.svg";
export default function HandIcon(){
return <img src={Rock} alt="" />;
}
인라인 스타일에 backgroundImage속성
다음과 같이 템플릿 리터럴로 url안에 import한 이미지를 넣어줘야 한다. 그리고 style attribute에는 하나의 객체가 들어간다. 즉, style={스타일}
이 아닌 style={ {스타일} }
이 맞다.
import HandIcon from './HandIcon';
import purpleImg from './assets/purple.svg';
function HandButton({ value, onClick }) {
const handleClick = () => onClick(value);
return (
<button
onClick={handleClick}
style={{
backgroundImage: `url(${purpleImg})`,
backgroundRepeat: "no-repeat"
}}
>
<HandIcon value={value} />
</button>
);
}
export default HandButton;
컴포넌트 자신의 스타일(버튼 색, 폰트 크기 등)은 컴포넌트 안에서, 버튼 끼리의 margin과 같은 외부에 영향을 미치는 스타일은 부모 요소에서 지정해주는 것이 맞다.
className 잘 사용하기: 다음과 같이 className prop에 문자열로 넣어주면 되고, 재사용성을 위해 className prop을 부모 컴포넌트에서 받으면 더 좋다.
import diceImg from './assets/dice.png';
import './Dice.css';
function Dice({ className = '' }) {
const classNames = `Dice ${className}`;
return <img className={classNames} src={diceImg} alt="주사위 이미지" />;
}
export default Dice;
더 프로답게 쓰는 방법은 템플릿 리터럴보다 배열을 사용하는 것이다.
function Button({ isPending, color, size, invert, children }) {
const classNames = [
'Button',
isPending ? 'pending' : '',
color,
size,
invert ? 'invert' : '',
].join(' '); //배열의 join연산으로 문자열로 바꿈
return <button className={classNames}>{children}</button>;
}
export default Button;
혹은 다음과 같이 classnames라는 라이브러리를 사용하는 방법도 있다. (npm으로 설치)
import classNames from 'classnames';
function Button({ isPending, color, size, invert, children }) {
return (
<button
className={classNames(
'Button',
isPending && 'pending',
color,
size,
invert && 'invert',
)}>
{ children }
</button >
);
}
export default Button;
npm run build
명령어로 빌드할 수 있다.npx serve build
“/*”
를 입력(모든 파일에 정책을 적용하겠다는 뜻)jsx는 for문을 사용할 수 없음으로 forEach나 map메서드를 활용하자.
정렬 기능 구현
예를 들어 여러 아이템을 별점 순으로, 또는 이름 순으로 정렬하는 필터를 구현해보자.
아이템 리트스를 렌더링하는 부모 컴포넌트에서 order라는 스테이트를 만들어 이 순서를 관리할 수 있다. 필터 기준이 아이템의 프로퍼티가 된다. order에는 아이템의 프로퍼티가 들어간다. createdAt이 들어가 있다면 생성 날짜로 정렬되고, rating이 들어간다면 별점 기준으로 정렬되는 등 기능 구현을 할 수 있다.
import items from '../mock.json';
function App(){
const [order, setOrder] = useState('createdAt');
const sortedItems = items.sort((a, b)=> b[order]-a[order]);
const handleNewestClik = ()=>setOrder('createdAt');
const handleBestClick = ()=>setOrder('rating');
return (
<div>
<button onClick={handleNewestClick}>최신순</button>
<button onClick={handleBestClick}>베스트 순</button>
<ReviewList items={sortedItems} />
</div>
);
}
리스트 아이템 삭제 기능 구현(filter)
각 아이템에 삭제 버튼이 있다고 가정하고 filter메서드로 삭제기능을 구현해보자.
import mockItems from '../mock.json';
function App(){
const [items, setItems] = useState(mockItems);
const handleDelete = (id) =>{
const newItems = items.filter((item)=>item.id !== id);
setItems(newItems);
}
return (
<div>
<ReviewList items={sortedItems} />
</div>
);
}
...
function ReviewListItem({item, onDelete}){
const handleDeleteClick = ()=>onDelete(item.id);
return (
...
<button onClick={handleDeleteClick}>삭제하기</button>
...
);
}
...
function ReviewList({items, onDelete}){
...
return (
items.map((item)=>{
return <ReviewListItem item={item} onDelete={onDelete} />
});
);
}
map 메소드에서 key가 필요한 이유
fetch 함수 async await 할 때 사용 주의점
export const getFoodData = async () =>{
const response = await fetch("https://learn.codeit.kr/2266/foods");
const { foods } = **await** response.json();
return foods;
}
useEffect에서 api콜로 받아온 데이터를 정렬하기
데이터를 서버에서 받아온 후 수동으로 정렬해줄 수도 있지만 서버에서 정렬된 데이터를 받아올 수도 있다.
...
const handleLoad = async (order) => {
const { foods } = await getFoods(order);
setItems(foods);
};
const sortedItems = items.sort((a, b) => b[order] - a[order]);
useEffect(() => {
handleLoad(order);
}, [order]);
...
// api.js
export async function getFoods(order = '') {
const query = `order=${order}`;
const response = await fetch(`https://learn.codeit.kr/api/foods?${query}`);
const body = await response.json();
return body;
}
페이지네이션 사용 패턴
오프셋 기반
오프셋: 지금까지 받아온 데이터의 개수
오프셋 기반 페이지네이션은 서버의 데이터가 추가되거나 삭제되면 중복된 데이터를 불러오거나 데이터가 누락되는 등의 문제가 있음. 데이터 기준이 아닌 서버의 데이터 수를 기준으로 알려주기 때문이다.
커서 기반
커서: 지금까지 받은 데이터를 표시한 책갈피
커서 기반은 서버에서 구현하기 까다롭기도 하고, 데이터의 변화가 많지 않을 때는 오프셋기반 api도 충분히 사용할 수 있다.
오프셋 기반 페이지네이션 api 사용 패턴
// api.js
export async function getData({order="createdAt', offset = 0, limit = 6}) {
const query = `order=${order}&offset=${offset}&limit=${limit}`;
const response = await fetch(
`https://learn.../api/film-reviews?${query}`
);
const body = await response.json();
return body;
}
...
//app.js
const LIMIT = 6;
function App(){
..
const [offset, setOffset] = useState(0);
const [hasNext, setHasNext] = useState(true);
const handleLoad = async (options) =>{
const {reviews, paging} = await getData(options);
if (options.offset == 0)
setItems(reviews);
else setItems([...items, ...reviews]);
setOffset(options.offset + reviews.length);
setHasNext(paging.hasNext);
}
const handleLoadMore = async (options) =>{
handleLoad({order, offset, limit: LIMIT});
}
useEffect(()=>{
handleLoad({order, offset: 0, limit: LIMIT});
}, [order]);
...
return (
...
<button disabled={!hasNext} onClick={handleLoadMore}>더 보기</button>
...
);
}
커서 기반 페이지네이션 api 사용 패턴
import { useEffect, useState } from 'react';
import { getFoods } from '../api';
import FoodList from './FoodList';
const LIMIT = 10;
function App() {
const [order, setOrder] = useState('createdAt');
const [items, setItems] = useState([]);
const [cursor, setCursor] = useState('');
const handleNewestClick = () => setOrder('createdAt');
const handleCalorieClick = () => setOrder('calorie');
const handleDelete = (id) => {
const nextItems = items.filter((item) => item.id !== id);
setItems(nextItems);
};
const handleLoad = async (options) => {
const { foods, paging } = await getFoods(options);
if(!options.cursor){
setItems(foods);
}
else {
setItems((prev) => [...prev, ...foods]);
}
console.log(paging.nextCursor);
setCursor(paging.nextCursor);
};
const handleLoadMore = () => {
handleLoad({order, cursor, limit: LIMIT});
};
const sortedItems = items.sort((a, b) => b[order] - a[order]);
useEffect(() => {
handleLoad({order, cursor, limit: LIMIT});
}, [order]);
// https://learn.codeit.kr/api/foods?order=createdAt&limit=10
return (
<div>
<button onClick={handleNewestClick}>최신순</button>
<button onClick={handleCalorieClick}>칼로리순</button>
<FoodList items={sortedItems} onDelete={handleDelete} />
<button disabled={!cursor} onClick={handleLoadMore}>더보기</button>
</div>
);
}
export default App;
...
//api.js
export async function getFoods({ order = '', cursor = '', limit = 10 }) {
const query = `order=${order}&cursor=${cursor}&limit=${limit}`;
const response = await fetch(`https://learn.codeit.kr/api/foods?${query}`);
const body = await response.json();
return body;
}
비동기로 State변경 시 주의점
const handleDelete = (id) =>{
const nextItems = items.filter((item)=>item.id !== id);
setItems(nextItems);
}
const handleLoad = async (options) =>{
const {reviews} = await getReviews();
setItems([...items, ...reviews]); //getReviews가 처리되기 전에 handleDelete가 불리면
//컴포넌트의 items 스테이트는 하나가 삭제된 것으로 변경이 되었지만
// 여기 이 handleLoad는 비동기 함수라서 여기서 사용하는 items는 handleLoad함수가
// 호출되는 시점의 items 값을 가지고 있음.
}
// 따라서 다음과 같은 setState형태로 만들어주면 된다.
setItems((prev)=>[...prev, ...reviews]);
// 이전 state값을 사용하겠다고 명시한 코드
loading 스테이트 사용 패턴
function Component(){
const [isLoading, setIsLoading] = useState(false);
const handleLoad = async (options) =>{
let result;
try {
setIsLoading(true); //api호출 직전에 로딩값을 true로 설정
result = await apiCall();
} catch(err){
} finally {
setIsLoading(false); //에러가 나든 성공하든 끝나면 로딩값을 false로
}
}
}
네트워크 에러 처리 패턴
function Component(){
const [loadingError, setLoadingError] = useState(null);
const handleLoad = async (options) =>{
let result;
try {
setLoadingError(null);
result = await apiCall();
} catch(err) {
setLoadingError(err);
}
}
return (
...
{loadingError?.message && <span>{loadingError.message}</span>}
...
);
}
생각보다 많은 것들을 새로 알게 되어서 정리하는 데 오래 걸렸던 것 같다. 그리고 양이 워낙 많아서 알고리즘 문제를 풀지 못하였다.