컴포넌트를 좀 더 나눠봤다.

박정훈·2022년 11월 18일
0

알약 프로젝트

목록 보기
7/7

코드가 별로 마음에 안들었다. 중복되는걸 또 만들어서 쓰고 있다던가, 관심사 분리라던가...
그래서 코드를 어떻게 더 개선시킬 수 있을까를 고민을 했다.🙄

일단 이미지 capture기능을 하는 컴포넌트가 두 페이지에서 사용되고 있었다. 이를 common으로 뺐다.

import React from "react";
import { CaptureButton, Container } from "./CaptureContainer.style";

interface CaptureContainerProp {
  handleCapture: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
}

function CaptureContainer({ handleCapture }: CaptureContainerProp) {
  return (
    <Container>
      <input
        accept="image/*"
        id="icon-button-file"
        type="file"
        onChange={handleCapture}
        style={{ display: "none" }}
      />
      <CaptureButton htmlFor="icon-button-file" />
    </Container>
  );
}

export default CaptureContainer;

필요에 맞는 onChange함수만 맞게 만들어서 넘겨주면 되게 했다.
중복이 생기면 재활용해야지..
그리고 handleCapture에서 사용되는 api 함수들을 search 폴더 하위에 api.ts파일을 만들어서 거기서 api 함수를 만들고 꺼내와 사용하게 바꿨다.
기존에는 api함수도 Capture컴포넌트 안에서 만들어 사용했다...

// Capture.tsx
import { useEffect } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import CaptureContainer from "common/capture/CaptureContainer";
import { ROUTE } from "@utils/constant";
import { Toastify } from "@utils/toastify";
import {
  deleteImageOnGCS,
  getPreSignedURL,
  postImageToAIServer,
  putImageOnGCS,
} from "@searchComp/api";

function Capture() {
  const router = useRouter();

  const handleCapture = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const { files } = event.target;
    if (!files) return;

    try {
      const uuid = Date.now().toString();
      const { result } = await getPreSignedURL(uuid);
      await putImageOnGCS(result.url, files[0]);
      const imgURL = result.url.split("png")[0] + "png";

      await postImageToAIServer({ imgURL });
      await deleteImageOnGCS(uuid);
    } catch (e) {
      console.error(e);
      Toastify.fail();
    }
  };

  useEffect(() => {
    const eventSource = new EventSource(
      `${process.env.NEXT_PUBLIC_AI_SERVER_URL}/classify`,
    );
    eventSource.addEventListener("sse", function (e) {
      const data = JSON.parse(e.data);
      const colors = data.colors.split("/");
      router.push({
        pathname: ROUTE.SEARCH_OPTION,
        query: {
          colors,
          letters: data.letters == "NONE" ? "" : data.letters,
          shape: data.shape,
        },
      });
    });

    eventSource.onerror = (e) => {
      eventSource.close();
    };

    return () => {
      eventSource.close();
    };
  }, []);

  return (
    <>
      <Image
        src={"/images/search/captureImage.png"}
        alt="wondering-pill-logo"
        layout="fill"
        objectFit="contain"
        priority={true}
      />
      <CaptureContainer handleCapture={handleCapture} />
    </>
  );
}

export default Capture;

사진을 클릭해서 이미지를 업로드하면, ai서버가 이미지를 분석하고, response로 넘겨준 값을 가지고 query로 다음 page로 넘어간다.

이전 포스팅에 memo를 적용했던 option 페이지다.

현재 바뀐건 다음과 같다. (이름 짓기 너무 어렵다..)

// Option.tsx
export interface ButtonValue {
  name: string;
  isSelected: boolean;
}

function Option({ colors, letters, shape }: OptionPageProps) {
  return (
    <Container>
      <TitleContainer />
      <MainContainer shape={shape} letters={letters} colors={colors} />
    </Container>
  );
}

export default Option;

이전에는 TitleContainer와 MainContainer의 내용 전부 다 Option 컴포넌트 안에 쥬르르륵~
changeStateWithQuery를 지금과 같이 할지, 인수로 넣어서 넘길지는... 흠!
적혀있었다.

근데 가만 보니까 TitleContainer의 내용은 상태가 전혀 없는 단순 UI컴포넌트였다. 일단 얘를 뽑아냈다.

// TitleContainer
import { ACCENT_COLOR, MAIN_COLOR } from "@utils/constant";
import {
  Description,
  Title,
  TitleContainer as Container,
  TitleContent,
  TopBorder,
} from "./Option.style";

function TitleContainer() {
  return (
    <Container>
      <TitleContent>
        <TopBorder $borderColor={ACCENT_COLOR} />
        <Title $color={ACCENT_COLOR}>약 검색</Title>
        <Description $color={MAIN_COLOR}>
          머신러닝으로 추출한 검색값을 <br /> 확인해보세요!
        </Description>
      </TitleContent>
    </Container>
  );
}

export default TitleContainer;

MainContainer를 살펴보면

import { useCallback } from "react";
import { MAIN_COLOR } from "@utils/constant";
import useShapeButtons from "@hooks/option/useShapeButtons";
import useColorButtons from "@hooks/option/useColorButtons";
import useMarkButtons from "@hooks/option/useMarkButtons";
import { MainContent } from "./Option.style";
import ButtonSection from "./buttonSection/ButtonSection";
import Form from "./form/OptionForm";
import { ButtonValue } from "./Option";

export const SHAPE = "체형";
export const COLOR = "색상";
export const MARK = "문양";
const KEY = "0";

interface MainContainerProps {
  shape: string;
  letters: string;
  colors: string | string[];
}

export const changeStateWithQuery = (
  buttons: { [key in string]: ButtonValue },
  queries: string | string[] | undefined,
) => {
  const curButtons = { ...buttons };
  if (typeof queries === "string") {
    curButtons[queries].isSelected = !curButtons[queries].isSelected;
  } else {
    Object.entries(curButtons).map(([key, _]) => {
      if (queries?.includes(key)) {
        curButtons[key].isSelected = !curButtons[key].isSelected;
      }
    });
  }
  return curButtons;
};

function MainContainer({ shape, letters, colors }: MainContainerProps) {
  const { shapeButtons, handleSetShapeButtons } = useShapeButtons(shape);
  const { colorButtons, handleSetColorButtons } = useColorButtons(colors);
  const { markButtons, handleSetMarkButtons } = useMarkButtons();

  const setSelectedButtons = useCallback(
    (buttons: { [key in string]: ButtonValue }) => {
      return Object.entries(buttons)
        .filter(([_, value]) => value.isSelected === true)
        .map((value, _) => value[KEY]);
    },
    [],
  );

  return (
    <MainContent $borderColor={MAIN_COLOR}>
      <ButtonSection
        title={SHAPE}
        buttons={shapeButtons}
        handleSetButtons={handleSetShapeButtons}
      />
      <ButtonSection
        title={COLOR}
        buttons={colorButtons}
        handleSetButtons={handleSetColorButtons}
      />
      <ButtonSection
        title={MARK}
        buttons={markButtons}
        handleSetButtons={handleSetMarkButtons}
      />
      <Form
        shape={setSelectedButtons(shapeButtons)}
        colors={setSelectedButtons(colorButtons)}
        mark={markButtons["mark"].isSelected ? "1" : "0"}
        letters={letters}
      />
    </MainContent>
  );
}

export default MainContainer;

shapeButtons, colorButtons, markButtons 들의 상태가 자리를 꽤 많이 차지하고 있었고, 그게 싫어서 hooks로 뺐다.
changeStateWithQuery를 지금과 같이 할지, 인수로 넣어서 넘길지는... 흠!
여기서만 열심히 상태를 지지고 볶고 하면 된다.

잘한건가

일단 분리는 했다. hooks로 빼지도, api를 분리를 하지도 않고, 컴포넌트 분리도 안하고 죄다 한 파일에 몰아 넣어져 있던 상황은 어떻게 정리가 되긴 했다.
솔직히... 잘한건지는..? 모르겠다. 누가 채점 해 주는것도 아니고~

다만 이런 고민을 늦게 시작한거 같아서 조금 아쉽다! 무지성으로 마구마구 코딩할 때보다 확실히 시간도 더 걸렸다. 근데 난 지금 이게 좀 더 보기 좋으니까...
은근히 재밌기도..? 뭐 아무튼! 앞으로도 고민해 봐야지!
다른건 언제 다 바꾸나~

profile
그냥 개인적으로 공부한 글들에 불과

0개의 댓글