- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- graphql
- error
- Coin
- apollo
- Firebase
- nestjs
- typescript
- 항해99
- 주식
- Flutter
- Redux
- react
- chart
- typeorm
- 차트
- nextjs
- javascript
- 비전공자
- 에러
- 채팅
- 주식차트
- 차트만들기
- websocket
- 3주차
- 리액트
- rtk
- API
- 코인
- 코인차트
- 차트구현
Act99 기술블로그
[Nextjs] 주식사이트 만들기-8 홈 화면 구현 (React table typescript filter search / setGlobalFilter error 해결) 본문
[Nextjs] 주식사이트 만들기-8 홈 화면 구현 (React table typescript filter search / setGlobalFilter error 해결)
Act99 2021. 12. 16. 15:50이번에는 주식사이트의 홈 화면을 구현하려고 한다.
대부분 코인거래소 홈 화면은 코인들의 표가 나타난다. (현재가, 상승 하락률 저가, 고가 등등)
이번에 데이터를 가져올 사이트는 코인랭킹 이라는 사이트이며, RTK(Redux toolkit) 을 이용해 가져올 것이다.
먼저 결과물부터 게시하면,
이런식으로 구현을 했다.
구현을 위한 코드는 아래와 같다.
RTK Service 코드는 다음과 같이 구성했다.
const cryptoApiHeaders = {
"x-rapidapi-host": "coinranking1.p.rapidapi.com",
"x-rapidapi-key": process.env.NEXT_PUBLIC_CRYPTO_API_KEY,
};
const baseUrl = "https://coinranking1.p.rapidapi.com";
const createRequest = (url: string) => ({ url, headers: cryptoApiHeaders });
export const cryptoApi = createApi({
reducerPath: "cryptoApi",
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getCryptos: builder.query({
query: (name) => createRequest(`/${name}`),
}),
}),
});
export const { useGetCryptosQuery } = cryptoApi;
코인랭킹 API 는 http Headers의 api Key 값을 요구하기 때문에 다음처럼 API Headers 상수를 만들어주었다.
api-key의 경우 환경변수로 설정해두었다.
다음으로 store에 리듀서를 작성해주었다.
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import { createWrapper } from "next-redux-wrapper";
import {
cryptoApi,
cryptoCompareHistoryApi,
cryptoHistoryApi,
} from "../services/cryptoApi";
export const store = configureStore({
reducer: {
[cryptoApi.reducerPath]: cryptoApi.reducer,
[cryptoHistoryApi.reducerPath]: cryptoHistoryApi.reducer,
[cryptoCompareHistoryApi.reducerPath]: cryptoCompareHistoryApi.reducer,
},
});
setupListeners(store.dispatch);
cryptoApi 를 제외한 나머지 리듀서들은 코인차트에 필요한 리듀서이다.
다음으로 RTK service에서 만들어둔, query 상태를 홈 화면으로 불러오고,
React Table을 이용해 테이블에 필요한 Column 데이터를 만들어주었다.
코인 가격은 100달러 이상일 경우 소수점에 2번째까지만, 이하일 경우 소수점에 4번째까지만 나타나게 만들었으며, 상승 하락률은 상승시 초록, 하락시 빨간색으로 설정해두었다.
- data Set
- index.tsx
import type { GetStaticProps, NextPage } from "next";
import Head from "next/head";
import { CoinTable } from "../containers/coin/coinTable";
import LoadingComponent from "../components/loading/loading";
import { useGetCryptosQuery } from "../store/services/cryptoApi";
type ColumnProps = {
value: string;
};
const Home: NextPage = () => {
const { data, isLoading, error } = useGetCryptosQuery("coins");
const globalStats = data?.data?.stats;
const columns = [
{
// Header: "name",
accessor: "iconUrl",
Cell: ({ value }: ColumnProps) => (
<div className=" flex justify-end">
<img src={value} className=" w-5 h-5" />
</div>
),
},
{
Header: () => (
<div className=" flex justify-start">
<h3>Trading Pairs</h3>
</div>
),
accessor: "symbol",
Cell: ({ value }: ColumnProps) => (
<div className=" flex justify-start">
<h3>{value}</h3>
</div>
),
},
{
Header: "Latest Traded Price",
accessor: "price",
Cell: ({ value }: ColumnProps) => (
<div className=" flex justify-start">
<h3>
{parseFloat(value) < 100
? parseFloat(value).toLocaleString(undefined, {
maximumFractionDigits: 4,
}) +
" " +
"$"
: parseFloat(value).toLocaleString(undefined, {
maximumFractionDigits: 2,
}) +
" " +
"$"}
</h3>
</div>
),
},
{
Header: "24H Change %",
accessor: "change",
Cell: ({ value }: ColumnProps) => (
<div className=" flex justify-start">
<h3
className={
parseFloat(value) < 0 ? "text-red-600" : "text-green-500"
}
>
{value + " " + "%"}
</h3>
</div>
),
},
// {
// Header: "name",
// accessor: "name",
// },
{
Header: "Trading Volume",
accessor: "volume",
Cell: ({ value }: ColumnProps) => (
<div className=" flex justify-start">
<h3>{parseFloat(value).toLocaleString()}</h3>
</div>
),
},
];
const tableData = data?.data?.coins;
console.log(tableData);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div>
<Head>Hi</Head>
<div className="flex flex-col bg-gray-200">
<div className=" flex flex-col justify-center items-center">
<CoinTable columns={columns} data={tableData} />
</div>
</div>
</div>
);
};
export default Home;
- coinTable.tsx
import { range } from "lodash";
import React, { useState } from "react";
import {
useAsyncDebounce,
useFilters,
useGlobalFilter,
useTable,
} from "react-table";
type Props = {
columns: any;
data: any;
};
export const CoinTable: React.FC<Props> = ({ columns, data }) => {
const [start, setStart] = useState(0);
const [end, setEnd] = useState(10);
const [filter, setFilter] = useState("");
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable(
{
columns,
data,
},
useGlobalFilter,
useFilters
);
const sliceData = (data: any, startLen: number, endLen: number) => {
const arr = [];
for (let i = startLen; i < endLen; i++) {
arr.push(data[i]);
}
return arr;
};
const nextOnClick = () => {
if (start < rows.length - 10 && end < rows.length) {
setStart(start + 10);
setEnd(end + 10);
} else {
setStart(start - 0);
setEnd(end - 0);
}
};
const prevOnClick = () => {
if (start != 0 && end != 10) {
setStart(start - 10);
setEnd(end - 10);
} else {
setStart(start - 0);
setEnd(end - 0);
}
};
function Search({ onSubmit }: any) {
const [search, setSearch] = useState("");
return (
<form>
<input name="filter" />
<button>Search</button>
</form>
);
}
const handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.currentTarget;
setFilter(value);
};
console.log(data[0].symbol);
console.log(sliceData(rows, start, end));
return (
<>
<div className=" flex flex-col p-5 w-10/12">
<h3 className=" py-5 text-3xl font-extrabold">Markets</h3>
<input value={filter} onChange={handleFilter} placeholder="Search" />
<table className=" " {...getTableProps()}>
<thead className=" text-gray-400 text-left bg-gray-300 h-14 p-5">
{headerGroups.map((headerGroups) => (
<tr {...headerGroups.getHeaderGroupProps()}>
{headerGroups.headers.map((columns) => (
<th {...columns.getHeaderProps()}>
{columns.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableProps()}>
{sliceData(rows, start, end).map((row, i) => {
prepareRow(row);
// console.log(i);
return (
<tr
className={
i % 2 == 0
? " bg-white h-14 p-5"
: " bg-gray-300 h-14 p-5"
}
{...row.getRowProps()}
>
{row.cells.map((cell: any) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
)
</table>
<div className=" flex flex-row justify-evenly py-5">
<button
className=" bg-yellow-500 px-10 py-3 rounded-2xl text-center font-bold"
onClick={prevOnClick}
>
prev
</button>
<button
className=" bg-yellow-500 px-10 py-3 rounded-2xl text-center font-bold"
onClick={nextOnClick}
>
next
</button>
</div>
</div>
</>
);
};
// ** Typescript에서는 React - table Searchfilter 구현이 안되어있음//
// function GlobalFilter({
// preGlobalFilteredRows,
// globalFilter,
// setGlobalFilter,
// }: any) {
// const count = preGlobalFilteredRows.length;
// const [value, setValue] = React.useState(globalFilter);
// const onChange = useAsyncDebounce((value) => {
// setGlobalFilter(value || undefined);
// }, 200);
// return (
// <span>
// Search:{" "}
// <input
// value={value || ""}
// onChange={(e) => {
// setValue(e.target.value);
// onChange(e.target.value);
// }}
// placeholder={`${count} records...`}
// style={{
// fontSize: "1.1rem",
// border: "0",
// }}
// />
// </span>
// );
// }
결과는
(여기서 Search는 위에 코드에서는 아직 구현 안된 것)
잘 나오고 있다.
이젠 검색창(필터 Filter)을 만들 차례이다. 하지만 여기서 TypeScript 사용시 React Table Filter 가 제대로 작동되지 않으며, setGlobalFilter 가 정상적으로 import 되지 않는 것을 확인할 수 있다.
이것은 : any 로 타입 처리를 해주면 쉽게 풀린다.
하지만 rows 데이터를 slice 하게되면 globalFilter를 사용할 수 없게 된다.
(에러를 봤을 때, slice한 rows는 "rows"라는 타입이 아니기 때문에 globalFilter에 적용될 수 없는 것 같다.)
따라서 React table pagination 을 사용해야 하며,
이때, initialState 적용 시 error 에러가 발생한다.
따라서
react-table-config.d.ts
파일을 만들어준 후
밑에 코드를 카피해서 붙여주어야 한다.
import {
UseColumnOrderInstanceProps,
UseColumnOrderState,
UseExpandedHooks,
UseExpandedInstanceProps,
UseExpandedOptions,
UseExpandedRowProps,
UseExpandedState,
UseFiltersColumnOptions,
UseFiltersColumnProps,
UseFiltersInstanceProps,
UseFiltersOptions,
UseFiltersState,
UseGlobalFiltersColumnOptions,
UseGlobalFiltersInstanceProps,
UseGlobalFiltersOptions,
UseGlobalFiltersState,
UseGroupByCellProps,
UseGroupByColumnOptions,
UseGroupByColumnProps,
UseGroupByHooks,
UseGroupByInstanceProps,
UseGroupByOptions,
UseGroupByRowProps,
UseGroupByState,
UsePaginationInstanceProps,
UsePaginationOptions,
UsePaginationState,
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
UseResizeColumnsOptions,
UseResizeColumnsState,
UseRowSelectHooks,
UseRowSelectInstanceProps,
UseRowSelectOptions,
UseRowSelectRowProps,
UseRowSelectState,
UseRowStateCellProps,
UseRowStateInstanceProps,
UseRowStateOptions,
UseRowStateRowProps,
UseRowStateState,
UseSortByColumnOptions,
UseSortByColumnProps,
UseSortByHooks,
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState
} from 'react-table'
declare module 'react-table' {
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseFiltersOptions<D>,
UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>,
UsePaginationOptions<D>,
UseResizeColumnsOptions<D>,
UseRowSelectOptions<D>,
UseRowStateOptions<D>,
UseSortByOptions<D>,
// note that having Record here allows you to add anything to the options, this matches the spirit of the
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
// feature set, this is a safe default.
Record<string, any> {}
export interface Hooks<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedHooks<D>,
UseGroupByHooks<D>,
UseRowSelectHooks<D>,
UseSortByHooks<D> {}
export interface TableInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderInstanceProps<D>,
UseExpandedInstanceProps<D>,
UseFiltersInstanceProps<D>,
UseGlobalFiltersInstanceProps<D>,
UseGroupByInstanceProps<D>,
UsePaginationInstanceProps<D>,
UseRowSelectInstanceProps<D>,
UseRowStateInstanceProps<D>,
UseSortByInstanceProps<D> {}
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderState<D>,
UseExpandedState<D>,
UseFiltersState<D>,
UseGlobalFiltersState<D>,
UseGroupByState<D>,
UsePaginationState<D>,
UseResizeColumnsState<D>,
UseRowSelectState<D>,
UseRowStateState<D>,
UseSortByState<D> {}
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnOptions<D>,
UseGlobalFiltersColumnOptions<D>,
UseGroupByColumnOptions<D>,
UseResizeColumnsColumnOptions<D>,
UseSortByColumnOptions<D> {}
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnProps<D>,
UseGroupByColumnProps<D>,
UseResizeColumnsColumnProps<D>,
UseSortByColumnProps<D> {}
export interface Cell<D extends Record<string, unknown> = Record<string, unknown>, V = any>
extends UseGroupByCellProps<D>,
UseRowStateCellProps<D> {}
export interface Row<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedRowProps<D>,
UseGroupByRowProps<D>,
UseRowSelectRowProps<D>,
UseRowStateRowProps<D> {}
}
- code
import { range } from "lodash";
import React, { useState } from "react";
import { useGlobalFilter, usePagination, useTable } from "react-table";
type Props = {
columns: any;
data: any;
};
export const CoinTable: React.FC<Props> = ({ columns, data }) => {
const {
getTableProps,
headerGroups,
rows,
prepareRow,
setGlobalFilter,
previousPage,
nextPage,
canPreviousPage,
canNextPage,
state: { pageIndex, pageSize },
}: any = useTable(
{
columns,
data,
initialState: { pageIndex: 2 },
},
useGlobalFilter,
usePagination
);
function Search({ onSubmit }: any) {
const handleSubmit = (event: any) => {
event.preventDefault();
onSubmit(event.target.elements.filter.value);
};
return (
<form onSubmit={handleSubmit}>
<input name="filter" />
<button>Search</button>
</form>
);
}
return (
<>
<div className=" flex flex-col p-5 w-10/12">
<h3 className=" py-5 text-3xl font-extrabold">Markets</h3>
<Search onSubmit={setGlobalFilter} />
<table className=" " {...getTableProps()}>
<thead className=" text-gray-400 text-left bg-gray-300 h-14 p-5">
{headerGroups.map((headerGroups: any) => (
<tr {...headerGroups.getHeaderGroupProps()}>
{headerGroups.headers.map((columns: any) => (
<th {...columns.getHeaderProps()}>
{columns.render("Header")}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableProps()}>
{rows.map((row: any, i: number) => {
prepareRow(row);
// console.log(i);
return (
<tr
className={
i % 2 == 0 ? " bg-white h-14 p-5" : " bg-gray-300 h-14 p-5"
}
{...row.getRowProps()}
>
{row.cells.map((cell: any) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div className=" flex flex-row justify-evenly py-5">
<button
className=" bg-yellow-500 px-10 py-3 rounded-2xl text-center font-bold"
onClick={previousPage()}
disabled={!canPreviousPage}
>
prev
</button>
<button
className=" bg-yellow-500 px-10 py-3 rounded-2xl text-center font-bold"
onClick={nextPage()}
disabled={!canNextPage}
>
next
</button>
</div>
</div>
</>
);
};
하지만 이런 오류가 발생한다.
이 문제는 차차 해결해나갈 예정이며,
원래 계획했었던 것은 코인거래소에서 사용하는 마켓 데이터 표이며, 마켓데이터 표는 pagination을 구현시키지 않았기 때문에 pagination은 구현하지 않는 것으로 할 예정이다.
저 오류는 차근차근 찾아봐야겠다.
결과물
'개발팁저장소 > nextjs' 카테고리의 다른 글
[Nextjs] 주식사이트 만들기-9 코인 정보 슬라이드 (Carousel) 만들기 (0) | 2021.12.17 |
---|---|
[Nextjs] 주식사이트 만들기-9 코인 정보 카드 만들기 (Intl.NumberFormat) (0) | 2021.12.16 |
[Nextjs] 주식사이트 만들기-7 윈도우 사이즈 에러 해결 (0) | 2021.12.15 |
[Nextjs] 주식사이트 만들기-7 채팅 프론트엔드 Apollo client - polling & refetch 를 이용한 실시간 채팅 구현 (0) | 2021.12.15 |
[Nextjs] 주식사이트 만들기-6 채팅 프론트엔드 (Websocket Error) (0) | 2021.12.14 |