Next.js 캠프가 벌써 2주차가 완료되었다. 이번 주차에는 오프라인으로 수업을 들었는데, 통학 시간도 길고 하필 출퇴근 시간이 겹쳐서 적응하기 힘들었다. 그래도 오프라인으로 들으니 집중도 잘 되고, 아침형 인간이 되어서 오히려 럭키비키?🍀
이번주엔 리액트에 대해서 배웠다. vite와 Tailwind CSS를 활용한 프로젝트 환경 설정부터, Zustand를 활용한 상태관리와 json server 데이터 통신까지 정말 엄청 많은 것을 배웠다. 진도가 너무 빨리 나가서 버겁긴 했지만, 많은 것을 얻을 수 있어서 너무 재밌는 한 주였다. (너무 빨라서 가끔씩은 숨이 차요..)
리액트를 시작하기 앞서 가장 먼저 vite와 tailwind 개발 환경 설정을 배웠다. 나는 지금까지 react 프로젝트를 만들때 CRA 방식을 사용했다..근데 이번 강의를 들으면서 정말 깊게 반성하게 되었다. (반성을 시작해 반성으로 끝난 한주)
CRA는 굉장히 오래된 라이브러리이고, last release가 2022년도로 업데이트가 되지 않고 있다.
Vite는 프론트엔드 개발을 위한 빌드 도구이다. react 뿐만 아니라 vanilla, svelte, vue 등 프론트엔드 개발 어느 영역에서도 사용할 수 있다.
Vite를 사용하면 CRA를 사용할 때보다 더 빠른 개발 서버를 사용할 수 있다. 필요한 모듈만 그때그때 제공하므로 초기 서버 시작 시간이 빠르고, 개발 중 변경 사항에 대한 반응 속도가 빠르다.
tailwind css는 utility-first
를 지원하는 CSS 프레임워크이다. 컴포넌트 css 스타일링 방법은 굉장히 다양하다. Styled-Components, Emotion, Vanilla Extract, Linara 등 매우 많은 CSS 라이브러리가 있는데, 요즘은 Tailwind CSS가 핫한 것 같다.
코드가 직관적이다.
<div class="bg-slate-100 rounded-xl p-8 dark:bg-slate-800">
이런 방식으로 표기하여, 각 요소에 필요한 스타일만 적용할 수 있어 매우 직관적이다. 커스터마이징 하기도 좋고, 반응형 디자인을 쉽게 구현할 수 있다. 많은 장점이 있지만, 코드가 길어지고 가독성이 떨어져서 나는 개인적으로 좋아하지는 않는다..그래도 효율이 좋아서 앞으로는 tailwind css를 적극적으로 써봐야겠다.
아무튼 vite와 tailwind css를 설치하고, 기본 세팅된 상태를 저장하여 빠르게 프로젝트 생성이 가능하도록 react-tailwind-starter-kit으로 만들었당. Git Repo에서 클론받아서 쓰면 매우 편함
이번 주는 거의 리액트 훅을 배우느라 다 지나갔다고 봐도 무방할 만큼 훅에 대해서 많이 배우고, 또 사용해봤다. useState
, useRef
, useCallback
, useMemo
, React.memo
, useReducer
을 배우고, 각 기능을 활용하여 투두리스트를 만들어보면서 사용방법을 익혔다.
- useState: 상태 변수를 선언하고, 해당 상태를 갱신하는 함수를 반환한다.
const [state, setState] = useState(initialState);
- useRef: DOM 요소나 다른 값의 참조를 저장하고 유지할 수 있게 한다.
const ref = useRef(initialValue);
- useCallback: 함수를 메모이제이션하여, 컴포넌트가 리렌더링 될 때 동일한 함수 인스턴스를 유지한다.
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
- useMemo: 값의 계산을 메모이제이션하여, 의존성 배열의 값이 변경되지 않으면 이전 계산의 결과를 반환한다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- React.Memo: 고차 컴포넌트를 사용하여 컴포넌트가 동일한 props로 리렌더링 되는 것을 방지한다.
const MemoizedComponent = React.memo(Component);
- useReducer: 상태와 상태를 갱신하는 로직을 컴포넌트 외부로 추출하여 복잡한 상태 관리를 처리한다.
const [state, dispatch] = useReducer(reducer, initialState);
- useEffect: 리액트 함수형 컴포넌트에서 사이드 이펙트인 데이터 가져오기, 구독 설정, DOM 업데이트 등을 수행할 수 있게 한다. 의존성 배열을 통해 특정 값이 변경될 때만 효과를 실행하거나, 마운트 될 때 한 번만 실행되도록 설정
import React, { useEffect, useState } from 'react'; function DataFetchingComponent() { const [data, setData] = useState(null); useEffect(() => { // 데이터를 가져오는 함수 async function fetchData() { const response = await fetch('url'); const result = await response.json(); setData(result); } // 컴포넌트가 마운트될 때 데이터 가져오기 fetchData(); }, []); // 빈 배열을 의존성 배열로 전달하여 마운트 시 한 번만 실행
혼자 공부할땐 많이 어렵게 느껴졌고, useState만 주구장창 사용했었는데 강의를 들으니 굉장히 재미있다. 아직 각 훅을 혼자서 응용하는 것에는 조금 어려움이 있어서, 조금 더 개념을 깊게 공부하고 예제 코드를 만들어봐야겠다.
Context API로 상태 관리 하는 방법을 배우고, 수업 중 context를 활용하여 투두리스트를 만들어보았다. 수업 중엔 todos와 setTodos를 넘겨서 투두를 구현해보았는데, todos, addTodo, toggleTodo, deleteTodo를 넘겨서 투두리스트를 만들어보라고 과제를 내주셨다.
todoFuncContext.tsx
//context 임포트
import { createContext, useState } from "react";
export type TTodo = {
id: number;
text: string;
isCompleted: boolean;
};
type TTodoFuncContext = {
todos: TTodo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
};
export const TodoFuncContext = createContext<TTodoFuncContext>({
todos: [],
addTodo: () => {},
toggleTodo: () => {},
deleteTodo: () => {},
});
const TodoFuncContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [todos, setTodos] = useState<TTodo[]>([]);
const addTodo = (text: string) => {
setTodos((prev) => [...prev, { id: Date.now(), text, isCompleted: false }]);
};
const toggleTodo = (id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
)
);
};
const deleteTodo = (id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
return (
<TodoFuncContext.Provider
value={{ todos, addTodo, toggleTodo, deleteTodo }}>
{children}
</TodoFuncContext.Provider>
);
};
export default TodoFuncContextProvider;
먼저 3개의 함수를 넘겨주는 context 컴포넌트를 만들고, App에서 TodoFuncContextProvider
을 사용하여 Todo 컴포넌트를 감싸주었다.
TodoEditor.tsx
import { useContext, useState } from "react";
import Button from "../../html/Button";
import Input from "../../html/Input";
import { TodoFuncContext } from "../../../context/todoFuncContext";
//import { TodoContext } from "../../../context/todoContext";
const TodoEditor = () => {
const [text, setText] = useState("");
const { addTodo } = useContext(TodoFuncContext);
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
addTodo(text);
setText("");
};
return (
<>
<form className="grid gap-4" onSubmit={onSubmitHandler}>
<div className="flex gap-2">
<Input
type="text"
placeholder="Enter Todo List"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Button
type="submit"
className="bg-[#4f4f4f] text-white w-[77px] shrink-0 rounded-lg">
Add
</Button>
</div>
</form>
</>
);
};
export default TodoEditor;
TodoEditor에서 addTodo
를 받아서, Input 박스 내에 입력된 텍스트 값을 추가하도록 구현하였다.
TodoList.tsx
import { useContext } from "react";
import TodoListItem from "./TodoListItem";
import { TodoFuncContext } from "../../../context/todoFuncContext";
const TodoList = () => {
const { todos } = useContext(TodoFuncContext);
return (
<ul className="flex flex-col gap-4 mt-4 max-h-[284px] overflow-scroll">
{todos.map((todo) => (
<TodoListItem key={todo.id} todo={todo} />
))}
</ul>
);
};
export default TodoList;
TodoList 컴포넌트에서는 todos
를 TodoListItem 컴포넌트로 전달한다.
TodoListItem.tsx
import { useContext } from "react";
import { twMerge } from "tailwind-merge";
import { TodoFuncContext } from "../../../context/todoFuncContext";
import Button from "../../html/Button";
import CheckBox from "../../html/CheckBox";
import { TTodo } from "../todo_props_memo/Todo";
import React from "react";
const TodoListItem = ({ todo }: { todo: TTodo }) => {
const { toggleTodo, deleteTodo } = useContext(TodoFuncContext);
console.log("TodoListItem");
return (
<li className="flex items-center justify-between border border-[#4F4F4F] h-[44px] px-[15px] rounded-lg bg-[rgba(53,56,62,0.05)] select-none shrink-0">
<CheckBox checked={todo.isCompleted} onChange={() => toggleTodo(todo.id)}>
<span
className={twMerge(
`text-[#35383E]`,
todo.isCompleted && "line-through"
)}>
{todo.text}
</span>
</CheckBox>
<Button
className="border border-[#4f4f4f] rounded w-[23px] h-[23px] flex justify-center items-center"
onClick={() => deleteTodo(todo.id)}>
<svg
width="15"
height="16"
viewBox="0 0 15 16"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.50002 9.81827L12.9548 15.2731L14.7731 13.4548L9.31829 8L14.7731 2.54518L12.9548 0.726901L7.50002 6.18173L2.04519 0.726902L0.226918 2.54518L5.68174 8L0.226919 13.4548L2.04519 15.2731L7.50002 9.81827ZM7.50002 9.81827L9.31829 8L7.50002 6.18173L5.68174 8L7.50002 9.81827Z"
fill="#4F4F4F"
/>
<path
d="M7.50002 9.81827L9.31829 8L7.50002 6.18173L5.68174 8L7.50002 9.81827Z"
fill="#4F4F4F"
/>
</svg>
</Button>
</li>
);
};
export default React.memo(TodoListItem);
TodolistItem에서 직접 toggleTodo
와 deleteTodo
함수를 useContext
로 불러와 사용한다.
실행시켜보니 정상적으로 투두리스트가 동작하는 것을 확인할 수 있다.!
라우터는 주소에 따라 서로 다른 페이지를 보여준다. React는 SPA(Single Page Application)으로 기본적으로는 주소에 따른 다른 내용을 보여주지 못한다.
예전에는 React에서 라우팅을 처리하기 위해 Route
컴포넌트를 사용했다.
BrowserRouter
와,Routes
, Route
를 사용하는 방식이었다. 옛날 방식...이지만 나는 이 방식으로 프로젝트를 최근까지 만들어서 심하게 진짜 많이 반성했다.😭😭
요즘 방식은
router/index.tsx
import { createBrowserRouter } from "react-router-dom";
import About from "../pages/About";
import Contact from "../pages/Contact";
import Write from "../pages/Write";
import Read from "../pages/Read";
import Layout from "../components/Layout";
import Home from "../pages/Home";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "",
element: <Home />,
},
{
path: "/about",
element: <About />,
},
{
path: "/contact",
element: <Contact />,
},
{
path: "/write",
element: <Write />,
},
{
path: "/read/:id",
element: <Read />,
},
],
},
]);
export default router;
Main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./assets/css/index.css";
import router from "./router/index.tsx";
import { RouterProvider } from "react-router-dom";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
RouterProvider
을 쓴다.
2주차 마지막 실습으로 지금까지 배운 내용들을 활용하여 블로그 만들기 실습을 해보았다. 아직 익숙하지 않은 개념들로 바로 실습을 쫒아가려니 힘들었는데, 강사님께서 영상을 업로드 해주셔서 복습을 할 수 있었다. 감사합니다.
블로그를 만들면서 배웠던 것 중 디바운스
라는 신기한 개념을 배웠다
이벤트 핸드러가 여러 번 호출되는 것을 방지하기 위해 사용되는 기법
실시간 검색 기능을 구현할 때 사용한다. 검색어 하나하나 입력할 때마다 요청할 경우 요청이 너무 많아진다. 검색어를 다 입력하고, 입력이 일정 시간 이상 없을 때 API를 호출하는 방식이다. 연속되는 타이핑 과정 중 불필요한 API 호출을 예방할 수 있다.
디바운싱을 활용하면 타이핑 도중에는 API 호출이 일어나지 않고, 사용자가 입력을 멈추고 일정 시간 이후에만 API 호출이 일어나기 때문에 네트워크와 서버 자원을 효율적으로 사용할 수 잇다.!
useEffect(() => {
const timer = setTimeout(() => {
console.log("db검색");
fetchPosts("/posts?title_like=" + searchText);
}, 500); //0.5초 기다리기 이후에 함수 실행
return () => {
clearTimeout(timer);
};
}, [searchText]);
타이핑을 통한 useEffect
내 콜백은 searchText가 변경될 때마다 호출된다. 이때 설정된 타이머는 500ms 동안 대기한 후 fetchPosts 함수를 호출한다. 만약 500ms 내에 searchText가 다시 변경되면 기존 타이머는 클리어되고 새로운 타이머가 설정된다. 이를 통해 사용자가 타이핑을 멈췄을 때만 API 호출이 이루어지도록 한다. clearTimeout
을 사용하여 이전 타이머를 제거하여 불필요한 호출을 방지한다.
일주일동안 진짜 알차게 배웠다. 다음주는 Next.js를 배운다. 너무 기대된다. 더...열시미히 해야지...
본 후기는 [유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 과정(B-log) 리뷰로 작성 되었습니다.
#유데미 #udemy #웅진씽크빅 #스나이퍼팩토리 #인사이드아웃 #미래내일일경험 #프로젝트캠프 #부트캠프 #Next.js #프론트엔드개발자양성과정 #개발자교육과정