Act99 기술블로그

[NextJs] 주식사이트 만들기 11 / Redux 를 이용해 유저가 선택한 코인의 상태를 관리하기 -2 본문

개발팁저장소/nextjs

[NextJs] 주식사이트 만들기 11 / Redux 를 이용해 유저가 선택한 코인의 상태를 관리하기 -2

Act99 2021. 12. 20. 17:23

이번에는 Redux를 이용해 유저가 선택한 코인의 상태를 관리하고,

동시에 유저가 코인 리스트를 클릭했는지, 안했는지에 대한 상태관리도 Redux로 할 예정이다.

 

결과물은 다음과 같다.

 

 

유저가 원하는 코인을 선택할 때, 차트와 코인 세부사항이 전부 변하게 만들었다.

state 관리를 redux를 이용해 사용한 이유는 다음과 같다.

 

1. Page, Container, Components의 모든 요소들이 같은 state를 공유하고 있기 때문에 props 늪에 빠질 수 밖에 없다.

2. Container에서도, Components에서도 state 값을 변경시켜주어야 한다. 하지만 SetState를 props로 넘겨주기엔 코드가 복잡해지고 에러가 날 시 어디서 에러가 생겼는지 찾기 어렵다.

 

따라서 Redux 와 RTK를 이용해 상태관리하기로 결정했다.

 

먼저, 코인의 State & Reducer를 만들어주었다.

 

- services/coinSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface CoinState {
  coin: string;
}

const initialState: CoinState = {
  coin: "BTC",
};

export const coinSlice = createSlice({
  name: "selectedCoin",
  initialState,
  reducers: {
    selectedCoin: (state: any, action: any) => {
      state.coin = action.payload;
    },
  },
});

export const { selectedCoin } = coinSlice.actions;

export default coinSlice.reducer;

 

다음으로 버튼을 클릭했는지에 대한 State & Reducer를 만들어주었다.

 

- services/coinSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export interface OnClickState {
  coinSelected: boolean;
}

const initialState: OnClickState = {
  coinSelected: false,
};

export const onClickSlice = createSlice({
  name: "onClicked",
  initialState,
  reducers: {
    onCoinSelectBtnClicked: (state: any, action: any) => {
      state.coinSelected = action.payload;
    },
  },
});

export const { onCoinSelectBtnClicked } = onClickSlice.actions;

export default onClickSlice.reducer;

 

다음으로 리듀서들을 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";

import selectedCoinReducer from "../services/coinSlice";
import onCoinSelectBtnClickedReducer from "../services/onClickSlice";

export const store = configureStore({
  reducer: {
    [cryptoApi.reducerPath]: cryptoApi.reducer,
    [cryptoHistoryApi.reducerPath]: cryptoHistoryApi.reducer,
    [cryptoCompareHistoryApi.reducerPath]: cryptoCompareHistoryApi.reducer,
    selectedCoin: selectedCoinReducer,
    CoinSelectBtnClick: onCoinSelectBtnClickedReducer,
  },
});

export type CommonRootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

setupListeners(store.dispatch);

 

이젠 원하는 state를 useSelector를 통해 불러올 수 있으며,

useDispatch를 통해 상태변화와 관리가 가능해졌다.

 

다음으로 한 작업은 상단 코인 상세내용 Container였다.

 

- coinListTable.tsx

import React from "react";
import { useDispatch, useSelector } from "react-redux";
import LoadingComponent from "../../components/loading/loading";
import MilBilCal from "../../functions/milBilCal";
import { CommonRootState } from "../../store/app/store";
import { useGetCryptosQuery } from "../../store/services/cryptoApi";
import { onCoinSelectBtnClicked } from "../../store/services/onClickSlice";

interface Props {
  // onClick: boolean;
}

const CoinListTable: React.FC<Props> = ({}) => {
  const { data, isLoading, error } = useGetCryptosQuery("coins");
  const selectedCoin = useSelector(
    (state: CommonRootState) => state.selectedCoin.coin
  );
  const selectedList = useSelector(
    (state: CommonRootState) => state.CoinSelectBtnClick.coinSelected
  );
  const dispatch = useDispatch();

  const refinedData = data?.data.coins;
  const handleChange = (e: any) => {
    console.log(e.target.value);
  };
  const handleClick = () => {
    selectedList == true
      ? dispatch(onCoinSelectBtnClicked(false))
      : dispatch(onCoinSelectBtnClicked(true));
  };
  console.log(refinedData);
  // console.log(refinedData[0]);
  const handleCoinData = (selectedCoin: string) => {
    for (let i = 0; i < refinedData.length; i++) {
      if (refinedData[i].symbol == selectedCoin) {
        return refinedData[i];
      }
    }
  };
  if (isLoading) {
    return <LoadingComponent />;
  }
  return (
    <div className=" flex flex-row items-center  text-xs">
      <button onClick={handleClick} className=" ml-3 p-2 text-white text-left">
        <div className=" flex flex-row">
          <img
            src={handleCoinData(selectedCoin).iconUrl}
            width={35}
            height={35}
          ></img>
          <div className=" flex flex-col ml-3">
            <h3 className=" font-bold text-lg">
              {handleCoinData(selectedCoin).symbol}
            </h3>
            <h3 className=" text-sm">USD Trading</h3>
          </div>
        </div>
      </button>
      <h3
        className={
          handleCoinData(selectedCoin).change > 0
            ? "text-green-500 text-lg"
            : "text-red-500 text-lg"
        }
      >
        {parseFloat(handleCoinData(selectedCoin).price).toLocaleString() +
          "$" +
          " " +
          "[USD]"}
      </h3>
      <div className=" flex flex-col ml-5 text-white text-xs">
        <h3>24H Change %</h3>
        <h3
          className={
            handleCoinData(selectedCoin).change > 0
              ? "text-green-500"
              : "text-red-500"
          }
        >
          {handleCoinData(selectedCoin).change + "%"}
        </h3>
      </div>
      <div className=" flex flex-col ml-10 text-white ">
        <h3>24H Price [USD]</h3>
        <h3>{handleCoinData(selectedCoin).price.toLocaleString()}</h3>
      </div>
      <div className=" flex flex-col ml-5 text-white ">
        <h3>24H Turnover [USD]</h3>
        <h3>{handleCoinData(selectedCoin).volume.toLocaleString()}</h3>
      </div>
      <div className=" flex flex-col ml-5 text-white ">
        <h3>Number of Exchanges</h3>
        <h3>
          {handleCoinData(selectedCoin).numberOfExchanges.toLocaleString()}
        </h3>
      </div>
      <div className=" flex flex-col ml-5 text-white ">
        <h3>Ranking</h3>
        <h3>{handleCoinData(selectedCoin).rank.toLocaleString()}</h3>
      </div>
      <div className=" flex flex-col ml-5 text-yellow-500 ">
        <h3>Total Supply</h3>
        <h3 className=" text-white">
          {MilBilCal(handleCoinData(selectedCoin).totalSupply)}
        </h3>
      </div>
    </div>
  );
};

export default CoinListTable;

 

결과물

 

 

상세내용에 대한 컨테이너를 만들었으니, 코인 이름이나 아이콘 클릭 시

코인 리스트가 나와야하며, 따라서 코인 리스트 컨테이너와 컴포넌트를 만들었다.

 

- coinList.tsx

 

import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { useGlobalFilter, useTable } from "react-table";
import { CommonRootState } from "../../store/app/store";
import { selectedCoin } from "../../store/services/coinSlice";
import { ImCancelCircle } from "react-icons/im";
import { onCoinSelectBtnClicked } from "../../store/services/onClickSlice";

type Props = {
  columns: any;
  data: any;
  onClick: any;
};

export const CoinList: React.FC<Props> = ({ columns, data, onClick }) => {
  const {
    getTableProps,
    headerGroups,
    rows,
    prepareRow,
    setGlobalFilter,
  }: any = useTable(
    {
      columns,
      data,
    },

    useGlobalFilter
  );

  const coinSelecto = useSelector(
    (state: CommonRootState) => state.selectedCoin.coin
  );
  const selectedList = useSelector(
    (state: CommonRootState) => state.CoinSelectBtnClick.coinSelected
  );
  const handleClick = () => {
    dispatch(onCoinSelectBtnClicked(false));
  };
  const onCoinClick = (rowSymbol: string) => {
    dispatch(selectedCoin(rowSymbol));
    dispatch(onCoinSelectBtnClicked(false));
  };
  const dispatch = useDispatch();

  function Search({ onSubmit }: any) {
    const handleSubmit = (event: any) => {
      event.preventDefault();
      onSubmit(event.target.elements.filter.value);
    };

    return (
      <form onSubmit={handleSubmit}>
        <div className=" ">
          <input
            name="filter"
            placeholder={"  " + "Search"}
            className=" p-1 bg-chartGray-default border-2 text-white w-full "
          />
        </div>
      </form>
    );
  }

  return (
    <>
      <div
        className={
          " flex flex-col p-5 w-128 transition-opacity ease-in delay-500 h-192 overflow-y-scroll bg-chartGray-default"
        }
      >
        <div className="flex flex-row justify-between pb-5 mb-5">
          <h3 className=" text-white font-bold text-2xl">Crypto Currency</h3>
          <div className="items-center cursor-pointer ">
            <ImCancelCircle color="white" size={25} onClick={handleClick} />
          </div>
        </div>
        <Search onSubmit={setGlobalFilter} />

        <table className=" " {...getTableProps()}>
          <thead className=" text-gray-100 text-right h-14 p-5 text-xs">
            <tr>
              <th className=" text-right">Cont.</th>
              <th></th>
              <th>Price</th>
              <th>Change</th>
              <th>Volume</th>
            </tr>
          </thead>
          <tbody {...getTableProps()}>
            {rows.map((row: any, i: number) => {
              prepareRow(row);

              return (
                <tr
                  className="h-14 p-5 text-white cursor-pointer"
                  {...row.getRowProps({
                    onClick: () => {
                      const rowSymbol: string = row.cells[1].value;
                      onCoinClick(rowSymbol);
                      console.log(row.cells[1].value);
                      console.log(coinSelecto);
                    },
                  })}
                >
                  {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"></div>
      </div>
      )
    </>
  );
};

 

 

결과물은 아래와 같다.

 

 

 

이젠 코인 차트 내에 모든 컨테이너와 컴포넌트들을 불러와주었다.

 

- coin.tsx

import { useSelector } from "react-redux";
import { CoinList } from "../components/list/coinList";
import Chat from "../containers/chat/chat";
import { CoinChart } from "../containers/coin/coinchart";
import CoinListTable from "../containers/coin/coinListTable";
import { CoinColumns } from "../etc/coinColumns";
import { useWindowSize } from "../hooks/usewindowsize";
import { CommonRootState } from "../store/app/store";
import { useGetCryptosQuery } from "../store/services/cryptoApi";

interface Size {
  width: number | undefined;
  height: number | undefined;
}

export const Coin = ({}) => {
  const { data, isLoading, error } = useGetCryptosQuery("coins");
  const selectedList = useSelector(
    (state: CommonRootState) => state.CoinSelectBtnClick.coinSelected
  );
  const refinedData = data?.data.coins;

  const size: Size = useWindowSize();
  return (
    <>
      <div className=" relative">
        <div className=" absolute">
          {selectedList == true ? (
            <>
              <div>
                <CoinList
                  columns={CoinColumns}
                  data={refinedData}
                  onClick={selectedList}
                />
              </div>
            </>
          ) : (
            <div></div>
          )}
        </div>
        <div className="bg-chartGray-default flex-col flex w-screen h-auto">
          <CoinListTable />
          <div className="flex flex-row">
            <CoinChart
              width={size.width == undefined ? undefined : size.width * 1}
              height={size.height}
            />
            <Chat
              width={size.width == undefined ? undefined : size.width * 0.2}
              height={size.height}
            />
          </div>
        </div>
      </div>
    </>
  );
};
export default Coin;

 

 

결과물

 

 

구현이 완료되었다.

 

차트에도 Redux를 통해 Coin State값을 불러와주어 연동시켰다.

 

이젠 코인 차트만들기가 끝났다.

 

실시간 데이터 & 원하는 데이터값(볼린저, MACD, 선행 후행스펜 데이터 등등)이 있었으면

더 정밀하고 기능이 많은 차트를 구현할 기회가 있겠으나

 

무료로 가져올 수 있는 코인 데이터는 너무 적기 때문에 차트 구현은 여기서 멈추기로 했다.

조금 아쉽지만 코드정리 후 github를 통해 배포할 예정이다.

배포할 때는 채팅기능과 국내증시 차트는 없앨 예정이다.

 

코드정리를 한 후 github를 통해 배포할 예정이다.

 

https://github.com/act99/stock-chat

 

GitHub - act99/stock-chat: stock&coin chart with Chatting Nestjs, Apollo, Redux, Redux-Toolkit

stock&coin chart with Chatting Nestjs, Apollo, Redux, Redux-Toolkit - GitHub - act99/stock-chat: stock&coin chart with Chatting Nestjs, Apollo, Redux, Redux-Toolkit

github.com