From 3edf3ab44808100c48c05c2eb3a83b17760737e1 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Wed, 10 Apr 2024 20:15:05 +0900 Subject: [PATCH 01/12] use react-hook-form --- Document/State-Management.md | 147 +++++++++++++++++ img/home.png | Bin 2107 -> 0 bytes package-lock.json | 16 ++ package.json | 1 + src/App.tsx | 16 +- src/Atoms.ts | 6 - src/ToDoList.tsx | 20 +++ src/api.ts | 23 --- src/index.tsx | 13 +- src/router.tsx | 20 --- src/routes/Chart.tsx | 86 ---------- src/routes/Coin.tsx | 298 ----------------------------------- src/routes/Coins.tsx | 127 --------------- src/routes/Price.tsx | 97 ------------ src/theme.ts | 9 -- 15 files changed, 195 insertions(+), 684 deletions(-) create mode 100644 Document/State-Management.md delete mode 100644 img/home.png delete mode 100644 src/Atoms.ts create mode 100644 src/ToDoList.tsx delete mode 100644 src/api.ts delete mode 100644 src/router.tsx delete mode 100644 src/routes/Chart.tsx delete mode 100644 src/routes/Coin.tsx delete mode 100644 src/routes/Coins.tsx delete mode 100644 src/routes/Price.tsx diff --git a/Document/State-Management.md b/Document/State-Management.md new file mode 100644 index 0000000..c05bfbc --- /dev/null +++ b/Document/State-Management.md @@ -0,0 +1,147 @@ +아마도 이번에 하는 것도 Crypto-Tracker의 연장선이 될 수 있다. + +# State Management + +## Recoil + +- FackBook사람들이 개발한 state관리 libarary +- 아주 미니멀하고, 간단하다고 한다. + +## state management에 대해서 알아보자 + +먼저! state management가 무엇인지 이해하기 위해 state management가 무엇인지 알아보고, +Recoil을 해보기전에 state management를 사용하지않고, 저번에 만들었던 코인 웹에 기능을 구현해볼 것이다. + +**이것을 왜 알아야 하나?** + +- 사람들이 React에서 state-management를 공부하고 사용해야 된다고 함 (Recoil, Redux) - But? state-management가 왜 필요한지 모름 + > 기술은 문제를 해결하기 위해서 만들어진다. + > 그 기술이 왜 만들어졌는지 모르고 쓰는 건 + > 그 기술의 본질을 모르는 것이다. + +우리는 처음으로 recoil을 사용하지 않고 다크|라이트 모드 체인지 기능을 구현할 것이다. + +### TypeScript Interface 함수 + +props로 함수를 넘어줄 떄는 + +```JS +interface IRouterProps { +  toggleDark: () => void; +} +``` + +이런식으로 타입을 지정해줄 수 있다. + +--- + +- isDark를 Chart까지 넘겨주기위해서 우리는 App -> Router -> Coin -> Chart라는 엄청나게 긴 여정으로 값을 넘겨줄 수 있었다. + - 여기서 다른 값이 더 생길 수도 있고 비효율적이라고 할 수 있다. + - 심지어 App과 Chart를 제외하고는 isDark를 사용하지도 않는다.. +- 이것을 global state라고 한다. + - 어플리케이션 어떤 state를 인지하고 접근해야 할 때 사용한다. + - component가 어디 있는지는 중요하지 않음 +- global state의 예시로는 유저 로그인 상황을 예로 들수 있다. + - 만약에 React-State만을 사용하게 된다면? + - screen과 component가 많아질 수록 더 많은 props가 생기게 되고 너무 귀찮아 질 것이다. + - 그렇기에 State-Management가 필요한 것이다. + +### State-Management를 사용하면 어찌되나? + +지금까지 우리는 완전 계층구조 형식으로 데이터를 전달해주었다. +`IsDark`: App -> Router -> Coin -> Chart 😭 +하지만, State-Management를 사용하게 된다면 어디서든 접근할 수 있게 된다. +App -> (`isDark`) <- Chart + +- isDark를 어딘가에 넣어서 접근한다는 느낌이다. + +이게 Recoil의 **Point**!라고 할 수 있다! + +## Recoil 시작 + +`Recoil`은 위에 계층구조 형식을 해결하기 위해서 만들어졌다고 볼 수 있다. +위에서 isDark를 어딘가에 넣는다고 하였는데 그것을 `Recoil`에서는 **Atom**이라고 부른다. +그 **Atom**에 어떤 value를 저장해서 사용할 수 있는 것이다. + +> Atom은 특정 Component에 종속되어 있지 않는다. + +그리고 **Component**가 **Atom**의 정보를 원한다면? +**Component**를 직접 **Atom**에 연결하면 되는 것이다! + +이러면 좋은점이 value가 필요한 **Component**만 value를 가진다는 것이다! + +굳이 위에서처럼 App -> Router -> Coin -> Chart에서 Router와 Coin에게 쓸모없는 값을 줄 필요가 없다는 것이다. + +## Recoil 사용 해볼까? + +그럼 이제 Recoil을 사용해보자! + +먼저 Recoil을 다운받아준다. +`npm install recoil` + +그리고 react-query를 사용할 때 queryclient로 `index.tsx`를 감싸준 것과 같이 recoil은 recoilroot로 감싸준다. + +```JS +root.render( +  +    +      +    , +); +``` + +이러면 Recoil을 사용할 준비는 끝난 것이다! + +그리고 `Atoms.ts`라는 파일을 만들어주자. + +```JS +export const isDarkAtom = atom() +``` + +우리는 이제 이런식으로 atom을 만들 것이다. + +atom에는 두 가지 요소가 필요한데, +key와 default이다. + +```JS +export const isDarkAtom = atom({ +  key: "isDark", +  default: false, +}); +``` + +이렇게 하면 atom을 사용할 수 있다. + +그럼 이 atom을 어떻게 component와 연결할 수 있을까? + +```JS +const isDark = useRecoilValue(isDarkAtom); +``` + +위와 같이 작성하면 App에 isDarkAtom을 연결한 것이다. + +사용해보니 거쳐가는게 없어서 굉장히 편한 것 같다. + +### Recoil 값 변경 + +Recoil에서 값을 변경하기 위해서는 위에서 useRecoilValue처럼 hook을 사용해야하는데 + +```JS +const setterFn = useSetRecoilState(isDarkAtom); +``` + +이런식으로 `useSetRecoilState`라는 함수를 사용해서 set함수를 받아올 수 있다. + +`useSetRecoilState`로 받은 함수는 React의 setState와 같은 방식으로 동작한다. + +### 요약 + +Recoil에 대해서는 아직 배워야 할 것이 많지만, 짧게 설명해보자면 + +> Recoil은 Atom이라는 파편에게서 component들이 값을 관찰하고, 수정할 수 있다는 것이 핵심 개념이라고 볼 수 있다. + +또한 component가 Atom을 관찰(구독)하였을 때 Atom의 값이 수정된다면, 리렌더링이 된다. + +그래서 이런 Recoil같은 상태관리 라이브러리를 사용하게 된다면! +쓸모없는 행위가 줄고 코드 가독성도 좋아진다. (심지어 Recoil은 이해하기도 쉽다) diff --git a/img/home.png b/img/home.png deleted file mode 100644 index 03b65e4efa45bddf9c69e3c96d0a2294ae3516c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2107 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&w@L)Zt|+Cx8@lv6E*A2M5RPhyD+MT+RZI z$YP-Kry$Ju>{aG^prB-lYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt*){ zJ|V9E|NjRv2MXLz>0AP|TeBp{FPMRmm77OYOh!@3z|hFqB_gt*u&BDTt9!To~#DLx2x9^rVf0`#JZ(rK_Zr*&K%O{J=SG;@r^2>3* z_+2hV()r8lzm**}t=j#)?pOZ%_dDHlqve+y|K9ObY3*J68Fe=f)|zX+y?FiQoqcvi zQ_q$4FXOg$a++ytJ1?!RMeFzE`e$t|Qom<|D5l@%?2Db97Qff|e}Ww{T@1e0_^-p%r-hHWm znp=+DD*_5D=ZpTEsk$(CyL95}kNiSXrK>*y$>P-@eNZgbTYt|d`-ZfN$Rl}`kmm?& za;U6t+17U%$9=7V=DmXAPe^Q-7&<*v)Lh%He*Ch6b=@OVkPokI7GHkZ!MgULt22=I z&;0+M*=@J2F@gmYL?v@=t!K8~J87JM=b^mH6r?~y4nSl!DQdPvTa{_H?A(eBt@ZZlzvg`Nj@3-N6INuBjn;@}~#gW;lYI5s8IoVgwe`kP2C#p)A zO>b*H3vJ(;U*6F2!GAUs&uTIIW^>?e{`+el9RIg?!te{le>=TZz6ktRPy`122Y#VL z_CR^UoLy}HYYHyMXUm%blQxlhV9JdDg{dq4F@6@N3M5~JDFexOVTwTV)3nI~|0JtV z16|OwZ#B@3J^NP!*&=0{K(C*A1{5eUb9ef&d`mYlMxw0!fy{04K+!K{(}2D#ybolW zeAfr6ef3XC@lWu*d&eGKefaC!P1pRS`PYA>tiSe4ec^Gr=W5?73d7YI7-ul}FsLyI zH*g+cNnkP{mCK^Qz{ugiz$DPXK!E9>*>HIF&oj1@r&_ diff --git a/package-lock.json b/package-lock.json index c8e3a43..7b4ad07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-apexcharts": "^1.4.1", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", + "react-hook-form": "^7.51.2", "react-query": "^3.39.3", "react-router-dom": "5.3", "react-scripts": "5.0.1", @@ -15097,6 +15098,21 @@ "react": ">=16.3.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", + "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 1b87b82..760afb6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react-apexcharts": "^1.4.1", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", + "react-hook-form": "^7.51.2", "react-query": "^3.39.3", "react-router-dom": "5.3", "react-scripts": "5.0.1", diff --git a/src/App.tsx b/src/App.tsx index 1fb2588..e944124 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,5 @@ -import styled, { ThemeProvider, createGlobalStyle } from "styled-components"; -import Router from "./router"; -import { ReactQueryDevtools } from "react-query/devtools"; -import { darkTheme, lightTheme } from "./theme"; -import { useRecoilValue } from "recoil"; -import { isDarkAtom } from "./Atoms"; +import styled, { createGlobalStyle } from "styled-components"; +import ToDoList from "./ToDoList"; const GlobalStyle = createGlobalStyle` /* http://meyerweb.com/eric/tools/css/reset/ @@ -78,14 +74,10 @@ a { `; function App() { - const isDark = useRecoilValue(isDarkAtom); return ( <> - - - - - + + ); } diff --git a/src/Atoms.ts b/src/Atoms.ts deleted file mode 100644 index c7ec93a..0000000 --- a/src/Atoms.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from "recoil"; - -export const isDarkAtom = atom({ - key: "isDark", - default: true, -}); diff --git a/src/ToDoList.tsx b/src/ToDoList.tsx new file mode 100644 index 0000000..8e93ee8 --- /dev/null +++ b/src/ToDoList.tsx @@ -0,0 +1,20 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; + +function ToDoList() { + const { register, watch } = useForm(); + console.log(watch()); + return ( +
+
+ + + + + +
+
+ ); +} + +export default ToDoList; diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index 245d5b5..0000000 --- a/src/api.ts +++ /dev/null @@ -1,23 +0,0 @@ -const BASE_URL = `https://api.coinpaprika.com/v1`; - -export function fetchCoins() { - return fetch(`${BASE_URL}/coins`).then((response) => response.json()); -} - -export function fetchCoinInfo(coinId: string) { - return fetch(`${BASE_URL}/coins/${coinId}`).then((response) => - response.json(), - ); -} - -export function fetchCoinTickers(coinId: string) { - return fetch(`${BASE_URL}/tickers/${coinId}`).then((response) => - response.json(), - ); -} - -export function fetchCoinHistory(coinId: string) { - return fetch( - `https://ohlcv-api.nomadcoders.workers.dev?coinId=${coinId}`, - ).then((response) => response.json()); -} diff --git a/src/index.tsx b/src/index.tsx index b14c58b..d6acf67 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,18 +1,19 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; -import { QueryClient, QueryClientProvider } from "react-query"; import { RecoilRoot } from "recoil"; - -const queryClient = new QueryClient(); +import { ThemeProvider } from "styled-components"; +import { darkTheme } from "./theme"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement, ); root.render( - - - + + + + + , ); diff --git a/src/router.tsx b/src/router.tsx deleted file mode 100644 index a51f4b1..0000000 --- a/src/router.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { BrowserRouter, Route, Switch } from "react-router-dom"; -import Coins from "./routes/Coins"; -import Coin from "./routes/Coin"; - -function Router() { - return ( - - - - - - - - - - - ); -} - -export default Router; diff --git a/src/routes/Chart.tsx b/src/routes/Chart.tsx deleted file mode 100644 index 1aed5cf..0000000 --- a/src/routes/Chart.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from "react"; -import { useQuery } from "react-query"; -import { fetchCoinHistory } from "../api"; -import ReactApexChart from "react-apexcharts"; -import { isDarkAtom } from "../Atoms"; -import { useRecoilValue } from "recoil"; - -interface ChartProps { - coinId: string; -} - -interface IHistorical { - time_open: string; - time_close: string; - open: number; - high: number; - low: number; - close: number; - volume: number; - market_cap: number; -} - -export default function Chart({ coinId }: ChartProps) { - const isDark = useRecoilValue(isDarkAtom); - const { isLoading, data } = useQuery(["ohlcv", coinId], () => - fetchCoinHistory(coinId), - ); - const isError = !Array.isArray(data); - return ( -
- {isLoading ? ( - "Loading Chart" - ) : isError ? ( -

값이 존재하지 않습니다..😭

- ) : ( - Number(price.close)) ?? [], - }, - ]} - options={{ - theme: { - mode: isDark ? "dark" : "light", - }, - chart: { - height: 400, - width: 500, - toolbar: { - show: false, - }, - background: "transparent", - }, - grid: { show: false }, - stroke: { - width: 5, - }, - yaxis: { show: false }, - xaxis: { - labels: { show: false }, - axisTicks: { show: false }, - axisBorder: { show: false }, - type: "datetime", - categories: - data?.map((price) => - new Date(Number(price.time_close) * 1000).toUTCString(), - ) ?? [], - }, - fill: { - type: "gradient", - gradient: { gradientToColors: ["#0be881"], stops: [0, 100] }, - }, - colors: ["#0fbcf9"], - tooltip: { - y: { - formatter: (value) => `$ ${value.toFixed(2)}`, - }, - }, - }} - /> - )} -
- ); -} diff --git a/src/routes/Coin.tsx b/src/routes/Coin.tsx deleted file mode 100644 index aad2e5c..0000000 --- a/src/routes/Coin.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Helmet } from "react-helmet"; -import { - Link, - Route, - Switch, - useLocation, - useParams, - useRouteMatch, -} from "react-router-dom"; -import { styled } from "styled-components"; -import Price from "./Price"; -import Chart from "./Chart"; -import { useQuery } from "react-query"; -import { fetchCoinInfo, fetchCoinTickers } from "../api"; - -const Container = styled.div` - width: 100%; - height: 100%; - padding: 0px 20px; - max-width: 440px; - margin: 0 auto; -`; - -const Header = styled.header` - width: 100%; - height: 15vh; - display: flex; - align-items: center; - margin: 20px 0px; -`; - -const BackHome = styled.div` - width: 30%; - height: 50%; - display: flex; - align-items: center; - a { - width: 35px; - height: 35px; - border-radius: 10px; - } -`; - -const Title = styled.h1` - display: flex; - align-items: center; - width: 70%; - height: 50%; - font-size: 46px; - font-weight: 700; - color: ${(props) => props.theme.accentColor}; -`; - -const Loader = styled.h1` - font-size: 48px; - height: 60vh; - display: flex; - justify-content: center; - align-items: center; -`; - -const Overview = styled.div` - display: flex; - justify-content: space-around; - background-color: ${(props) => props.theme.cardBgColor}; - padding: 10px 20px; - margin: 15px 0px; - border-radius: 10px; - box-shadow: 0px 0px 5px 0px white; - border: 1px solid ${(props) => props.theme.textColor}; -`; -const OverviewItem = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - span:first-child { - font-size: 10px; - font-weight: 400; - text-transform: uppercase; - margin-bottom: 5px; - } -`; -const Description = styled.p` - margin: 15px 0px; - background-color: ${(props) => props.theme.cardBgColor}; - padding: 20px 20px; - border-radius: 10px; - box-shadow: 0px 0px 5px 0px white; - border: 1px solid ${(props) => props.theme.textColor}; -`; - -const Tabs = styled.div` - display: grid; - grid-template-columns: repeat(2, 1fr); - margin: 25px 0px; - gap: 10px; -`; - -const Tab = styled.span<{ $isActive: boolean }>` - text-align: center; - text-transform: uppercase; - font-size: 18px; - font-weight: 400; - /* background-color: rgba(0, 0, 0, 0.5); */ - padding: 14px 0px; - border-radius: 10px; - color: ${(props) => - props.$isActive ? props.theme.accentColor : props.theme.textColor}; - a { - position: relative; - display: block; - &::after { - content: ""; - position: absolute; - height: 2px; - bottom: -10px; - left: 87px; - width: 20px; - border-radius: 1px; - background-color: ${(props) => - props.$isActive ? props.theme.accentColor : props.theme.textColor}; - transition: background-color 0.3s ease 0s; - } - } -`; - -interface RouteParams { - coinId: string; -} - -interface RouteStates { - name: string; -} - -interface InfoData { - id: string; - name: string; - symbol: string; - rank: number; - is_new: boolean; - is_active: boolean; - type: string; - logo: string; - description: string; - message: string; - open_source: boolean; - started_at: string; - development_status: string; - hardware_wallet: boolean; - proof_type: string; - org_structure: string; - hash_algorithm: string; - first_data_at: string; - last_data_at: string; -} - -interface PriceData { - id: string; - name: string; - symbol: string; - rank: number; - total_supply: number; - max_supply: number; - beta_value: number; - first_data_at: string; - last_updated: string; - quotes: { - USD: { - ath_date: string; - ath_price: number; - market_cap: number; - market_cap_change_24h: number; - percent_change_1h: number; - percent_change_1y: number; - percent_change_6h: number; - percent_change_7d: number; - percent_change_12h: number; - percent_change_15m: number; - percent_change_24h: number; - percent_change_30d: number; - percent_change_30m: number; - percent_from_price_ath: number; - price: number; - volume_24h: number; - volume_24h_change_24h: number; - }; - }; -} - -function Coin() { - const { coinId } = useParams(); - const { state } = useLocation(); - const priceMatch = useRouteMatch("/:coinId/price"); - const chartMatch = useRouteMatch("/:coinId/chart"); - const { isLoading: infoLoading, data: infoData } = useQuery( - ["info", coinId], - () => fetchCoinInfo(coinId), - ); - const { isLoading: tickersLoading, data: tickerData } = useQuery( - ["tickers", coinId], - () => fetchCoinTickers(coinId), - ); - - const loading = infoLoading && tickersLoading; - return ( - - - - {state?.name - ? state.name - : loading - ? "코인 로딩중..." - : infoData?.name} - - -
- - - - - - - - - {state?.name - ? state.name - : loading - ? "코인 로딩중..." - : infoData?.name} - -
- {loading ? ( - 😫loading😫 - ) : ( - <> - - - 심볼 - ${infoData?.symbol} - - - 순위 - {infoData?.rank} - - - 가격 - {tickerData?.quotes.USD.price.toFixed(2)} - - - - - 총량 - {tickerData?.total_supply} - - - 최대 발행량 - {tickerData?.max_supply} - - - {infoData?.description} - - - - Chart - - - Price - - - - - - - - - - - - - )} -
- ); -} - -export default Coin; diff --git a/src/routes/Coins.tsx b/src/routes/Coins.tsx deleted file mode 100644 index a5536dc..0000000 --- a/src/routes/Coins.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useQueries, useQuery } from "react-query"; -import { Link } from "react-router-dom"; -import { styled } from "styled-components"; -import { fetchCoins } from "../api"; -import { Helmet } from "react-helmet"; -import { useRecoilValue, useSetRecoilState } from "recoil"; -import { isDarkAtom } from "../Atoms"; - -const Container = styled.div` - padding: 0px 20px; - max-width: 440px; - margin: 0 auto; -`; - -const Header = styled.header` - height: 15vh; - display: flex; - justify-content: center; - align-items: center; - margin: 20px 0px; -`; - -const CoinsList = styled.ul``; - -const Coin = styled.li` - background-color: ${(props) => props.theme.cardBgColor}; - color: ${(props) => props.theme.textColor}; - border-radius: 15px; - margin-bottom: 10px; - border: 1px solid ${(props) => props.theme.textColor}; - a { - padding: 20px; - transition: color 0.2s ease-in; - display: flex; - align-items: center; - } - &:hover { - a { - color: ${(props) => props.theme.accentColor}; - } - } -`; - -const Loader = styled.h1` - font-size: 48px; - height: 60vh; - display: flex; - justify-content: center; - align-items: center; -`; - -const Title = styled.h1` - font-size: 48px; - font-weight: 700; - color: ${(props) => props.theme.accentColor}; -`; - -const Img = styled.img` - width: 30px; - height: 30px; - margin-right: 20px; -`; - -const ToggleBtn = styled.button` - margin-left: 20px; - border: 3px solid ${(props) => props.theme.accentColor}; - border-radius: 50%; - padding: 10px 2.5px; - background-color: ${(props) => props.theme.toggleBgColor}; - color: ${(props) => props.theme.toggleTextColor}; - font-weight: 800s; -`; - -interface ICoin { - id: string; - name: string; - symbol: string; - rank: number; - is_new: boolean; - is_active: boolean; - type: string; -} - -function Coins() { - const isDark = useRecoilValue(isDarkAtom); - const setDarkAtom = useSetRecoilState(isDarkAtom); - const toggleDarkAtom = () => setDarkAtom((prev) => !prev); - const { isLoading, data } = useQuery("allCoins", fetchCoins); - return ( - - - Coins - -
- 코인 - - {isDark ? "white" : "black"} - -
- {isLoading ? ( - 😫loading😫 - ) : ( - - {data?.slice(0, 100).map((coin) => ( - - - - {coin.name} → - - - ))} - - )} -
- ); -} - -export default Coins; diff --git a/src/routes/Price.tsx b/src/routes/Price.tsx deleted file mode 100644 index d90a5bb..0000000 --- a/src/routes/Price.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react"; -import { useParams } from "react-router-dom"; -import { fetchCoinTickers } from "../api"; -import { useQuery } from "react-query"; -import { styled } from "styled-components"; - -const Container = styled.div``; - -const HighBox = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - background-color: rgba(0, 0, 0, 0.5); -`; - -const TextBox = styled.span` - margin-left: 10px; - color: white; - opacity: 0.5; - line-height: 30px; -`; - -const PriceBox = styled.h1` - margin-right: 10px; - font-size: 28px; - text-shadow: white 1px 0 3px; -`; - -interface RouteParams { - coinId: string; -} - -interface PriceData { - id: string; - name: string; - symbol: string; - rank: number; - total_supply: number; - max_supply: number; - beta_value: number; - first_data_at: string; - last_updated: string; - quotes: { - USD: { - ath_date: string; - ath_price: number; - market_cap: number; - market_cap_change_24h: number; - percent_change_1h: number; - percent_change_1y: number; - percent_change_6h: number; - percent_change_7d: number; - percent_change_12h: number; - percent_change_15m: number; - percent_change_24h: number; - percent_change_30d: number; - percent_change_30m: number; - percent_from_price_ath: number; - price: number; - volume_24h: number; - volume_24h_change_24h: number; - }; - }; -} - -export default function Price() { - const { coinId } = useParams(); - const { isLoading, data } = useQuery(["tickers", coinId], () => - fetchCoinTickers(coinId), - ); - let date: string = ""; - isLoading ? console.log("로딩중") : (date = data?.quotes.USD.ath_date as ""); - //2024-03-14T07:07:09Z - date = `${date.substring(0, 4)}. ${date.substring(5, 7)}. ${date.substring( - 8, - 10, - )}. ${date.substring(11, 13)}:${date.substring(14, 16)}:${date.substring( - 17, - 19, - )}`; - return ( - - {isLoading ? ( - 로딩중..😎 - ) : ( - - - {date} -
최고가 달성 -
- ${data?.quotes.USD.ath_price.toFixed(3)} -
- )} -
- ); -} diff --git a/src/theme.ts b/src/theme.ts index 39232cf..9951023 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -8,12 +8,3 @@ export const darkTheme: DefaultTheme = { toggleBgColor: "white", toggleTextColor: "black", }; - -export const lightTheme: DefaultTheme = { - bgColor: "whitesmoke", - textColor: "black", - accentColor: "#9c88ff", - cardBgColor: "white", - toggleBgColor: "black", - toggleTextColor: "white", -}; From c6d41660f2b4ecb8e927b0ca31f32b3542209493 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Wed, 10 Apr 2024 22:03:55 +0900 Subject: [PATCH 02/12] use hook in react-hook-form --- src/ToDoList.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/ToDoList.tsx b/src/ToDoList.tsx index 8e93ee8..653aa69 100644 --- a/src/ToDoList.tsx +++ b/src/ToDoList.tsx @@ -2,15 +2,24 @@ import React, { useState } from "react"; import { useForm } from "react-hook-form"; function ToDoList() { - const { register, watch } = useForm(); - console.log(watch()); + const { register, handleSubmit, formState } = useForm(); + console.log(formState.errors); + + const onValid = (data: any) => { + console.log(data); + }; return (
-
- - - - + + + + +
From aa082e4894826dee4a6afbca39edcc21269129d5 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Thu, 11 Apr 2024 16:06:19 +0900 Subject: [PATCH 03/12] use various function in react-hook-form --- src/ToDoList.tsx | 68 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/src/ToDoList.tsx b/src/ToDoList.tsx index 653aa69..5994543 100644 --- a/src/ToDoList.tsx +++ b/src/ToDoList.tsx @@ -1,12 +1,37 @@ import React, { useState } from "react"; import { useForm } from "react-hook-form"; +import { styled } from "styled-components"; + +const Span = styled.span` + color: red; +`; + +interface FormType { + userName: string; + email: string; + password: string; + checkPassword: string; + extraError: string; +} function ToDoList() { - const { register, handleSubmit, formState } = useForm(); - console.log(formState.errors); + const { + register, + handleSubmit, + formState: { errors }, + setError, + } = useForm({ + defaultValues: { + email: "@naver.com", + }, + }); + console.log(errors); - const onValid = (data: any) => { - console.log(data); + const onValid = (data: FormType) => { + if (data.password !== data.checkPassword) { + return setError("checkPassword", { message: "password가 다릅니다." }, { shouldFocus: true }); + } + //setError("extraError", { message: "서버 오프라인" }); }; return (
@@ -14,13 +39,42 @@ function ToDoList() { + value.includes("200won") ? "아닛 200원을 포함하고 있다닛!!!" : true, + noYejun: (value) => + value.includes("Yejun") ? "아닛 Yejun을 포함하고 있다닛!!!" : true, + noSeunghun: (value) => + value.includes("Seunghun") ? "아닛 Seunghun을 포함하고 있다닛!!!" : true, + }, })} placeholder="userName" /> - - - + {errors?.userName?.message} + + {errors?.email?.message} + + {errors?.password?.message} + + {errors?.checkPassword?.message} + {errors?.extraError?.message}
); From d5fe37ca02d89651950edabed2305d7c04b0b9b4 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Thu, 11 Apr 2024 20:37:25 +0900 Subject: [PATCH 04/12] Make ToDoList and Refectoring ToDoList --- src/App.tsx | 2 +- src/ToDoList.tsx | 83 ----------------------------------- src/atoms.ts | 12 +++++ src/components/CreateToDo.tsx | 29 ++++++++++++ src/components/ToDo.tsx | 13 ++++++ src/components/ToDoList.tsx | 33 ++++++++++++++ 6 files changed, 88 insertions(+), 84 deletions(-) delete mode 100644 src/ToDoList.tsx create mode 100644 src/atoms.ts create mode 100644 src/components/CreateToDo.tsx create mode 100644 src/components/ToDo.tsx create mode 100644 src/components/ToDoList.tsx diff --git a/src/App.tsx b/src/App.tsx index e944124..d41a4f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import styled, { createGlobalStyle } from "styled-components"; -import ToDoList from "./ToDoList"; +import ToDoList from "./components/ToDoList"; const GlobalStyle = createGlobalStyle` /* http://meyerweb.com/eric/tools/css/reset/ diff --git a/src/ToDoList.tsx b/src/ToDoList.tsx deleted file mode 100644 index 5994543..0000000 --- a/src/ToDoList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from "react"; -import { useForm } from "react-hook-form"; -import { styled } from "styled-components"; - -const Span = styled.span` - color: red; -`; - -interface FormType { - userName: string; - email: string; - password: string; - checkPassword: string; - extraError: string; -} - -function ToDoList() { - const { - register, - handleSubmit, - formState: { errors }, - setError, - } = useForm({ - defaultValues: { - email: "@naver.com", - }, - }); - console.log(errors); - - const onValid = (data: FormType) => { - if (data.password !== data.checkPassword) { - return setError("checkPassword", { message: "password가 다릅니다." }, { shouldFocus: true }); - } - //setError("extraError", { message: "서버 오프라인" }); - }; - return ( -
-
- - value.includes("200won") ? "아닛 200원을 포함하고 있다닛!!!" : true, - noYejun: (value) => - value.includes("Yejun") ? "아닛 Yejun을 포함하고 있다닛!!!" : true, - noSeunghun: (value) => - value.includes("Seunghun") ? "아닛 Seunghun을 포함하고 있다닛!!!" : true, - }, - })} - placeholder="userName" - /> - {errors?.userName?.message} - - {errors?.email?.message} - - {errors?.password?.message} - - {errors?.checkPassword?.message} - - {errors?.extraError?.message} -
-
- ); -} - -export default ToDoList; diff --git a/src/atoms.ts b/src/atoms.ts new file mode 100644 index 0000000..3692a7a --- /dev/null +++ b/src/atoms.ts @@ -0,0 +1,12 @@ +import { atom } from "recoil"; + +export interface IToDo { + text: string; + id: number; + category: "TO_DO" | "DOING" | "DONE"; +} + +export const toDoState = atom({ + key: "toDo", + default: [], +}); diff --git a/src/components/CreateToDo.tsx b/src/components/CreateToDo.tsx new file mode 100644 index 0000000..a62dadc --- /dev/null +++ b/src/components/CreateToDo.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { useSetRecoilState } from "recoil"; +import { toDoState } from "../atoms"; + +interface IForm { + toDo: string; +} + +function CreateToDo() { + const setToDos = useSetRecoilState(toDoState); + const { register, handleSubmit, setValue } = useForm(); + const onValid = ({ toDo }: IForm) => { + console.log("add to do", toDo); + setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: "TO_DO" }, ...oldToDos]); + setValue("toDo", ""); + }; + return ( +
+ + +
+ ); +} + +export default CreateToDo; diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx new file mode 100644 index 0000000..78bb360 --- /dev/null +++ b/src/components/ToDo.tsx @@ -0,0 +1,13 @@ +import { IToDo, toDoState } from "../atoms"; + +function ToDo({ text }: IToDo) { + return ( +
  • + {text} + + + +
  • + ); +} +export default ToDo; diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx new file mode 100644 index 0000000..562b19b --- /dev/null +++ b/src/components/ToDoList.tsx @@ -0,0 +1,33 @@ +import { useRecoilValue } from "recoil"; +import CreateToDo from "./CreateToDo"; +import ToDo from "./ToDo"; +import { toDoState } from "../atoms"; +import { styled } from "styled-components"; + +const ToDoUL = styled.ul` + list-style: square; + margin: 5px 20px; +`; + +function ToDoList() { + const toDos = useRecoilValue(toDoState); + return ( +
    +

    투두 리스트!

    +
    + + + {toDos.map((toDo) => ( + + ))} + + +
  • 123
  • +
  • 123
  • +
  • 123
  • +
    +
    + ); +} + +export default ToDoList; From 8c355efb63cfaf33c018ae8b36ba40f60aa8cbf6 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Mon, 15 Apr 2024 08:11:01 +0900 Subject: [PATCH 05/12] category --- src/components/ToDo.tsx | 36 +++++++++++++++++++++++++++++++----- src/components/ToDoList.tsx | 5 ----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx index 78bb360..97f6923 100644 --- a/src/components/ToDo.tsx +++ b/src/components/ToDo.tsx @@ -1,12 +1,38 @@ +import styled from "styled-components"; import { IToDo, toDoState } from "../atoms"; +import React from "react"; +import { useSetRecoilState } from "recoil"; -function ToDo({ text }: IToDo) { +const TextSpan = styled.span` + margin-right: 10px; +`; + +function ToDo({ text, category }: IToDo) { + const setToDos = useSetRecoilState(toDoState); + const onClick = (event: React.MouseEvent) => { + const { + currentTarget: { name }, + } = event; + console.log(name); + }; return (
  • - {text} - - - + {text} + {category !== "TO_DO" && ( + + )} + {category !== "DOING" && ( + + )} + {category !== "DONE" && ( + + )}
  • ); } diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx index 562b19b..9b5f047 100644 --- a/src/components/ToDoList.tsx +++ b/src/components/ToDoList.tsx @@ -21,11 +21,6 @@ function ToDoList() { ))} - -
  • 123
  • -
  • 123
  • -
  • 123
  • -
    ); } From 6388cbea6a2dc44b9796947246bf073adfa8a4df Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Mon, 15 Apr 2024 08:33:13 +0900 Subject: [PATCH 06/12] use findIndex Func --- src/components/ToDo.tsx | 9 +++++++-- src/components/ToDoList.tsx | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx index 97f6923..5886b1b 100644 --- a/src/components/ToDo.tsx +++ b/src/components/ToDo.tsx @@ -7,13 +7,18 @@ const TextSpan = styled.span` margin-right: 10px; `; -function ToDo({ text, category }: IToDo) { +function ToDo({ text, category, id }: IToDo) { const setToDos = useSetRecoilState(toDoState); const onClick = (event: React.MouseEvent) => { const { currentTarget: { name }, } = event; - console.log(name); + setToDos((oldToDos) => { + const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); + const oldToDo = oldToDos[findIndex]; + const newToDo = { text, id, category: name }; + return oldToDos; + }); }; return (
  • diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx index 9b5f047..d38136e 100644 --- a/src/components/ToDoList.tsx +++ b/src/components/ToDoList.tsx @@ -11,6 +11,7 @@ const ToDoUL = styled.ul` function ToDoList() { const toDos = useRecoilValue(toDoState); + console.log(toDos); return (

    투두 리스트!

    From 090c840d017193416934604e06bcf49888e0de2d Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Mon, 15 Apr 2024 15:58:03 +0900 Subject: [PATCH 07/12] change design and complete change category func --- .vscode/settings.json | 3 ++ src/App.tsx | 2 ++ src/components/CreateToDo.tsx | 42 ++++++++++++++++++++++++--- src/components/ToDo.tsx | 31 +++++++++++++------- src/components/ToDoList.tsx | 53 +++++++++++++++++++++++++++++------ src/index.css | 4 +++ src/index.tsx | 5 ++-- 7 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/index.css diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d041488 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["ULBG"] +} diff --git a/src/App.tsx b/src/App.tsx index d41a4f8..300b7e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,6 +61,8 @@ table { } body { /* font-size: 100px; */ + width: 100vw; + height: 100vh; font-family: "Noto Sans KR", sans-serif; background-color: ${(props) => props.theme.bgColor}; color: ${(props) => props.theme.textColor}; diff --git a/src/components/CreateToDo.tsx b/src/components/CreateToDo.tsx index a62dadc..db0e02c 100644 --- a/src/components/CreateToDo.tsx +++ b/src/components/CreateToDo.tsx @@ -2,6 +2,40 @@ import React from "react"; import { useForm } from "react-hook-form"; import { useSetRecoilState } from "recoil"; import { toDoState } from "../atoms"; +import { styled } from "styled-components"; + +const ToDoInput = styled.input` + width: 85%; + height: 35px; + padding: 5px 1px; + border-radius: 0px; + outline: none; + border: none; + background-color: inherit; + color: white; + font-weight: 500; + border-bottom: 2px solid gray; + transition: all 0.5s ease-in-out; + &:focus { + border-bottom: 2px solid white; + } +`; + +const ToDoBtn = styled.button` + width: 12%; + height: 35px; + font-size: 12px; + font-weight: bold; + background-color: #52ac81; + border: 2px solid gray; +`; + +const ToDoForm = styled.form` + width: 100%; + height: 35px; + display: flex; + justify-content: space-between; +`; interface IForm { toDo: string; @@ -16,13 +50,13 @@ function CreateToDo() { setValue("toDo", ""); }; return ( -
    - + - - + 추가 + ); } diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx index 5886b1b..d41e9cf 100644 --- a/src/components/ToDo.tsx +++ b/src/components/ToDo.tsx @@ -7,6 +7,17 @@ const TextSpan = styled.span` margin-right: 10px; `; +const CategoryBtn = styled.button` + background-color: #434341; + color: white; + border: 2px solid gray; + margin-right: 5px; +`; + +const ToDoList = styled.li` + /* margin-bottom: 5px; */ +`; + function ToDo({ text, category, id }: IToDo) { const setToDos = useSetRecoilState(toDoState); const onClick = (event: React.MouseEvent) => { @@ -16,29 +27,29 @@ function ToDo({ text, category, id }: IToDo) { setToDos((oldToDos) => { const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); const oldToDo = oldToDos[findIndex]; - const newToDo = { text, id, category: name }; - return oldToDos; + const newToDo = { text, id, category: name as IToDo["category"] }; + return [...oldToDos.slice(0, findIndex), newToDo, ...oldToDos.slice(findIndex + 1)]; }); }; return ( -
  • + {text} {category !== "TO_DO" && ( - + )} {category !== "DOING" && ( - + )} {category !== "DONE" && ( - + )} -
  • +
    ); } export default ToDo; diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx index d38136e..04accfd 100644 --- a/src/components/ToDoList.tsx +++ b/src/components/ToDoList.tsx @@ -7,22 +7,57 @@ import { styled } from "styled-components"; const ToDoUL = styled.ul` list-style: square; margin: 5px 20px; + display: flex; + flex-direction: column; +`; + +const ULBG = styled.div` + background-color: rgba(1, 1, 1, 0.2); + border: 2px solid rgba(255, 255, 255, 0.3); + padding: 10px; + height: 100%; + overflow-y: scroll; +`; + +const Title = styled.h1` + font-size: 36px; + font-weight: bold; +`; + +const Main = styled.div` + width: 100%; + height: 87%; + display: flex; + flex-direction: column; + gap: 10px; +`; + +const ToDoSection = styled.div` + padding: 35px 35px 0px 35px; + border: 2px solid white; + width: 40%; + height: 100%; + margin: 0 auto; `; function ToDoList() { const toDos = useRecoilValue(toDoState); console.log(toDos); return ( -
    -

    투두 리스트!

    + + 투두 리스트!
    - - - {toDos.map((toDo) => ( - - ))} - -
    +
    + + + + {toDos.map((toDo) => ( + + ))} + + +
    + ); } diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..7a92389 --- /dev/null +++ b/src/index.css @@ -0,0 +1,4 @@ +#root { + width: 100%; + height: 100%; +} diff --git a/src/index.tsx b/src/index.tsx index d6acf67..77d8b65 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,10 +4,9 @@ import App from "./App"; import { RecoilRoot } from "recoil"; import { ThemeProvider } from "styled-components"; import { darkTheme } from "./theme"; +import "./index.css"; -const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement, -); +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( From e18320e0125198651a06a5783d7be25c8ee0403e Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Tue, 16 Apr 2024 17:17:25 +0900 Subject: [PATCH 08/12] use selector in recoil --- src/atoms.ts | 16 +++++++++++++++- src/components/ToDoList.tsx | 32 +++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/atoms.ts b/src/atoms.ts index 3692a7a..6dbd96e 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -1,4 +1,4 @@ -import { atom } from "recoil"; +import { atom, selector } from "recoil"; export interface IToDo { text: string; @@ -6,7 +6,21 @@ export interface IToDo { category: "TO_DO" | "DOING" | "DONE"; } +export const categoryState = atom({ + key: "category", + default: "TO_DO", +}); + export const toDoState = atom({ key: "toDo", default: [], }); + +export const toDoSelector = selector({ + key: "toDoSelector", + get: ({ get }) => { + const toDos = get(toDoState); + const category = get(categoryState); + return toDos.filter((toDo) => toDo.category === category); + }, +}); diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx index 04accfd..52f090a 100644 --- a/src/components/ToDoList.tsx +++ b/src/components/ToDoList.tsx @@ -1,8 +1,9 @@ -import { useRecoilValue } from "recoil"; +import { useRecoilState, useRecoilValue } from "recoil"; import CreateToDo from "./CreateToDo"; import ToDo from "./ToDo"; -import { toDoState } from "../atoms"; +import { categoryState, toDoSelector, toDoState } from "../atoms"; import { styled } from "styled-components"; +import React from "react"; const ToDoUL = styled.ul` list-style: square; @@ -40,18 +41,39 @@ const ToDoSection = styled.div` margin: 0 auto; `; +const ToDoSelect = styled.select` + padding: 10px 0px; + background-color: inherit; + color: white; + border: 1px solid white; + border-radius: 4px; + outline: none; + option { + background-color: #2f3640; + padding: 3px 0; + } +`; + function ToDoList() { - const toDos = useRecoilValue(toDoState); - console.log(toDos); + const toDos = useRecoilValue(toDoSelector); + const [category, setCategory] = useRecoilState(categoryState); + const onInput = (event: React.FormEvent) => { + setCategory(event.currentTarget.value); + }; return ( 투두 리스트!
    + + + + + - {toDos.map((toDo) => ( + {toDos?.map((toDo) => ( ))} From b5cb2c34cb564faa261b530c1a24f15798d6da58 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Tue, 16 Apr 2024 17:41:10 +0900 Subject: [PATCH 09/12] use enum --- src/atoms.ts | 12 +++++++++--- src/components/CreateToDo.tsx | 7 ++++--- src/components/ToDo.tsx | 16 ++++++++-------- src/components/ToDoList.tsx | 10 +++++----- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/atoms.ts b/src/atoms.ts index 6dbd96e..5f011aa 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -1,14 +1,20 @@ import { atom, selector } from "recoil"; +export const enum Categories { + "TO_DO" = "TO_DO", + "DOING" = "DOING", + "DONE" = "DONE", +} + export interface IToDo { text: string; id: number; - category: "TO_DO" | "DOING" | "DONE"; + category: Categories; } -export const categoryState = atom({ +export const categoryState = atom({ key: "category", - default: "TO_DO", + default: Categories.TO_DO, }); export const toDoState = atom({ diff --git a/src/components/CreateToDo.tsx b/src/components/CreateToDo.tsx index db0e02c..467ec78 100644 --- a/src/components/CreateToDo.tsx +++ b/src/components/CreateToDo.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useForm } from "react-hook-form"; -import { useSetRecoilState } from "recoil"; -import { toDoState } from "../atoms"; +import { useRecoilValue, useSetRecoilState } from "recoil"; +import { categoryState, toDoState } from "../atoms"; import { styled } from "styled-components"; const ToDoInput = styled.input` @@ -43,10 +43,11 @@ interface IForm { function CreateToDo() { const setToDos = useSetRecoilState(toDoState); + const category = useRecoilValue(categoryState); const { register, handleSubmit, setValue } = useForm(); const onValid = ({ toDo }: IForm) => { console.log("add to do", toDo); - setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: "TO_DO" }, ...oldToDos]); + setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: category }, ...oldToDos]); setValue("toDo", ""); }; return ( diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx index d41e9cf..67faf9b 100644 --- a/src/components/ToDo.tsx +++ b/src/components/ToDo.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { IToDo, toDoState } from "../atoms"; +import { Categories, IToDo, toDoState } from "../atoms"; import React from "react"; import { useSetRecoilState } from "recoil"; @@ -27,25 +27,25 @@ function ToDo({ text, category, id }: IToDo) { setToDos((oldToDos) => { const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); const oldToDo = oldToDos[findIndex]; - const newToDo = { text, id, category: name as IToDo["category"] }; + const newToDo = { text, id, category: name as Categories }; return [...oldToDos.slice(0, findIndex), newToDo, ...oldToDos.slice(findIndex + 1)]; }); }; return ( {text} - {category !== "TO_DO" && ( - + {category !== Categories.TO_DO && ( + To Do )} - {category !== "DOING" && ( - + {category !== Categories.DOING && ( + Doing )} - {category !== "DONE" && ( - + {category !== Categories.DONE && ( + Done )} diff --git a/src/components/ToDoList.tsx b/src/components/ToDoList.tsx index 52f090a..7b78deb 100644 --- a/src/components/ToDoList.tsx +++ b/src/components/ToDoList.tsx @@ -1,7 +1,7 @@ import { useRecoilState, useRecoilValue } from "recoil"; import CreateToDo from "./CreateToDo"; import ToDo from "./ToDo"; -import { categoryState, toDoSelector, toDoState } from "../atoms"; +import { Categories, IToDo, categoryState, toDoSelector, toDoState } from "../atoms"; import { styled } from "styled-components"; import React from "react"; @@ -58,7 +58,7 @@ function ToDoList() { const toDos = useRecoilValue(toDoSelector); const [category, setCategory] = useRecoilState(categoryState); const onInput = (event: React.FormEvent) => { - setCategory(event.currentTarget.value); + setCategory(event.currentTarget.value as Categories); }; return ( @@ -66,9 +66,9 @@ function ToDoList() {
    - - - + + + From 6c4910617154e1630d02bc2ea387a54988e47415 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Tue, 16 Apr 2024 17:58:07 +0900 Subject: [PATCH 10/12] feat: remove func --- src/components/CreateToDo.tsx | 1 - src/components/ToDo.tsx | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/CreateToDo.tsx b/src/components/CreateToDo.tsx index 467ec78..1d646b8 100644 --- a/src/components/CreateToDo.tsx +++ b/src/components/CreateToDo.tsx @@ -46,7 +46,6 @@ function CreateToDo() { const category = useRecoilValue(categoryState); const { register, handleSubmit, setValue } = useForm(); const onValid = ({ toDo }: IForm) => { - console.log("add to do", toDo); setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: category }, ...oldToDos]); setValue("toDo", ""); }; diff --git a/src/components/ToDo.tsx b/src/components/ToDo.tsx index 67faf9b..835c2b6 100644 --- a/src/components/ToDo.tsx +++ b/src/components/ToDo.tsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import { Categories, IToDo, toDoState } from "../atoms"; import React from "react"; -import { useSetRecoilState } from "recoil"; +import { useRecoilState, useSetRecoilState } from "recoil"; const TextSpan = styled.span` margin-right: 10px; @@ -18,19 +18,31 @@ const ToDoList = styled.li` /* margin-bottom: 5px; */ `; +const RemoveBtn = styled.button` + background-color: #a91e1e; + color: white; + border: 2px solid gray; + margin-right: 5px; +`; + function ToDo({ text, category, id }: IToDo) { - const setToDos = useSetRecoilState(toDoState); + const [toDos, setToDos] = useRecoilState(toDoState); const onClick = (event: React.MouseEvent) => { const { currentTarget: { name }, } = event; setToDos((oldToDos) => { const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); - const oldToDo = oldToDos[findIndex]; const newToDo = { text, id, category: name as Categories }; return [...oldToDos.slice(0, findIndex), newToDo, ...oldToDos.slice(findIndex + 1)]; }); }; + const onRemove = () => { + setToDos((oldToDos) => { + const newToDo = oldToDos.filter((toDo) => toDo.id !== id); + return [...newToDo]; + }); + }; return ( {text} @@ -49,6 +61,7 @@ function ToDo({ text, category, id }: IToDo) { Done )} + Remove ); } From 2e945602d87763d0f8b188681ed10ee49fd42175 Mon Sep 17 00:00:00 2001 From: hyunmin200 Date: Tue, 16 Apr 2024 17:59:04 +0900 Subject: [PATCH 11/12] add: document --- Document/State-Management.md | 747 +++++++++++++++++++++++++++++++++++ 1 file changed, 747 insertions(+) diff --git a/Document/State-Management.md b/Document/State-Management.md index c05bfbc..7c6c97f 100644 --- a/Document/State-Management.md +++ b/Document/State-Management.md @@ -7,6 +7,8 @@ - FackBook사람들이 개발한 state관리 libarary - 아주 미니멀하고, 간단하다고 한다. +--- + ## state management에 대해서 알아보자 먼저! state management가 무엇인지 이해하기 위해 state management가 무엇인지 알아보고, @@ -21,6 +23,8 @@ Recoil을 해보기전에 state management를 사용하지않고, 저번에 만 우리는 처음으로 recoil을 사용하지 않고 다크|라이트 모드 체인지 기능을 구현할 것이다. +--- + ### TypeScript Interface 함수 props로 함수를 넘어줄 떄는 @@ -57,6 +61,8 @@ App -> (`isDark`) <- Chart 이게 Recoil의 **Point**!라고 할 수 있다! +--- + ## Recoil 시작 `Recoil`은 위에 계층구조 형식을 해결하기 위해서 만들어졌다고 볼 수 있다. @@ -135,6 +141,8 @@ const setterFn = useSetRecoilState(isDarkAtom); `useSetRecoilState`로 받은 함수는 React의 setState와 같은 방식으로 동작한다. +--- + ### 요약 Recoil에 대해서는 아직 배워야 할 것이 많지만, 짧게 설명해보자면 @@ -145,3 +153,742 @@ Recoil에 대해서는 아직 배워야 할 것이 많지만, 짧게 설명해 그래서 이런 Recoil같은 상태관리 라이브러리를 사용하게 된다면! 쓸모없는 행위가 줄고 코드 가독성도 좋아진다. (심지어 Recoil은 이해하기도 쉽다) + +# ToDo-List 만들기 + +자~! 이제 위애서 Recoil을 공부하였기때문에! 이번에는 ToDoList를 만들면서 Recoil의 새로운기능을 배우고 익숙해져 볼 것이다. + +먼저 복습차원으로 React에서 From을 어떤식으로 만들었는지 상기 시킬 것이다. + +```JS +const [toDo, setToDo] = useState(""); +const onChange = (event: React.FormEvent) => { +const { +  currentTarget: { value }, +} = event; +  setToDo(value); +}; +const onSubmit = (event: React.FormEvent) => { +  event.preventDefault(); +  console.log(toDo); +}; +return ( + 
    +   
    +      +      +    
    +  
    + ); +``` + +우리는 원래 React에서 Form을 구성할 때 이런식으로 구성을 했었다. +다른 기술을 사용하기전에 원래 코드에서 어떤식으로 개선 했는지 상기하기 위해 하였다. + +하지만 이 코드를 단 하나의 코드를 만들 수 있다. +바로 React-Hook-Form이라는 것을 사용해서 이 많은 것들을 한 줄로 가능한지 알아보자. + +## React-Hook-Form + +이 라이브러리는 니꼬 피셜 Form을 관리할 때 가장 편하고 좋은 라이브러리라고 한다. +그리고 큰 Form이나 검증이 필요할 때 특히 더 좋다고 한다. + +그럼 한 번 살펴보러가자 + +실제 우리가 앱을 빌드한다고 생각을 해보면 지금 처럼 input이 한 개가 아니라 여러 개 일 것이다. (아이디, 성, 이름, 비밀번호, 비밀번호 확인 등..) + +이런식으로 input이 많아진다면 우리는 위처럼 form에 많은 **state들을 등록**하게 될 것이다. +그리고 또 거기서 끝나는 것이 아니 **검증과정** 즉 데이터 타입 체크, 조건 확인 등..을 해야한다는 것이다. + +그렇게 되면 또..ErrorState를 만들고..submit함수에 조건 추가하고..이런 작업들은 복잡하지는 않지만 정말로 번거로운 작업이 될 것이다. + +그럼 이제 한 번 React-Hook-Form을 사용해보자! + +--- + +### register + +먼저 **register**라는 것을 사용해볼 것이다. + +`const { register } = useForm();` +register를 사용하게 되면 onChange이벤트 핸들러, input props, setState가 전부 필요없어진다. +이런것들을 적을 필요없이 register함수만 사용하면 된다. + +register함수가 반환해주는 값 안에는 다양한 것들이 있다. +`{name: 'toDO', onChange: ƒ, onBlur: ƒ, ref: ƒ}` log를 찍어보면 이러한 것들이 있다는 것을 알 수 있다. + +이렇게 반환 해주는 것들을 input에다가 바로 반환해서 props로 사용할 수도 있다. +``이런식으로 말이다. + +> 주의: register 사용시 ""사이에 띄어쓰기하면 안됨 + +실행해보면 실행은 잘된다. 하지만 좀 불안하기에 useForm에서 제공해주는 watch를 이용해보자 + +### watch + +**watch**는 우리가 form의 입력값 변화를 관찰할 수 있게 해주는 함수이다. + +만약에 우리가 여러 개의 인풋을 관리하게 된다면? + +```JS +  const { register, watch } = useForm(); +  console.log(watch()); + +  return ( +   
    +     
    +        +        +        +        +        +     
    +   
    +  ); +``` + +WoW 진짜 편하게 관리할 수 있다. + +그리고 watch로 변화하는 모든 것들을 볼 수 있다. +`{userName: '200원', email: 'gyejeongjin@gmail.com', password: '1234', checkPassword: '1234'}` + +--- + +### handleSubmit + +자 이제 위에 onSubmit을 대체 해볼 것이다. +대체하기 위해서는 **handleSubmit**이라는 함수를 사용하면 된다. + +handleSubmit이 여기서 검증과 eventPrevent도 담당해 줄 것이다. + +handleSubmit의 사용방법은 다음과 같다. +onSubmit에 넣어서 사용을 해주면 되는데 2가지의 인자를 넣어줘야 한다. + +1. 데이터가 유효할 때 호출되는 함수 + - 필수 +2. 데이터가 유효하지 않을 때 호출되는 함수 + - 필수 아님 + +그리고 onValid, 즉 첫 번째 함수는 data를 받는데 일단 타입은 any로 지정하고 log를 찍으면 +`{userName: '200원', email: 'gyejeongjin@gmail.com', password: '1234', checkPassword: '1234'}` 이런식으로 watch를 했을때 보았던 data를 볼 수 있다. + +```JS +const { register, watch, handleSubmit } = useForm(); +  const onValid = (data: any) => { +    console.log(data); +  }; +  return ( +   
    +     
    +        +        +        +        +        +     
    +   
    +  ); +``` + +이런식으로 handleSubmit은 submit을 하였을 때, 해야 할 일을 모두 끝내고 데이터가 유효하다면 함수를 호출해준다. + +그럼 이제는 form을 유효하지 않게 만들어보자 + +그러기위해서는 검증 조건이 있어야 하는데 간단하게 required를 해보자. + +```JS + + + + +``` + +왜 이런식으로 register안에 required를 넣는가 궁금할 수도 있는데, 만약 html에 넣어버린다면 누군가가 소스코드를 건드리거나, required를 지원하지 않는 곳에서 사용할 가능성이 있기 때문이다. (HTML에 의지하지 말고 JS에 의지하자!) + +이렇게 코드를 짠다면 짜잔! 값이 비어있다면 그 빈 input으로 이동을 시켜준다. (보호수단생김) + +검증하고 싶은걸 추가하고 싶다면 그냥 required처럼 넣어주기만 하면 바로 적용이된다!! +저 위에 코드처럼 if(어쩌구)이럴 필요가 없어진 것이다! + +그냥 react-hook-form이 다 해준다. + +fromState라는 속성을 추가해주고 errors를 출력해본다면? +`console.log(formState.errors);` +에러처리가 자동으로 되고 있는 것을 볼 수 있다. + +``` +1. checkPassword: {type: 'required', message: '', ref: input} +2. email: {type: 'required', message: '', ref: input} +3. password: {type: 'required', message: '', ref: input} +4. userName: {type: 'required', message: '', ref: input} +``` + +이런 에러처리를 그냥 자동을 해주고, 어떤 에러인지도 알려준다...미춋다.. + +그리고 보면 message라는 것도 있는걸 알 수 있는데 message도 보낼 수 있다.. + +```JS + +``` + +요런식으로 객체로 message를 보낼 수 있다. +`userName: {type: 'required', message: 'userName이 필요합니다!', ref: input}` + +그럼 이 react-hook-form을 사용해서 해결된 것을 본다면 +데이터의 검증이 자동으로 해결이 되고, 에러가 어디서 발생하는지도 알려주고, message도 보낼 수 있고, \[state, onChange, onSubmit\]함수들 안만들어도 되고, input props 일일이 안줘도 된다. + +그냥 미쳤다... + +그리고 추가로 또 다른 검증방법이 있는데 바로 정규식이다. +정규식으로 naver.com메일만 넘기게 할 수도 있고 전화번호도 특정 지역만 받을 수도 있다. + +`/^[A-Za-z0-9._%+-]+@naver.com$/`만약 이러한 정규식으로 사용한다면 naver.com메일만 받는다는 것이다. + +```JS +{...register("email", { + required: true, + pattern: { value: /^[A-Za-z0-9._%+-]+@naver.com$/, message: "naver메일만 허용됩니다." }, +})} +``` + +이런식으로 정규식을 사용할 수 있다. + +그럼 이제 이 에러객체의 메세지를 유저에게 보여주자! + +`{errors?.email?.message}` 메세지를 보여주기 위해서 styledComponent를 만들어주고 안에 message를 넣어주면 된다. (?.은 email이 undefind일수도 있기에 넣어줬다.) + +TypeScript를 사용한다면, useForm<> 제네릭에다가 interface를 넣어줘야 한다. + +```TS +interface FormType { +  userName: string; +  email: string; +  password: string; +  checkPassword: string; +} + +const { +    register, +    handleSubmit, +    formState: { errors }, +  } = useForm(); +``` + +여기서 더 사용할 수 있는 기능이 존재하는데 defaultValue를 줄수도 있다. + +```JS +useForm({ +    defaultValues: { +      email: "@naver.com", +    }, +  }); +``` + +이런식으로 default값을 넣어줄 수도 있다. + +--- + +### Custom Error + +백앤드와 연결한다면, 서버에서 문제가 생겼을 때 에러를 발생시켜야 할 경우도 있다. +그리고 지금 하고 있는 검사 방법 이외에도 validation 방법이 있는지 배워볼 것이다. +(지금도 좋은 validation을 하고 있지만 우리가 특수한 경우의 검증을 하기 위해서는 직접 만들어서 validation(검증)을 해야 할 때도 있을 것이다.) + +그럼 먼저 password와 checkPassword가 같지 않을 때를 검증해보자. + +먼저 data를 interface로 타입을 지정해주고 setError를 가져오자. +여기서 setError는 Error를 발생시켜준다. + +```JS +const onValid = (data: FormType) => { +    if (data.password !== data.checkPassword) { +      setError("checkPassword", { message: "password가 다릅니다." }); +    } +  }; +``` + +이런식으로 에러를 발생시킬 수 있다. + +그리고 만약에 서버가 해킹당해서 서버가 다운되었을 때 에러를 보여주고 싶다면, +TypeScript에 extraError라는 변수의 타입을 지정해주고, setError를 해주면 된다. +extraError라는 변수는 form전체적인 에러를 나타낼 때 사용할 것이다. (이름은 상관없다.) + +setError는 추가적으로 에러를 설정할 수 있게 해줘서 유용하다. + +setError의 또 다른 유용한 기능은 우리가 선택한 input항목에 강제로 focus시킬 수 있다. + +```JS +setError("checkPassword", { message: "password가 다릅니다." }, { shouldFocus: true }); +``` + +위처럼 shouldFocus를 사용해서 input항목에 커서를 focus시킬 수 있다. + +### validate + +우리가 원하는 어떤 조건도 검증할 수 있다! + +만약에 내가 사이트를 만들었는데 200won이라는 이름을 가진 사용자는 계정을 못만들게 막고 싶다?라고 한다면 validate를 사용할 수 있다. + +validate는 값을 함수로 가지고, 인자로는 항목에 현재 쓰여지는 값을 받는다. +그리고 true or false를 반환한다. + +```JS +validate: (value) => value.includes("200won") ? "아닛 200원을 포함하고 있다닛!!!" : true, +``` + +이런식으로 register안에 validate를 넣어서 조건에 맞게 에러를 낼수도 있다! + +그리고 validate는 하나의 함수뿐만 아니라 여러 함수가 있는 객체가 될 수도 있다. +여러 함수가 있는 객체는 input에 여러개의 검사를 할 때 필요할 수도 있다. + +```JS + + value.includes("200won") ? "아닛 200원을 포함하고 있다닛!!!" : true, + noYejun: (value) => + value.includes("Yejun") ? "아닛 Yejun을 포함하고 있다닛!!!" : true, + noSeunghun: (value) => + value.includes("Seunghun") ? "아닛 Seunghun을 포함하고 있다닛!!!" : true, + }, + })} + placeholder="userName" +/> +``` + +그리고 이렇게 사용할 수 있다! + +또한 validate의 함수를 async로 만들어서 서버에다가 확인하고 응답을 받을 수도 있다. + +--- + +### 요약 + +일단 한줄평을 먼저 해보자면 + +> Form의 규모가 크면 클수록 유용해지는 라이브러리 + +- register를 이용해서 state, eventFunc 대체가능 +- validation option을 이용해 편리한 검증가능 +- 에러 객체 제공으로 인해 작업하기 수월함 +- 이 모든걸 handleSubmit이 호출되면 수행됨 + - handleSubmit함수는 인자를 총 1~2개를 받음 + - onValid함수 onInValid함수 + - 검증 성공 | 검증 실패 + - onValid는 필수로 넣어줘야 함 + - onValid함수는 data를 받을 수 있음 +- validation은 only value or 객체 리터럴로 작성함 +- validation에서 error가 나게 되면 formState: {errors}에서 에러를 확인할 수 있음 +- useForm의 인자에 defaultValues를 넣어줄 수 있음 +- setError는 에러를 발생시킬 때 사용함 +- setError에서 shouldFocus라는 기능을 사용할 수 있음 + - 사용자가 form을 submit할 때 에러를 발생시키면 그 항목으로 focus됨 + +그럼 처음으로 돌아가서 toDoList의 토대를 react-hook-form을 사용해서 만들어 본다면, + +```JS +interface IForm { +  toDo: string; +} + +function ToDoList() { +  const { register, handleSubmit, setValue } = useForm(); +  const onValid = (data: IForm) => { +    console.log("add to do", data.toDo); +    setValue("toDo", ""); +  }; +  return ( +   
    +     
    +        +        +     
    +   
    +  ); +} + +export default ToDoList; +``` + +이런식으로 만들 수 있다. + +그리고 여기서는 setValue라는 속성도 사용을 해보았는데 setValue는 첫 번째 인자에 form state 이름을 적고, 두 번째 인자에는 값을 넣어주면 된다. + +이제 react-hook-form은 끝났고! + +다시 Recoil로 돌아간다! + +--- + +## useRecoilState + +일단 이번에는 Recoil을 중점으로 볼 것 같다. + +일단 todoList에다가 모든 것을 만들어놓고 분리시킬 것이다. + +이번에는 recoil에서 안써봤던 중요한 기능을 보여줄 것이다. +일단 atom을 만들어주자 + +```JS +const toDoState = atom({ +  key: "toDo", +  default: [], +}); +``` + +이런식으로 atom을 만들어주고, +전에 배웠던 것 처럼 atom을 가져오기 위해서 recoilValues를 사용해보자 + +```JS +const value = useRecoilValue(toDoState); +``` + +그리고 atom의 value를 변경하기 위해서는 useSetRecoilState로 modifier함수를 가져와서 변경할 수 있었다. + +```JS +const modFn = useSetRecoilState(toDoState); +``` + +이 두가지는 recoil을 사용하게 되면 엄청 자주 사용할 것이다. + +그러면 이런 방법말고 다른 방법을 사용해보자. +바로 useRecoilState를 사용하자! + +```JS +const [value, modFn] = useRecoilState(toDoState); +``` + +뭔가 익숙하지 않은가? useState와 거의 유사하게 사용할 수 있다. + +둘 중 하나만 하고 싶으면 useRecoilValue or useSetRecoilState를 둘 다 하고 싶으면 useRecoilState를 사용하면 된다. + +```JS +const [toDos, setToDos] = useRecoilState(toDoState); +``` + +이런식으로 toDos를 만들어주고 toDos의 타입을 지정해줄 것이다. + +거기에 text와 category를 만들어줄 것인데, +category에는 할 일, 하는 일, 한 일이 들어가야 하기 때문에 TO_DO, DOING, DONE이라는 string만 들어가야 한다. + +그렇때는 아래처럼 쓰면 된다. + +```JS +interface IToDo { +  text: string; +  category: "TO_DO" | "DOING" | "DONE"; +} +``` + +그리고 atom에게 interface를 제네릭으로 적용시켜주자. + +```JS +const toDoState = atom({ +  key: "toDo", +  default: [], +}); +``` + +그래서 이번에는 useRecoilState라는 것을 사용해보았는데 그냥 평소에 사용하던 useState랑 비슷하여서 쓰기 좋았던 것 같다. + +--- + +## Refectoring + +자 이번에는 리팩토링을 해보았다. + +```JS +/* ToDoList.tsx */ + +import { useRecoilValue } from "recoil"; +import CreateToDo from "./CreateToDo"; +import ToDo from "./ToDo"; +import { toDoState } from "../atoms"; + +function ToDoList() { + const toDos = useRecoilValue(toDoState); + return ( +
    +

    투두 리스트!

    +
    + +
      + {toDos.map((toDo) => ( + + ))} +
    +
    + ); +} + +export default ToDoList; + +``` + +```JS +/* CreateToDo.tsx */ + +import React from "react"; +import { useForm } from "react-hook-form"; +import { useSetRecoilState } from "recoil"; +import { toDoState } from "../atoms"; + +interface IForm { + toDo: string; +} + +function CreateToDo() { + const setToDos = useSetRecoilState(toDoState); + const { register, handleSubmit, setValue } = useForm(); + const onValid = ({ toDo }: IForm) => { + console.log("add to do", toDo); + setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: "TO_DO" }, ...oldToDos]); + setValue("toDo", ""); + }; + return ( +
    + + +
    + ); +} + +export default CreateToDo; + + +``` + +```JS +/* ToDo.tsx */ + +import { IToDo, toDoState } from "../atoms"; + +function ToDo({ text }: IToDo) { + return
  • {text}
  • ; +} +export default ToDo; +``` + +와 진짜 recoil을 사용하니까 리팩토링 할 때 component끼리의 의존성이 사라지고 모두 atom을 보면서 편안한 코딩을 할 수 있었다. recoil 만든 사람에게는 노벨상을 줘야 할 것 같다. + +==**팁**== +그리고 만약에 onClick같은 이벤트에서 인자를 넘기고 싶다?? +익명함수를 사용해서 인자를 넘겨주면 된다. + +```JS +{category !== "TO_DO" && } +{category !== "DOING" && } +{category !== "DONE" && } +``` + +이런식으로 사용할 수 있다! + +newCategory가 3가지의 string으로만 이루어져야 하기 때문에 IToDo 인터페이스의 category를 가져와서 type지정을 해줄 수 있다. + +```JS +const onClick = (newCategory: IToDo["category"]) => {}; +``` + +이렇게 interface안에 것만 가져올 수 있다. + +--- + +```js +function ToDo({ text, category }: IToDo) { + const onClick = (newCategory: IToDo["category"]) => { + console.log(newCategory); + }; + return ( +
  • + {text} + {category !== "TO_DO" && } + {category !== "DOING" && } + {category !== "DONE" && } +
  • + ); +} +``` + +이런식으로 구성을 해도 되지만 + +```js +function ToDo({ text, category }: IToDo) { + const setToDos = useSetRecoilState(toDoState); + const onClick = (event: React.MouseEvent) => { + const { + currentTarget: { name }, + } = event; + console.log(name); + }; + return ( +
  • + {text} + {category !== "TO_DO" && ( + + )} + {category !== "DOING" && ( + + )} + {category !== "DONE" && ( + + )} +
  • + ); +} +``` + +이런식으로 html name을 사용할수도 있다. + +--- + +만약 우리가 찾고 싶은 값이 있는데 어디 인덱스에 존재하는지 모르겠다? 라고 한다면 아래의 함수를 사용할 수 있다. + +```js +const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); +``` + +바로 findIndex라는 함수이다. + +findIndex라는 함수의 인자로 조건을 주는 함수를 넣어주면 그 조건의 맞는 인덱스를 return해준다. +요소가 없으면 -1을 반환한다. + +--- + +array에서 immutate하게 원소를 바꾸고 싶다면, + +```JS +const onClick = (event: React.MouseEvent) => { + const { + currentTarget: { name }, + } = event; + setToDos((oldToDos) => { + const findIndex = oldToDos.findIndex((toDo) => toDo.id === id); + const oldToDo = oldToDos[findIndex]; + const newToDo = { text, id, category: name as IToDo["category"] }; + return [...oldToDos.slice(0, findIndex), newToDo, ...oldToDos.slice(findIndex + 1)]; + }); +}; +``` + +이런식으로 코드를 짜준다면 mutate하지 않게 상태를 변환할 수 있다. + +--- + +## Selector in recoil + +이번에는 recoil의 selector라는 개념을 공부해볼 것이다. + +정의를 보면 + +- Selector는 파생된 state(derived state)의 일부를 나타낸다. +- 즉, 기존 state를 가져와서, 기존 state를 이용해 새로운 state를 만들어서 반환할 수 있다. +- 기존 state를 이용만할 뿐 변형시키지 않는다. +- derived state는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있기 때문에 강력한 개념이다. + 이렇다고 한다. + +지금 현재 코드는 아래처럼 카테고리 분류없이 todo라는 state하나에 모든 값을 넣고 있다. + +```JS +export const toDoState = atom({ +  key: "toDo", +  default: [], +}); +``` + +여기서 우리는 selector를 이용해서 카텤고리별 todo들을 분리할 것이다. + +selector를 이용하게 되면 어떤 state를 가져다가 다른 state를 만들 수 있다. +state를 우리 마음대로 변형할 수 있는 것이다. + +여기서 우리는 atom을 세 개 만드는 선택지도 있지만, 너무 번거롭고 todo하나에 담아놓고 싶다. +그걸 하기 위해서 우리는 **selector function**을 사용할 것이다. + +- selector는 atom의 아웃풋을 변형시키는 도구이다. +- selector는 state를 가져다가 무언가를 return한다. + +```JS +export const toDoSelector = selector({ +  key: "toDoSelector", +  get: ({ get }) => { +    // return 하는 값이 toDoSelector의 value가 된다. +    return "Hello"; +  }, +}); +``` + +이런식으로 selector를 만들 수 있고, 값을 가져와서 출력을 하면 Hello가 잘 나온다. + +여기서 중요하게 봐야할 점은 selector는 atom을 가져와서 output를 변형할 수 있다는 것이다. + +- 여기서 get func이 state를 가져올 수 있게 해주는 함수이다. + +```JS +export const toDoSelector = selector({ +  key: "toDoSelector", +  get: ({ get }) => { +    const toDos = get(toDoState); +    return toDos.length; +  }, +}); +``` + +이런식으로 toDoState를 가져올 수 있다. + +이렇게 atom를 가져오게 된다면, 이제 이 selector는 그 atom를 바라보고 있다는 것이다. +atom이 변하게 되면 selector도 변하게 되는 것이다! ( 의존성을 가지게 된다 ) + +```JS +export const toDoSelector = selector({ +  key: "toDoSelector", +  get: ({ get }) => { +    const toDos = get(toDoState); +    return [ +      toDos.filter((toDo) => toDo.category === "TO_DO"), +      toDos.filter((toDo) => toDo.category === "DOING"), +      toDos.filter((toDo) => toDo.category === "DONE"), +    ]; +  }, +}); +``` + +이런식으로 selector를 사용해서 atom의 아웃풋을 변형시켜서 값을 받을 수 있기 때문에 좋은 기능인 것 같다. + +"아웃풋"을 **변형**한다는 것이 엄청난 장점인 것 같다. +(Selector를 사용하면 component에서는 render만 할 수 있게 할 수도 있다.) + +또한 selector는 두 가지의 atom을 동시에 바라볼 수도 있다. ( 둘 중 하나가 바뀌면 다시 렌더 ) + +여기서 **팁** +단축문법을 사용함으로써 `category: category`이 코드를 `category`이렇게 만들 수 있다. + +--- + +끝 From d9e6d8829e8faeed5aaf7f08c0eddc5147c5f70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=ED=98=84=EB=AF=BC?= Date: Wed, 17 Apr 2024 07:38:54 +0900 Subject: [PATCH 12/12] Update src/components/CreateToDo.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 강승훈 <102217780+HUN1i@users.noreply.github.com> --- src/components/CreateToDo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CreateToDo.tsx b/src/components/CreateToDo.tsx index 1d646b8..2ea1c77 100644 --- a/src/components/CreateToDo.tsx +++ b/src/components/CreateToDo.tsx @@ -46,7 +46,7 @@ function CreateToDo() { const category = useRecoilValue(categoryState); const { register, handleSubmit, setValue } = useForm(); const onValid = ({ toDo }: IForm) => { - setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category: category }, ...oldToDos]); +setToDos((oldToDos) => [{ text: toDo, id: Date.now(), category }, ...oldToDos]); setValue("toDo", ""); }; return (