고차 컴포넌트
컴포넌트를 반환하는 컴포넌트
React Memoization
리액트는 상태가 변경 될 때 마다 컴포넌트를 리렌더링함 → 컴포넌트가 많거나 복잡한 로직을 가진 함수가 있으면 성능 저하의 우려가 있음
⇒ 로직의 계산값을 기억해 동일한 계산을 하지 않도록 해주는 기법
React Memo
React.memo로 감싼 컴포넌트와 하위 컴포넌트는 메모이제이션 됨
Memoization이 해제되는 조건
- 컴포넌트 내의 상태가 변경 되는 경우
- 컴포넌트로 전달되는 props가 변경되는 경우
- props로 함수가 전달되는 경우 → 컴포넌트가 렌더링 될 때 마다 함수가 재정의 되기 때문
memoization이 해제되면 하위 컴포넌트 역시 렌더링 됨
// A.tsx
import { useState } from "react";
import React from "react";
export default React.memo(function A() {
const [cl, setCl] = useState(true);
console.log("A rendered");
return (
<>
<div className="flex flex-col gap-4">
<div
className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
onClick={() => setCl(!cl)}
>
D
</div>
<E />
</div>
</>
);
});
//App.tsx
import { useState } from "react";
import A from "./components/A";
export default function App() {
const [cl, setCl] = useState(true);
return (
<>
<div className="item-middle flex-col gap-4">
<div
className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
onClick={() => setCl(!cl)}
>
App
</div>
<A />
</div>
</>
);
}
// APP 컴포넌트를 클릭하면 App만 렌더링
// A 컴포넌트는 변경 사항이 없기 때문에, 렌더링 되지 않음
// A 컴포넌트를 클릭시 변동사항이 생기기 때문에, A 컴포넌트만 렌더링 됨
useCallback
useCallback은 함수 메모이제이션에 쓰이는 hook
useCallback(콜백함수, 의존성 배열) → 의존성 배열의 값이 바뀌지 않으면 실행되지 않음
import { useCallback, useState } from "react";
import A from "./components/A";
export default function App() {
const [cl, setCl] = useState(true);
const log = useCallback(() => console.log(cl), [cl]);
//cl의 값이 바뀌지 않으면 log()가 실행되지 않음
log();
return (
<>
<div className="item-middle flex-col gap-4">
<div
className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
onClick={() => setCl(!cl)}
>
App
</div>
<A />
</div>
</>
);
useMemo
useCallback가 함수를 메모이제이션한다면 useMemo는 값을 메모이제이션 한다
import { useMemo, useState } from "react";
import A from "./components/A";
export default function App() {
const [cl, setCl] = useState(true);
const cal = (cl: boolean) => {
let sum = 0;
let op = cl ? 1 : -1;
for (let i = 0; i < 100000; i++) {
sum++;
}
return sum * op;
};
//cl의 값이 바뀌지 않으면 cal()함수가 실행되지 않고, 계산 결과가 변하지 않음
const result = useMemo(() => cal(cl), [cl]);
return (
<>
<div className="item-middle flex-col gap-4">
<div
className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
onClick={() => setCl(!cl)}
>
{result}
</div>
<A />
</div>
</>
);
}
useReducer
useReducer(reducer, 초기값)
reducer는 함수 형태 ⇒ 초기값 state와, state를 변경 할 수 있는 요소인 action을 매개변수로 가짐
→ useReducer은 reducer 함수의 반환값을 사용
Redux와 사용 방법 비슷
import { useReducer, useState } from "react";
const reducer = (state: number, action: string) => {
switch (action) {
case "INCREASE":
return state + 1;
case "DECREASE":
return state - 1;
case "RESET":
return 0;
default:
return state;
}
};
function App() {
//cnt의 초기값은 useReducer의 매개변수로 전달한 초기값
//이후 Dispatch(setCnt) 함수로 변경된 값이 cnt에 저장됨
const [cnt, setCnt] = useReducer(reducer, 0);
return (
<>
<div>현재 값 : {cnt}</div>
<button onClick={() => setCnt("INCREASE")}>증가</button>
<button onClick={() => setCnt("DECREASE")}>감소</button>
<button onClick={() => setCnt("RESET")}>초기화</button>
</>
);
}
export default App;
reducer를 다른 파일에서 export 하여 여러 컴포넌트에 사용 가능
//countReducer.ts
export const reducer = (state: number, action: string) => {
switch (action) {
case "INCREASE":
return state + 1;
case "DECREASE":
return state - 1;
case "RESET":
return 0;
default:
return state;
}
};
//App.tsx
import { useReducer, useState } from "react";
import { reducer } from "./reducer/countReducer";
function App() {
const [cnt, setCnt] = useReducer(reducer, 0);
return (
<>
<div>현재 값 : {cnt}</div>
<button onClick={() => setCnt("INCREASE")}>증가</button>
<button onClick={() => setCnt("DECREASE")}>감소</button>
<button onClick={() => setCnt("RESET")}>초기화</button>
</>
);
}
export default App;
useContext
context API를 통해 전역적으로 상태 관리를 할 수 있게 도와주는 hook
context를 사용하고자 하는 컴포넌트들의 최상위 컴포넌트에 Context 컴포넌트와 Provider 속성을 추가해 줘야 함
Context 컴포넌트의 value props에 사용하고자 하는 값 또는 메서드들을 전달
import { createContext, useState } from "react";
import Page from "./components/Page";
//Context 컴포넌트
export const CounterContext = createContext<{
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
} | null>(null);
function App() {
const [count, setCount] = useState(0);
return (
<>
//Provider 속성과 value를 추가한 Context 컴포넌트
<CounterContext.Provider value={{ count, setCount }}>
<Page />
</CounterContext.Provider>
</>
);
}
export default App;
useContext를 이용해 설정한 context를 사용
import { useContext } from "react";
import { CounterContext } from "../App";
function Page() {
//useContext를 사용해 CounterContext에 접근
const { count, setCount} = useContext(CounterContext)!;
return (
<>
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>증가</button>;
</>
);
}
export default Page;
Provider로 감싸져 있는 한 컴포넌트 계층 구조에 상관 없이 Context(변수) 사용 가능
👏 Non-null Assertion Operator ⇒ 구문 뒤에 **!**를 붙여 개발자 차원에서 null이 아님을 단언
Provider는 따로 컴포넌트로 빼서 사용 하기도 함
//CounterProvider.tsx
import { createContext, useState } from "react";
export const CounterContext = createContext<{
count: number;
setCount: React.Dispatch<React.SetStateAction<number>>;
} | null>(null);
function CounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<>
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
</>
);
}
export default CounterProvider;
//App.tsx
import Page from "./components/Page";
import CounterProvider from "./context/CounterProvider";
function App() {
return (
<>
//Provider를 하나의 컴포넌트로 만들어 사용
<CounterProvider>
<Page />
</CounterProvider>
</>
);
}
export default App;
zustand
떠오르는 전역 상태 관리 툴 → store에 상태와 set 함수를 작성하고 컴포넌트에서 불러와 사용 가능
import { create } from "zustand";
interface CounterStore {
count: number;
inc: () => void;
}
export const useCountStore = create<CounterStore>((setCount) => ({
count: 0,
inc: () =>
setCount((state) => ({
count: state.count + 1,
})),
}));
객체의 Key에 변수 할당
const keyName = "age"
const obj = {
name: 'john',
[keyName]: 20
}
//name: john
//age: 20
코드에 적용하여 코드 간소화 가능
import { useState } from "react";
export default function Basic() {
//객체 형식의 formState를 지정
const [formState, setFormState] = useState({
name: "",
email: "",
pw: "",
pwConfirm: "",
});
const onChangeFormState = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormState((formState) => ({
...formState,
//name:value 형식의 객체 요소를 추가
[e.target.name]: e.target.value,
}));
};
return (
<>
<form action="">
// 이 input이 실행되면 formState는
// name:(input 내용)이라는 속성을 가짐
<input
className="border"
name="name"
type="text"
onChange={onChangeFormState}
value={formState.name}
></input>
<input
className="border"
name="email"
type="text"
onChange={onChangeFormState}
value={formState.email}
></input>
<input
className="border"
name="pw"
type="text"
onChange={onChangeFormState}
value={formState.pw}
></input>
<input
className="border"
name="pwConfirm"
type="text"
onChange={onChangeFormState}
value={formState.pwConfirm}
></input>
</form>
</>
);
}
Custom Hook
다양한 기능을 하는 커스텀 함수들을 만들어 React Hook처럼 사용
// useInput.tsx
import { useState } from "react";
type useInputReturn = [
string,
(e: React.ChangeEvent<HTMLInputElement>) => void
];
export const useInput = (initialValue: string): useInputReturn => {
const [value, setValue] = useState(initialValue);
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
//useInput은 입력한 value와 입력 함수 onChangeHandler를 리턴
return [value, onChangeHandler];
};
//App.tsx
import { useInput } from "../utils/utils";
function App() {
const [name, onChange] = useInput("");
return (
<>
<form action="">
<input
className="border"
value={name}
type="text"
onChange={onChange}
></input>
</form>
</>
);
}
export default App;
데이터 통신
NEXT.JS는 axios보다는 Fetch API를 더 선호하는 편
REST API
REprentational State Transfer API
- GET
- PUT
- PATCH
- DELETE
- OPTION
FETCH API
Fetch API를 이용해 서버로부터 데이터를 받아 옴
Fetch는 비동기로 동작하기 때문에, 브라우저는 Fetch 내부 로직을 기다려주지 않음
→ 데이터를 받아오기 전에 브라우저가 렌더링되어 버리면 undefined 에러가 날 수 있음
비동기 처리
- Promise
- pending : 데이터를 받기 위해 대기중인 상태
- fulfilled : 데이터를 성공적으로 받아 Promise 객체로 받은 상태
- then() 메서드를 통해 데이터 사용
- response 객체를 이용하여 데이터에 접근 가능
- 단, fetch는 promise 객체만을 반환하기 때문에, useState를 이용해 response 객체로부터 받은 data를 할당해 주어야 함
- rejected : 데이터 전송이 거절된 상태
import { useEffect, useState } from "react";
function Fetch() {
const [data, setData] = useState([]);
useEffect(() => {
fetch("<https://jsonplaceholder.typicode.com/todos>", {
method: "GET",
})
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return (
<>
{data.map((el) => (
))}
;
);
}
export default Fetch;
- async / await
비동기 작업을 실행할 함수를 async 키워드로 감싼 뒤, 실행할 동작들 앞에 await 키워드를 붙여준다
→ await를 만나면 awiat의 실행이 끝날 때 까지 다른 동작을 실행하지 않음
import { useEffect, useState } from "react";
function Fetch() {
const [data, setData] = useState([]);
const fetchData = async () => {
const response = await fetch("<https://jsonplaceholder.typicode.com/todos>", {
method: "GET",
});
const data = await response.json();
setData(data);
};
useEffect(() => {
fetchData();
}, []);
return (
<>
{data.map((el) => (
))}
;
);
}
export default Fetch;
결과 타입은 제네릭으로 설정
에러 핸들링
try-catch문을 이용해 에러 핸들링
- fetch 단계에서 발생하는 에러는 catch에서 처리
- 이외의 에러는 throw new Error를 통해 에러 정의
- 모든 과정이 끝난다면 finally를 통해 fetch 종료
import { useEffect, useState } from "react";
function Fetch() {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsLoading(true);
//try문에서 fetch 시작
try {
const response = await fetch(
"<https://jsonplaceholder.typicode.com/todos>",
{
method: "GET",
}
);
const data = await response.json();
//에러가 발생할 경우 에러 정의
if (!response.ok) throw new Error("서버 통신 오류");
setData(data);
} catch (error) {
//에러 출력
setIsError(true);
console.log(error);
} finally {
//모든 동작 종료
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<>
{isError ? (
Error
) : isLoading ? (
Loading
) : (
data.map((el) =>
)
)}
;
);
}
export default Fetch;
라우팅
라우팅은 url 주소를 통해 다른 페이지로 이동하게 해주는 방법
과거와 많이 달라져서, routes나 route 등 복잡한 방법을 사용하지 않고도 쉽게 라우팅을 할 수 있게 되었다
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./css/index.css";
import App from "./App.tsx";
import Basic from "./components/Basic.tsx";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
//라우터 배열 생성
const router = createBrowserRouter([
// 라우터 배열 내에는 path(url 주소)와 element(이동할 페이지 컴포넌트)가 들어감
{
path: "/",
element: <App />,
},
{
path: "/login",
element: <Login />,
},
]);
createRoot(document.getElementById("root")!).render(
<StrictMode>
//RouterProvider을 이용해 라우터 사용
<RouterProvider router={router}></RouterProvider>
</StrictMode>
);
App.tsx보다는 view 폴더 내에 HomeView.tsx, LoginView.tsx 등의 파일로 생성
router가 길어지는 것을 대비해 외부 파일로 빼기도 함
Outlet
라우터에서는 Children 대신 Outlet이라는 컴포넌트를 사용해 내용을 변경
//DefaultLayout.tsx
import { Outlet } from "react-router-dom";
import Footer from "../components/Footer";
import Header from "../components/Header";
function DefaultLayout() {
return (
<>
<Header />
<Outlet />
<Footer />
</>
);
}
export default DefaultLayout;
//router.tsx
import { createBrowserRouter } from "react-router-dom";
import HomeView from "../view./HomeView";
import LoginView from "../view./LoginView";
import DefaultLayout from "../view./DefaultLayout";
const router = createBrowserRouter([
{
path: "/",
element: <DefaultLayout />,
// children 속성으로 원하는 컴포넌트들을 넘겨줌
children: [
{
path: "/home",
element: <HomeView />,
},
{
path: "/login",
element: <LoginView />,
},
],
},
]);
export default router;
등록되어있지 않은 모든 주소는 *로 처리
import { createBrowserRouter } from "react-router-dom";
import HomeView from "../view./HomeView";
import LoginView from "../view./LoginView";
import DefaultLayout from "../view./DefaultLayout";
const router = createBrowserRouter([
{
path: "/",
element: <DefaultLayout />,
children: [
{
path: "/home",
element: <HomeView />,
},
{
path: "/login",
element: <LoginView />,
},
],
},
{
path: "*",
element: <h1>없는 주소입니다</h1>
}
]);
export default router;
다른 페이지로 이동하게 만들고 싶으면 Link 태그를 사용하면 됨
//HomeView.tsx
import { Link } from "react-router-dom";
function HomeView() {
return (
<>
<div>LoginView</div>
<Link to="/login">로그인으로</Link>
</>
);
}
export default HomeView;
//LoginView.tsx
import { Link } from "react-router-dom";
function LoginView() {
return (
<>
<div>LoginView</div>
<Link to="/home">홈으로</Link>
</>
);
}
export default LoginView;
useNavigate() 훅 역시 이동 용도로 사용 가능
import { useNavigate } from "react-router-dom";
function LoginView() {
const navigate = useNavigate();
return (
<>
<div>LoginView</div>
<button onClick={() => navigate("/home")}>홈으로</button>
</>
);
}
export default LoginView;
이외 router용 hooks
- useParams
- url의 다양한 쿼리를 받을 때 사용
- useSearchParams
- url의 검색 쿼리를 받을 때 사용
- useLocation
- 현재 위치를 나타내줌
본 게시글의 예제 코드는 수코딩(https://www.sucoding.kr)을 참고했습니다
'Frontend Study' 카테고리의 다른 글
[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 7일차 (0) | 2024.10.08 |
---|---|
[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 6일차 (1) | 2024.10.08 |
[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 4일차 (0) | 2024.09.26 |
[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 3일차 (0) | 2024.09.25 |
[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 2일차 (0) | 2024.09.24 |