teklog

프론트 개발자 취업 회고록 2

2022/09/18

n°14

category : Recap

img


이전 글에서 이어집니다.



3. 담당 업무 / 코드 공유


앞서 말했듯이 인력이 부족한 상황이었고, 온보딩 코스가 따로 마련되지 않았기 때문에 첫날부터 바로 업무에 투입되었다. 입사하고 한달이 지나도 개발을 못한 다른 친구들도 있었기 때문에, 현업에서 구르며 빠르게 배울 수 있어 오히려 만족스러웠다.


프론트 리드님이 업무 중요도가 낮고, 구현 난이도가 어렵지 않은 업무부터 천천히 일을 던져주셨다. 하나씩 하나씩 일을 처리해가면서 점점 커가는 느낌이 들었다. 1인분의 몫을 하면서 점차 팀에 도움이 되는 것 같아 만족스러웠다.


- 통합 지갑


회사에서는 두가지의 암호화폐를 발행하고 있었다. 메인 서비스인 포탈에서 활동하면 얻을 수 있는 코인과 회사의 nft 관련된 교육 컨텐츠를 소비하면 얻을 수 있는 코인이었다. 또한 회사에서 발행하는 nft도 있었다. 이전에도 물론 이를 관리할 지갑이 있었지만, nft 코인은 별도의 도메인으로 운영 중이었기에 별도의 지갑으로 관리되었다. 하지만 메인 서비스 페이지에 nft 거래소를 추가하는 프로젝트가 기획되면서, 이 3가지 가상 자산을 통합적으로 관리할 지갑이 필요했다. 본격적으로 투입되는 프로젝트는 처음이었기에 기대반 걱정반이었다. 이 프로젝트에서 나는 다음 3가지 역할을 맡아야했다.


  • 통합 지갑을 위한 모바일 마이 페이지 리뉴얼
  • 마이페이지 내 암호화폐 지갑 컴포넌트 개발
  • 암호화폐 거래 내역 페이지 개발


리뉴얼은 어느정도 도가 튼 상황이었기에, 큰 무리없이 빠르게 진행할 수 있었다. 물론 이것이 가능했던 이유는 기존의 코드가 새로 개발하기 쉽도록 짜여졌던 점도 있었고, 내가 회사 코드에 어느 정도 적응했기 때문이기도 했다. 처음에 방대한 프로젝트의 폴더구조와 모듈을 보고 압도당한 기분이었는데, 다 이럴 때를 위해서였다.


특히 마이페이지를 리뉴얼하면서 유저의 개인 정보를 불러오는 여러 훅에 적응할 수 있었다. 리뉴얼 과정에서는 총 5개의 모듈 컴포넌트를 새로 개발하였고, 16개의 모듈 컴포넌트와 2개의 템플릿 컴포넌트, 1개의 커스텀 훅을 수정하였다. 모듈 컴포넌트는 마이페이지의 하위 메뉴 하나에 해당하는 컴포넌트였으며, 템플릿 컴포넌트는 마이페이지 전체 레이아웃과 prop으로 내려줄 state, fetch 데이터를 관리하는 페이지였다.


이 과정에서 업무에서 전역 상태 관리와 SSR을 경험했기에 공유한다.

*회사 코드를 그대로 가져온 것은 아닌, 설명을 하기 위한 snippet입니다.


마이페이지의 index.tsx

const Menu = (props) => <MenuTemplate {...props} />;


export const getServerSideProps: GetServerSideProps = async (context) => {
  axios.defaults.headers.common.Authorization = "";
  const { tokenState } = await initializeProps(context);

  return {
    props: {
      tokenState ,
    },
  };
}; 


아토믹 패턴을 적용하여 SSR이 일어나는 모든 페이지의 index.tsx는 이처럼 직관적으로 구성된다. util에 있는 initializeProps 함수를 이용해 로그인 이후 쿠키에 저장된 토큰을 반환받아 SSR의 템플릿에 Props로 내려준다. 기본적으로 인증이 필요한 SSR로 렌더링되는 모든 페이지는 이와 같은 방식으로 활용하였다. Node.js로 MVC 패턴의 api를 개발할 때를 떠올리게 하는 code splitting과 컴포넌트 구조였다.


마이페이지 / 지갑 아이템 공용 컴포넌트

interface IProps {
  backgroundColor: string;
  customHeaderRight?: ReactNode;
  children: ReactNode;
  title: string;
  titleIcon?: string;
}

const WalletItem = (props: IProps) => {
  const { backgroundColor, customHeaderRight, children, title, titleIcon } = props;
  return (
    <__Wrapper backgroundColor={backgroundColor}>
      <__HeaderWrapper alignItems="center" justifyContent="space-between">
        <__TitleWrapper alignItems="center">
          {titleIcon && <__TitleIcon src={titleIcon} alt={title} />}
          <span>{title}</span>
        </__TitleWrapper>
        <__CustomHeaderRightWrapper>{customHeaderRight}</__CustomHeaderRightWrapper>
      </__HeaderWrapper>
      <__ContentWrapper>{children}</__ContentWrapper>
    </__Wrapper>
  );
};


마이페이지의 지갑은 코인 지갑, nft 코인, nft 지갑 3 종류의 지갑이 한 페이지에 들어갔다. 세 컴포넌트가 동일한 UI를 공유하는 부분이 있었고, 지갑 안의 링크를 클릭하면 해당 코인/nft의 상세 정보 페이지로 이동했다. 세 지갑의 Wrapper가 동일했기에, 재사용할 수 있는 지갑 공용 컴포넌트를 만들었고, styled-components의 prop을 활용하여 지갑마다 다른 스타일링을 할 수 있도록 하였다.



  <WalletItem
        backgroundColor="linear-gradient(286.91deg, #7949F2 -0.72%, #8650C9 101.75%)"
        title="Nft Coin Wallet"
        titleIcon="https://s3.coinghost.com/cogo_mobile/image/wallet/wallet-pop-icon.svg"
        customHeaderRight={
          <__NftCoinGuide onClick={() => router.push("/guide/pop")}>
            <Flex alignItems="center">
              <p>Nft Coin 사용가이드</p>
              <Image
                src="https://s3.coinghost.com/cogo_mobile/image/arrows/arrow-white.svg"
                width="0.9rem"
                height="1.4rem"
              />
            </Flex>
          </__NftCoinGuide>
        }
      >
        <__MyPopWrapper>
          <p className="field">보유수량</p>
          <__MyWalletCount>
            <__MyPopBalance>
              <NumberFormat
                value={userWalletData?.lollipopCount}
                decimalScale={2}
                displayType="text"
                suffix=" POP"
                thousandSeparator
              />
            </__MyPopBalance>
            <p className="notice">(1POP = 1KRW)</p>
          </__MyWalletCount>
        </__MyPopWrapper>
        <__FooterWrapper alignItems="center" justifyContent="center" background="#9067EB">
          <__WalletButton width="220px" onClick={() => router.replace("/wallet/pop")}>
            __Nft Coin 내역
          </__WalletButton>
          <__FooterDiv footerBorderColor="#B693E8" />
          <__WalletButton width="187px" onClick={() => router.replace("/wallet/pop")}>
            충전하기
          </__WalletButton>
          <__FooterDiv footerBorderColor="#B693E8" />
          <__WalletButton width="255px" onClick={() => router.replace("/")}>
            Coin으로 스왑하기
          </__WalletButton>
        </__FooterWrapper>
      </WalletItem>

이처럼 재사용되었다.



- BMS 사이트 운영 관리자 페이지

메인 서비스와 nft 서비스를 운영팀이 관리할 수 있는 백오피스 페이지를 개발하였다. 총 24개의 페이지를 개발하였으며, 수많은 수정이 있었다. (내가 작성한 수정 관련 커밋이 50개 있었다.) 개발을 끝마치면 바로 운영팀에서 피드백이 돌아왔기에 수정할 일이 많았다. 다행히 사내에서만 사용되는 페이지이기에 사용자 경험에 대한 이해를 쌓아가며 빠르게 수정할 수 있었다. 만일 상용 페이지였다면 사용자 이탈이 많이 일어났을 것이다. 운영팀으로부터 요청받은 수정 사항들은 다음과 같았다.


  • 검색 필터링 이벤트 핸들러 수정
  • create 페이지의 필드 추가
  • 페이지네이션 필터링 수정
  • 필터링 버튼 작동방식 수정
  • etc..


사수분이 개발해주신 공용 모듈 컴포넌트를 사용하여 빠른 속도로 개발할 수 있었다. 중간에 이 모듈 컴포넌트를 수정할 일이 있었는데 까다로운 작업이었다. 모듈로 작성된 다른 페이지들이 영향받지 않도록 해야했다. 공용 컴포넌트를 개발할 때 훗날 어떤 기능이 추가될지 알 수 없기에 확장성을 고려하게 된 계기였다.


운영자 게시판 Create 페이지의 공용 모듈 컴포넌트

  /* nft이미지 업로드를 위한 변수 */
  const filterCategory = createFormData?.category;
  const mintingScheduleFields = JSON.stringify(['닉네임', '토큰 심볼', '이름', '설명', '발행인 수수료 주소', '발행인 수수료', '플랫폼 수수료', '롤리팝 가격', '코인 가격', '수량', '참조 주소']);
  const formFields = JSON.stringify(filterCategory.map((item) => item = item.input?.label).filter((item) => item !== undefined));


...

 const handleNftImage = () => {
    const contractName = getValues('contractName');
    if (contractName) {
      return contractName;
    }
    alert('체인을 입력해주세요');
    return false;
  };


  const handleImageChange = (value: string) => async (e) => {
    let nftUrl = '';
    if (formFields === mintingScheduleFields) {
      const validateNftUrl = handleNftImage();
      if (!validateNftUrl) {
        return;
      }
      nftUrl = validateNftUrl;
    }


    const image = e.target.files[0];
    const formData = new FormData();
    formData.append('image', image, image.name);
    Axios.post(!nftUrl ? `${apis.base}/images` : `${apis.base}/images?serviceName=nft&collection=${nftUrl}`, formData).then((res) => {
      setImage((prev) => ({...prev, [value]: res.data.url}));
      setValue(value, res.data.id);
    });
  };


문제상황 : 기존 create 페이지 모듈 컴포넌트에선 게시물에 이미지를 업로드 하기 위해서 백엔드 서버에 이미지를 post 요청으로 먼저 저장한 뒤에 이미지 url을 리턴받았다. 하지만 nft 이미지를 업로드할 때는 백엔드에 다른 주소로 요청을 보내주어야 했다. 그러나 내가 개발한 모든 관리자 create 페이지는 이 공용 컴포넌트로 되어 있어기 때문에, 기존 이미지 업로드 url은 유지하면서 nft 이미지의 요청 url만 변경해주어야 했다. 또한 nft의 체인에 따라 다른 경로로 보내주어야 했기에, url은 동적으로 변경돼야했다.


해결 : props로 넘겨받는 테이블의 필드들이 nft create 페이지의 특정한 필드와 동일한지 비교하여 만일 동일하다면 업로드된 nft 이미지의 체인 이름을 반환하여 url에 넣어주는 것으로 해결했다. 이 방식을 통해 기존 이미지 업로드도 유지하면서, nft 체인에 따라 동적으로 이미지를 요청할 수 있게 해주었다. 또한 nft 체인을 먼저 입력해서 올바른 url로 이미지를 업로드할 수 있도록 alert를 이용했다.




1const swr = useSWR(`/otc/mintings/schedules/${params.id}`);
  const {data: detailData, mutate} = swr;
  mutate({...detailData,
    tokenSymbol: detailData?.symbol,
    nickName: detailData?.creator?.merchantUser?.nickName,
    LollipopPrice: detailData?.lollipopPrice,
  }, false);


2)
const handleOnSubmit = async (data) => {
    try {
      const body = {
        ...data,
        creatorFee: Number(data.creatorFee),
        platformFee: Number(data.platformFee),
        count: Number(data.count),
        thumbnailId: Number(data.thumbnailId),
      };
      const res = await Axios.post(`/otc/mintings/schedules`, body);
      navigate(`/dashboard/coinghost/nft-gallery/minting-schedule`);
    } catch (e) {
      console.log(e.message);
      alert(e.message);
    }
  };


1) 이벤트 수정 페이지에서 백엔드가 보내주는 필드에 뎁스가 많이 있고, 프론트에서 데이터를 사용할 필드의 이름과 달라서 mutate함수를 이용해 가공하였다.


2) 반대로 POST, PUT 요청을 통해 데이터를 보내야할 때 타입이 다른 데이터는 적절한 타입으로 변경하여 보내준다.



- 유지 및 보수

BMS 페이지를 담당한 이후로 점차 유지 보수 관련 티켓을 받기 시작했다. 간단한 부분 스타일 변경부터 시작하여, 나중에는 페이지 전체의 전반적인 디자인을 리뉴얼하게 되었다. 유지 보수 업무 중 기억에 남는 것은 모바일 페이지의 게시글 작성페이지였다.


포털 게시글 작성 공용 컴포넌트 & fetch 함수



1)
...
export const getServerSideProps: GetServerSideProps = async (context) => {
  const {
    req: { headers },
    res,
  } = context;
  const { tokenState } = await initializeProps(context);


 if (headers["mining-type"] && headers["event-id"]) {
    res.setHeader(
      "set-cookie",
      `miningType=${JSON.stringify({
        miningType: headers["mining-type"],
        eventScheduleId: Number(headers["event-id"]),
      })}; path=/blogs`,
    );
  }
...

2)
...
const createFetch = async () => {
    try {
      const miningType = cookie.get("miningType") && JSON.parse(cookie.get("miningType"));
      const res = await APIs[path].post({
        title,
        contents,
        ...miningType,
      });
      const { data: postData } = res;
      if (res.status === 200) {
        cookie.remove("miningType", { path: "/blogs" });
        return router.replace(`/${redirectPathValue}/${postData.data.id}`);
      }
    } catch (e) {
      modal({ message: e?.data?.message || "네트워크 환경이 불안정합니다." });
    } finally {
      loading();
    }
  };


이벤트에 당첨된 유저들이 게시판에 후기를 남기면 보상을 주도록 수정해야했다. 모바일 앱에서 웹뷰를 사용하고 있었기 때문에, 서버사이드 쪽의 요청 헤더에 접근하여 event-id가 있을 시 응답 헤더에 event-id를 저장해주었다. 이후 create 페이지의 fetch 함수 내부에서 쿠키에 접근하여 이벤트 당첨 유저인지를 판별하였다.


이 작업이 인상 깊었던 이유는 모바일 팀과 협업 때문이다. 앱은 플러터와 스위프트를 이용해 개발되었는데, 앱에서도 웹뷰를 띄우는 페이지가 많았기 때문에 프론트 팀과 협업이 자주 있었다. 모바일 앱에서 요청을 보내면 프론트 웹에서 헤더를 stringify해서 쿠키에 저장하고, POST 요청을 보내기 전에 쿠키에 저장된 stringify된 이벤트 정보를 parse하여 다시 요청할 때 보내준다. 당첨된 유저가 후기를 작성했다면 더이상 보상을 줄 필요가 없기 때문에 POST 요청 이후 쿠키를 삭제해준다. 비동기 관련 업무는 처음이었기에 기억에 남았다. 이 과정에서 리드님의 도움도 많이 받았기에 특히 기억에 남았다.



4. 마치며


드디어 그동안 근무하면서 배운 여러가지 사항을 회고할 수 있어서 개운하다. 야근하느라 바빠서 퇴근 이후에도 개인 공부나 회고할 시간이 많지 않았는데, 이렇게 정리하고 보니 정말 많은 일을 했다.


체계가 제대로 잡혀진 기업에서 우리 회사가 일하는 모습을 본다면, 정말 체계없이 일한다고 혀를 찰지도 모르겠다. 그러나 나는 이런 급박한 상황이 좋았다. 대기업에서 일하면서 자신이 무슨 일을 하고 있는 지, 나의 개발이 사용자에게 어떻게 와닿는지 느낄 수 없어 답답했다는 어떤 분의 말씀이 떠올랐기 때문에. (카카오같은 회사는 왜 퇴사하는 걸까?) 시스템이 체계화되고, 내가 담당하는 부분이 거대한 기계의 아주 작은 일부였다면 이만큼 경험을 쌓지 못했을 것이다. 서로 등을 맞대고 운영팀, 백엔드팀, 기획팀, 디자인팀이 수시로 요청 사항을 전달하며 분주하게 개발하는 환경에 익숙해지면서, 내가 어떤 역할을 하고 있고 내가 하는 일이 비즈니스적으로 어떤 의미를 갖는지 어림잡을 수 있었다. (게시판을 활성화 시키고, nft 거래량을 늘리고, 이용자 수를 늘려서 회사 코인의 가격을 올려야했다!)


첫 야근을 하면서 12시간 근무를 했는데, 팀원들이 드디어 나도 야근 생활 시작했다고 농담하던 모습이 떠오른다. 야근 할 때도 열정적인 사수들을 보면서 맡은 일은 끝내고 만다는 직업 의식을 느낄 수 있었다. 또한 프론트 팀은 상대적으로 경력이 짧았음에도 개발 환경 구축, 기술 스택 등등을 결정해가는 사수분들을 보면서 동경심 또한 갖게 되었다.


신입임에도 빠르게 적응할 수 있도록 도와주신 사수님들께 감사 또 감사를 드리며..