Act99 기술블로그

[Nextjs] 주식사이트 만들기-8 홈 화면 구현 (React table typescript filter search / setGlobalFilter error 해결) 본문

개발팁저장소/nextjs

[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은 구현하지 않는 것으로 할 예정이다.

 

저 오류는 차근차근 찾아봐야겠다.

 

결과물