코드 리팩토링을 결심하게 된 계기는...일단 만든 프로그램이 돌아가긴 하지만 볼일 본 뒤 닦지 않은 듯한 찝찝함 때문이었다. 예전 팀 프로젝트들을 진행하면서, 시간이 좀 더 있었다면 리뷰도 받고 고쳐 보고 싶었는데, 커리큘럼을 따라가느라고 제대로 해 보지 못한 부분도 있다.
아주 간단한 기능들만 들어간 투두리스트이긴 하지만, 코드를 내 마음대로 여기 저기 뜯어 고쳐 보고 공부 할 수 있는 좋은 기회라 생각해 진행 해 보기로 했다.
*댓글로 더 나은 방향성을 제시 해 주신다면 매우 감사드립니다.
나는 추상적인 개념에 대한 이해가 잘 안되는 편이다. 처음에 코드의 추상화라는 단어를 들었을 때는 잉 코드를 추상화 한다니 그게 무슨 말인가 싶었다. Computer Science에서의 추상화라는 것은 복잡한 자료나 모듈, 시스템 등으로 부터 핵심적인 개념 또는 기능을 간추려 내는 것이라고 한다. 일단 리팩토링을 진행 전 내 코드를 보면, 뷰를 구성하는 모든 비즈니스 로직이 그 컴포넌트에 다 들어있고, 반복되는 코드들이 산재했다. 그거슨 가독성이 매우 떨어졌고, 나 조차도 코드를 수정 하기가 번거로웠다...그 외에도 문제가 많았지만, 일단 리팩토링 진행 과정을 기록 하기 전에, 코드 추상화란 무엇인지 알아봅시다.
코드에서 추상화라는 개념은 객체 지향 프로그래밍의 특성에서 찾아 볼 수 있다. 그래서 객체 지향 프로그램에 대해 알아 보았다.
객체 지향 프로그래밍은 무엇? from 코드잇
프로그램을 단순히 명렁어의 집합으로만 보지 않고, 프로그램의 각 부분을 독립적 단위의 객체로 보는 프로그래밍 관점이다.
객체 지향 프로그래밍의 장점
프로그램의 생산성이 높다
프로그램을 효율적으로 관리, 유지보수하기 쉽다
프로그램 코드 간 의존도가 낮기 때문에 새로운 기능을 추가하거나 기존의 기능을 수정할 때 소요되는 리소스가 적게든다.
객체 지향 프로그래밍의 특성
1. 추상화
어떤것을 사용, 이용할 때 몰라도 되는 정보를 감추고 필수적인 부분만 남기는 것을 의미한다. 프로그래밍에서는 프로그래머들이 특정 코드를 사용할 때 필수적인 정보를 제외한 나머지 세부사항은 가리는 것을 의미.
2. 캡슐화
객체의 일부 구현 내용에 대한 외부로부터의 직접적인 엑세스를 차단하는 것. 캡슐화 하지 않으면 데이터가 손실되거나 의도치않게 변경되는 경우가 생긴다.
객체의 속성과 그것을 사용하는 행동을 하나로 묶는 것인데 간단히 말해 변수에 메소드로만 접근할 수 있도록 하는 것을 의미한다. 이러면 코드를 수정할 때 발생할 수 있는 오류를 줄여준다.
3. 상속
클래스 사이에 부모-자식 관계를 설정해서 부모 클래스의 변수나 메소드를 자식 클래스에서 사용할 수 있는 것을 상속이라 한다. 중복되는 코드를 작성하지 않아도 되어 코드의 재활용성이 올라감.
4. 다형성
다형성이란 하나의 메소드나 클래스가 있을 때 이것들이 다양한 방식으로 동작하는 것을 의미한다. 예로 키보드의 모든 키들은 누르면 어떤 입력을 컴퓨터에 전달한다는 공통점이 있다. 하지만 각 키들은 고유의 동작을 가지고있다. (esc: 작업취소, enter: 작업실행...)
이렇게 조작하는 방법은 같지만 대상에 따라 실제 동작 방식이 달라지는 것을 의미한다.
이러한 특징을 가지고 있다고 하는데, 솔직히 내가 진행한 리펙토링이 객체 지향 프로그래밍을 위한 그런 거창한 리팩토링인지 잘 모르겠다. 일단 시도는 했고, 시작 한 것에 의의를 두었다. 정답이 없는 것이라고는 하지만 계속 해나가다 보면 좀더 나은 방향으로 코드를 쓰게 될 수 있지 않을까 싶다.
코드 리팩토링
# 아키텍쳐
첫번째 컴포넌트 관계도를 보면, Layout이란 컴포넌트는 Header, Form, Footer를 렌더링만 해주는 재사용을 하지 않는 컴포넌트였고 불필요한 depth를 만들었다. 그래서 Layout을 위와 같이 라우팅에 따라 Form을 보여주기도 하고, TodoDetail 이란 컴포넌트를 보여주기도 하는 구조로 바꾸어 주었다.
수정 전
// 수정 전 Main.js
import Layout from "../components/Layout";
const Main = () => {
return <Layout />;
};
export default Main;
// 수정 전 Detail.js
import TodoDetail from "../components/TodoDetail";
const Detail = () => {
return <TodoDetail />;
};
export default Detail;
// 수정 전 Layout.jsx
import Footer from "./Footer";
import Form from "./Form";
import Header from "./Header";
const Layout = () => {
return (
<>
<Header />
<Form />
<Footer />
</>
);
};
export default Layout;
수정 후
// 수정 후 MainPage.js
import Footer from "../components/Footer";
import Form from "../components/Form";
import Header from "../components/Header";
import Layout from "../components/Layout";
const MainPage = () => {
return (
<Layout>
<Header />
<Form />
<Footer />
</Layout>
);
};
export default MainPage;
// 수정 후 DetailPage.js
import Footer from "../components/Footer";
import Header from "../components/Header";
import Layout from "../components/Layout";
import TodoDetail from "../components/TodoDetail";
const DetailPage = () => {
return (
<Layout>
<Header />
<TodoDetail />
<Footer />
</Layout>
);
};
export default DetailPage;
// 수정 후 Layout.jsx
import styled from "styled-components";
const StyledLayout = styled.div`
max-width: 900px;
height: 100%;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
`;
const Layout = (props) => {
return <StyledLayout>{props.children}</StyledLayout>;
};
export default Layout;
수정 후 Layout 컴포넌트에서 children을 이용해 공통 컴포넌트들인 Header, Footer의 중복 import를 피했다.
# 직관적이지 않은 naming
Main, Detail => MainPage, DetailPage 그리고 List => TodoList 로 바꾸어 주었다.
# App.css, index.css
CRA로 프로젝트를 구성하면서 App.css나 index.css같은 파일을 생각없이 그냥 뒀었는데, 이것이 나중에 프로젝트에 어떤 영향을 끼칠 수도 있기 때문에 제거 해 주어야 한다. 더욱이 컴포넌트의 스타일링을 위해 styled components를 이용했기 때문에 스타일링이 중복되지 않도록 이 파일들은 지워준다.
# 반복되는 UI
API가 offline일 때를 대비해 로컬스토리지에 데이터를 저장해서 화면을 보여주도록 구성하는 과정에서 반복되는 UI 해결, Button이나 Input같은 공통된 요소를 컴포넌트로 분리함.
1. EditLocalTodoModal 과 EditTodoModal
두 컴포넌트는 각각 로컬스토리지와 서버 데이터에 저장된 To-do 의 상세조회 시 수정 버튼을 눌렀을 때 뜨는 모달이다. 수정을 위해 initialState를 해당 id의 데이터로 설정 해 주었는데, 처음에는 같은 UI이지만 initialState만 각각 다른 데이터에서 받아오는 다른 컴포넌트들이었다.
전역상태관리를 redux를 사용해서 해주고 있었기 때문에 error라는 state에 네트워크 에러 상태를 저장해 error일 때만 local storage에 저장된 데이터가 보일 수 있도록 삼항연산자를 이용해 코드를 수정하였고 EditTodoModal이라는 컴포넌트에서 로컬스토리지 데이터와 API request에서 받는 데이터를 모두 처리 하도록 구현했다.
수정 전
// 수정 전 EditLocalTodoModal.jsx
// ...생략
/* 로컬스토리지 데이터 수정용 모달 */
const EditLocalTodoModal = (props) => {
const navigate = useNavigate();
const { closeModal, localTodosDetail } = props;
const initialState = {
id: localTodosDetail[0].id,
text: localTodosDetail[0].text,
deadLine: localTodosDetail[0].deadLine,
};
const [text, setText] = useState(initialState.text);
const [deadLine, setDeadLine] = useState(initialState.deadLine);
// ...생략
수정 후
// 수정 후 EditTodoModal.jsx (EditLocalTodoModal.jsx은 삭제)
const EditTodoModal = (props) => {
const navigate = useNavigate();
const { detail, closeModal, error, localTodosDetail } = props;
const initialState = {
id: error === null ? detail.id : localTodosDetail[0].id,
text: error === null ? detail.text : localTodosDetail[0].text,
deadLine: error === null ? detail.deadLine : localTodosDetail[0].deadLine,
};
const [text, setText] = useState(initialState.text);
const [deadLine, setDeadLine] = useState(initialState.deadLine);
// ...생략
2. Input, Button 컴포넌트로 분리해서 재활용하기
수정 전
// 수정 전 Form.jsx
const Form = () => {
//...생략
return (
<>
<StForm onSubmit={onSubmitHandler}>
<StElementsDiv>
<StInput
type="text"
value={text}
required
placeholder="투두를 입력하세요"
onChange={(e) => setText(e.target.value)}
/>
<StInput
style={{ cursor: "pointer" }}
type="date"
value={deadLine}
required
min={todaysDate}
onChange={(e) => setDeadLine(e.target.value)}
/>
<StButton type="submit">등록</StButton>
<StButton type="button" onClick={onDeleteHandler}>
삭제
</StButton>{" "}
<StSearchInput
type="search"
value={query || ""}
onChange={handleSearch}
placeholder="검색어를 입력하세요"
/>
</StElementsDiv>
</StForm>
<List
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
query={query}
todos={todos}
setTodos={setTodos}
/>
</>
);
};
export default Form;
수정 후
// 수정 후 Button.jsx
import styled from "styled-components";
const StyledButton = styled.button`
margin-top: 35px;
width: 50px;
height: 30px;
border: none;
cursor: pointer;
border-radius: 5px;
background: aliceblue;
`;
const Button = (props) => {
return (
<StyledButton type={props.type} onClick={props.onClick}>
{props.children}
</StyledButton>
);
};
export default Button;
// 수정 후 Input.jsx
import styled from "styled-components";
const StyledInput = styled.input`
margin: 35px 0 20px 0;
width: 200px;
height: 30px;
border: none;
border-radius: 5px;
`;
const Input = (props) => {
return (
<StyledInput
style={props.style}
type={props.type}
required={props.required}
value={props.value || ""}
onChange={props.onChange}
placeholder={props.placeholder}
min={props.min}
/>
);
};
export default Input;
// 수정 후 Form.jsx
const Form = () => {
//...생략
return (
<>
<StForm onSubmit={onSubmitHandler}>
<StElementsDiv>
<Input
type={"text"}
required
value={text}
placeholder={"투두를 입력하세요"}
onChange={(e) => setText(e.target.value)}
/>
<Input
style={{ cursor: "pointer" }}
type={"date"}
value={deadLine}
required
min={todaysDate}
onChange={(e) => setDeadLine(e.target.value)}
/>
<Button type={"submit"}>등록</Button>
<Button type={"button"} onClick={onDeleteHandler}>
삭제
</Button>
<Input
type="search"
value={query || ""}
onChange={handleSearch}
placeholder="검색어를 입력하세요"
/>
</StElementsDiv>
</StForm>
<TodoList
checkedItems={checkedItems}
setCheckedItems={setCheckedItems}
query={query}
/>
</>
);
};
export default Form;
Button, Input 컴포넌트를 만들어 주고 그들이 필요로 하는 정보를 props로 넘겨주는 방식으로 구현하여 컴포넌트 재활용하기
3. error 일 때 or 아닐 때 alert 띄우는 것 중복된 코드 제거
// 수정 전 TodoDetail.jsx
const TodoDetail = () => {
// ...생략
useEffect(() => {
if (isLoading) return;
if (detail?.deadLine !== undefined) {
setTimeout(() => {
if (!daysLeft) return;
if (daysLeft && 0 < daysLeft && daysLeft < 4) {
alert(`D-day 까지 ${daysLeft}일 남았습니다`);
} else if (daysLeft === 0) {
alert("D-day입니다");
}
}, 500);
}
}, [detail.deadLine]);
/*--------------------- localStorage ---------------------*/
// ...생략
useEffect(() => {
if (localTodosDetail[0]?.deadLine !== undefined) {
const todaysDate = new Date().toISOString().split("T")[0];
const today = Date.parse(todaysDate);
const selectedDate = Date.parse(localTodosDetail[0]?.deadLine);
const milliSeconds = 24 * 60 * 60 * 1000;
const daysLeft = Math.ceil((selectedDate - today) / milliSeconds);
setTimeout(() => {
if (0 < daysLeft && daysLeft < 4) {
alert(`D-day 까지 ${daysLeft}일 남았습니다`);
} else if (daysLeft === 0) {
alert("D-day입니다");
}
}, 500);
}
}, [localTodosDetail[0]?.deadLine]);
if (error) {
return (
<StDetailDiv>
<StWrapperDiv>
{modal ? (
<EditLocalTodoModal
localTodosDetail={localTodosDetail}
closeModal={closeModal}
/>
) : null}
<div style={{ display: "flex", justifyContent: "space-between" }}>
<StIdDiv>{localTodosDetail[0].id}</StIdDiv>
<StDeadLineDiv>D-Day: {localTodosDetail[0].deadLine}</StDeadLineDiv>
</div>
<StTextDiv>{localTodosDetail[0].text}</StTextDiv>
<StEditButton
onClick={() => {
setModal(true);
}}
>
수정
</StEditButton>
<StGoBackButton
onClick={() => {
navigate("/");
}}
>
이전
</StGoBackButton>
</StWrapperDiv>
</StDetailDiv>
);
}
return (
<>
<StDetailDiv>
<StWrapperDiv>
{modal ? (
<EditTodoModal detail={detail} closeModal={closeModal} />
) : null}
<div style={{ display: "flex", justifyContent: "space-between" }}>
<StIdDiv>{detail.id}</StIdDiv>
<StDeadLineDiv>D-Day: {detail.deadLine}</StDeadLineDiv>
</div>
<StTextDiv>{detail.text}</StTextDiv>
<StEditButton
onClick={() => {
setModal(true);
}}
>
수정
</StEditButton>
<StGoBackButton
onClick={() => {
navigate("/");
}}
>
이전
</StGoBackButton>
</StWrapperDiv>
</StDetailDiv>
</>
);
};
export default TodoDetail;
const TodoDetail = () => {
// ...생략
useEffect(() => {
date.alertfrom3DaysLeft(detail?.deadLine);
if (error?.message === "Network Error") {
date.alertfrom3DaysLeft(localTodosDetail[0]?.deadLine);
}
}, [(error && localTodosDetail[0]?.deadLine) || detail.deadLine]);
return (
<>
<StDetailDiv>
<StWrapperDiv>
{modal ? (
<EditTodoModal
detail={detail}
localTodosDetail={localTodosDetail}
closeModal={closeModal}
error={error}
/>
) : null}
<div style={{ display: "flex", justifyContent: "space-between" }}>
<StIdDiv>{error ? localTodosDetail[0]?.id : detail.id}</StIdDiv>
<StDeadLineDiv>
D-Day: {error ? localTodosDetail[0]?.deadLine : detail.deadLine}
</StDeadLineDiv>
</div>
<StTextDiv>
{error ? localTodosDetail[0]?.text : detail.text}
</StTextDiv>
<StyledButtonsDiv>
<Button
onClick={() => {
setModal(true);
}}
>
수정
</Button>
<Button
onClick={() => {
navigate("/");
}}
>
이전
</Button>
</StyledButtonsDiv>
</StWrapperDiv>
</StDetailDiv>
</>
);
};
export default TodoDetail;
수정 전 컴포넌트는 error일때와 아닐때 UI가 반복되고 useEffect 또한 마찬가지이다. 모두 error일 조건에 따라 다른 데이터를 보여주도록 수정해 주었다.
# 뷰와 비지니스 로직 분리 안됨
작은 프로젝트이기 때문에 비지니스 로직을 한 곳에 모아놓아도 유지 보수는 어찌저찌 되겠지만 이게 프로젝트가 커지면 디버깅이 힘들어지고, 기능이라도 추가 된다면 기존 코드에서 확장하기가 쉽지 않다.
일단 수정 전 코드를 보면 머리가 절로 아프다. 아무리 주석을 달아놔도 보고 싶지 않음. 필요한 로직만 보고싶다는 생각이 마구 든다.
// 수정 전 List.jsx
import styled from "styled-components";
import Todo from "./Todo";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchTodos } from "../redux/modules/todos";
import Pagination from "./Pagenation";
const List = (props) => {
const { todos, error } = useSelector((state) => state.todos);
const query = props.query || "";
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(1);
const [todosPerPage] = useState(5);
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const indexOfLastTodo = currentPage * todosPerPage;
const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
// 키워드 search시 페이지네이션 된 투두를 필터
const filteredTodos =
todos &&
todos.filter((todo) => {
if (query === "") return todos;
const todoo = todo.text || "";
return todoo.toLowerCase().includes(query && query.toLowerCase());
});
// 각 페이지에서 보여질 투두 배열
const currentTodos = filteredTodos?.slice(indexOfFirstTodo, indexOfLastTodo);
// 페이지 나누기
const pageNumber = [];
const totalTodos = todos.length;
for (let i = 1; i <= Math.ceil(totalTodos / todosPerPage); i++) {
pageNumber.push(i);
}
/*--------------------- localStorage ---------------------*/
let localPageNumber = [];
// 로컬스토리지의 투두들을 리스트로 변환
const todosFromLocalStorage = localStorage.getItem("allTodos");
const localTodos = JSON.parse(todosFromLocalStorage);
// 키워드 search시 페이지네이션 된 로컬스토리지 투두 필터
const filteredLocalTodos =
localTodos &&
localTodos.filter((todo) => {
if (query === "") return localTodos;
const todoo = todo.text || "";
return todoo.toLowerCase().includes(query.toLowerCase());
});
// 각 페이지에서 보여질 로컬스토리지의 투두 배열
const currentLocalTodos = filteredLocalTodos?.slice(
indexOfFirstTodo,
indexOfLastTodo
);
const totalLocalTodos = localTodos && localTodos.length;
//pageNumber ( 전체 페이지 수 / 각 페이지 당 포스트 수) 를 계산하여 전체 페이지 번호를 구한 배열
for (let i = 1; i <= Math.ceil(totalLocalTodos / todosPerPage); i++) {
localPageNumber.push(i);
}
useEffect(() => {
dispatch(fetchTodos());
}, []);
if (error) {
return (
<div>
{currentLocalTodos &&
currentLocalTodos.map((todo) => {
return (
<Todo props={props} todo={todo} key={todo.id} error={error} />
);
})}
<StPageNumberUl>
{localPageNumber.map((pageNum) => {
return (
<Pagination
pageNum={pageNum}
key={pageNum}
paginate={paginate}
selected={currentPage}
/>
);
})}
</StPageNumberUl>
</div>
);
}
return (
<>
{currentTodos.map((todo, idx) => {
return <Todo props={props} todo={todo} key={todo.id} idx={idx} />;
})}
<StPageNumberUl>
{pageNumber.map((pageNum) => {
return (
<Pagination
pageNum={pageNum}
key={pageNum}
paginate={paginate}
selected={currentPage}
/>
);
})}
</StPageNumberUl>
</>
);
};
export default List;
const StPageNumberUl = styled.ul`
display: flex;
justify-content: center;
margin-block-start: 0;
margin-block-end: 0;
list-style: none;
padding: 0;
gap: 10px;
padding-top: 20px;
padding-bottom: 20px;
background-color: aliceblue;
`;
일단 모든 컴포넌트들이 저런식으로 작성되어있기 때문에 utils라는 디렉토리에 목적별로 date.js, api.js, page.js, storage.js 를 생성해 주었다. date는 alert창을 띄우기 위하여 날짜를 계산하는 로직이 분리되고, api는 서버에 request하고 데이터를 처리하는 모든 로직들,
page는 pagenation을 위한 로직, storage는 local storage에 데이터를 추가, 수정, 삭제, 불러오는 로직이 분리되었다.
수정 후
// 수정 후 TodoList.jsx
import styled from "styled-components";
import Todo from "./Todo";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchTodos } from "../redux/modules/todos";
import { storage } from "../utils/storage";
import { page } from "../utils/page";
import Pagination from "./Pagination";
const StPageNumberUl = styled.ul`
display: flex;
justify-content: center;
margin-block-start: 0;
margin-block-end: 0;
list-style: none;
padding: 0;
gap: 10px;
padding-top: 20px;
padding-bottom: 20px;
background-color: aliceblue;
`;
const TodoList = (props) => {
const { todos, error } = useSelector((state) => state.todos);
const query = props.query || "";
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(1);
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const localTodos = storage.parseToArray("allTodos");
// 각 페이지에서 보여질 투두 배열
const currentTodos = page.showCurrentTodos(
page.indexOfFirstTodo(currentPage),
page.indexOfLastTodo(currentPage),
error ? localTodos : todos,
query
);
// 페이지 나누기
const pageNumber = [];
page.numberArray(error ? localTodos : todos, pageNumber);
useEffect(() => {
dispatch(fetchTodos());
}, []);
return (
<>
{currentTodos &&
currentTodos.map((todo) => {
return <Todo props={props} todo={todo} key={todo.id} error={error} />;
})}
<StPageNumberUl>
{pageNumber.map((pageNum) => {
return (
<Pagination
pageNum={pageNum}
key={pageNum}
paginate={paginate}
selected={currentPage}
/>
);
})}
</StPageNumberUl>
</>
);
};
export default TodoList;
수정 후의 코드에서 currentTodo라는 변수에 담긴 배열은 local storage의 데이터 혹은 서버의 데이터를 검색어로 filter하고, size 5 로 나누어 각각의 페이지에 표시하게되는 최종적으로 UI에 보여지게 될 배열이다. filter, paginate 하는 과정은 UI를 렌더링하는것에 불 필요한 존재이기 때문에 분리 해주었다.
리팩토링 하면서 느낀점
일단 리팩토링에 앞서 질문을 드렸을 때 친절히 알려주신 모든 분들께 너무나 감사하다... ㅠ____ㅠ
간단한 투두리스트를 만드는 것이라 생각해 디렉토리 구조에 대한 깊은 이해 없이 진행 했던 것이 가장 큰 문제였다고 생각한다. 첫 단추부터 잘못 꿴 셈이기 때문에 이렇게 설계에 대한 계획 없이 만든 프로젝트는 결국 상당히 비효율적으로 돌아가는 프로젝트를 만들게 된다는 것을 깨달았다. 시간과 인력을 낭비하기 때문에 충분한 시간을 가지고 설계를 해야겠다.
중복되는 코드들에 대한 부분은 조금 더 생각을 차근히 했다면 결국 해결 할 수 있었던 부분이었던 것 같다. local storage에 데이터를 저장해서 error일 때는 이렇게 아닐땐 저렇게 보여준다는 것에만 포커스를 맞춰서 진행하다 보니 일단 돌아가게는 만들어야겠다구 생각했던듯...
일단 뷰랑 비지니스로직이 분리되고 중복된 UI를 제거 해주니 코드의 양이 눈에 띄게 줄어서 가독성이 높아졌다. 같은 목적을 가진 로직들이 한 파일에 모이니 이 메소드가 무엇을 위한 것인지 한 눈에 들어오는 것 같다. 공통된 컴포넌트를 분리 하고, 컴포넌트를 캡슐화 하라는게 꼭 필라테스에서 척추를 하나씩 쌓으면서 올라가세요~라는 소리 처럼 들렸었었는데 이제 조금 알 것 같다.
모지란 리팩토링이었지만 또 수정할 부분은 보완하고 필요한 것들을 연습해야겠다.
'공부 정리' 카테고리의 다른 글
22.12.17: TypeScript 특징, 쓰는 이유? (0) | 2022.12.17 |
---|---|
22.12.08: IT 인프라 (0) | 2022.12.08 |
22.12.05: redux-saga 실습 해보기 (0) | 2022.12.05 |
22.11.30: Redux + thunk, saga (0) | 2022.12.02 |
22.11.29: 리액트의 렌더링 최적화 (useCallback, React.memo, useMemo) (0) | 2022.11.29 |