투자관리서비스 어드민

YEONGHUN KO·2022년 12월 2일
1

NEXJS - MINIPROJECTS

목록 보기
3/5

TIL

데이터를 넘겨주는 방식

랜더링 되는 데이터를 한곳에 모두 보관하고 싶다면 컴포넌트는 그 데이터를 안에서 받는게 아니라 바로 한 뎁스 바깥에서 props로 받는게 좋다.

왜냐면 그게 테스트하기 좋기 때문이다. 만약 Headers 컴포넌트에 icons:['a','b','c'] 와 같은 데이터를 받는다고 하자. icons를 전역화 시켜놓고 Headers 컴포넌트안에서 받아버리면 테스트할때 조금 까다로워진다. storybook에서 테스트를 진행할때 args를 통해 data를 넘겨주도록 되어있기 때문이다.

그래서 전역화된 icons를 Headers바로 바깥에서 선언하고 dependency로 Headers에 prop으로 넘겨주는 것이 테스트하기 편하다.

또한 전역화가 나와서 바로 얘기를 하자면, Headers가 위치한 곳은 어디가 될지 모른다. 5뎁스가 될지,1뎁스가 될지 모른다는 이야기이다.

그래서 icons를 전역화 시켜놓고 Headers 바로 바깥에 선언을 하는게 깔끔하다는 것을 배웠다.


그리고 정적인 데이터만 data안에 넣는게 좋을것 같다.

  • tableBody같이 react query를 이용해서 자주 바뀌는 건 어쩔 수 없이 tableBody props를 통해 따로 주입해주었다.

typescript

기존 리액트 엘리먼트의 attribute 타입을 일단 기본적으로 모두 상속하는 법을 알았다.

LoginInput의 prop interface를 아래와 같이 지정해주었다.

interface LoginInputType {
  placeholder:string;
  variant: INPUT_VARIANT;
  type: InputType;
  [key: string]: any;
}

placeholder라는 input의 기본 타입이 보인다. 현재로선 기존 input과 겹치는 prop이 이거 하나밖에 없지만, 개발하다보면 기존 input prop을 더 추가해야하는 일이 생길 수 있다.

이럴때는 기존 input attribute prop을 상속받으면 해결된다.

interface LoginInputType extends React.HTMLAttributes<HTMLInputElement> {
  variant: INPUT_VARIANT;
  type: InputType;
  [key: string]: any;
}

이로서 마음이 더욱 든든해졌다.

에러

@types

type을 types라는 폴더에 모아두고 tsconfig.json에 alias를 @types라고 하였다. 그러니 아래와 같은 에러가 생겼다.

path alias를 @types 라고 해서 생겨난 오류이다. 이렇게 이름을 지으면 타입 라이브러리처럼 사용한다는 이야기이다. 그러고 보니 라이브러리 중에 @types로 시작하는 라이브러리를 본적이 있다!

그래서 @type으로 폴더를 바꾸어주니 해결되었다.

아래 링크는 @types폴더 안에 타입 라이브러리를 만드는 방법이다.

타입라이브러리

window not defined

recoil persist를 사용하면서 sessionStorage를 적용해야했다. server에서 페이지를 만드는 과정에서 당연히 인식할 수 없다. 따라서 아래의 코드를 적용해주었다.


const sessionStorage =
  typeof window !== 'undefined' ? window.sessionStorage : undefined;

const { persistAtom } = recoilPersist({
  key: 'accountQueryParams',
  storage: sessionStorage,
});

pageProps가 안넘어옴

accounts.ts안에, getServersideProps에서 넘겨준 prefetch된 queryClient가 props를 통해 넘어오지 못해서 몇시간 동안 애를 먹었다.

이유는 account.ts가 non-page라서 그랬다. pages/accounts/accounts.ts 여서 그랬던것 같다. pages/accounts.ts로 폴더구조를 옮겨주니 잘 되었다.

hydration error

Hydration failed because the initial UI does not match what was rendered on the server

NavLink만들때 위와 같은 에러가 나왔다. 예전 linked in clone했을때 맞딱뜨렸던 에러이다.

next/link안에 styled.a 태그를 랜더링하고 active에 따라서 스타일을 다르게 처리하는 코드를 짜고 있었다.

서버와 클라이언트에서 랜더링하는 페이지가 다르다는 이야기인데 아무리 비교해봐도 똑같았다.. 결론부터 얘기하면 Link안에 a태그를 랜더링할경우 Link에 passhref prop을 넘겨주어야한다....

<Link href={href} passHref legacyBehavior>
  <S.Anchor className={activeClass}>
     <Icon icon={type} size={size} />
     {children}
  </S.Anchor>
</Link>

그럼 실제 브라우저에선 a태그가 1개만 랜더링 된다.

공식문서에 그대로 나와있었다.. 공식문서에 적혀있는것을 그냥 넘어가지말고 '왜 passHref 가 있을까?' 하면서 하나하나 유심히 보는 연습을 하자

The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

숫자가 아닐 수 있는 요소끼리 사칙연산을 하게 될때 나타나는 에러.

pagination의 totalNumber를 구하려고 할때 맞딱뜨린 에러이다.

  const totalPagesNumber = data?.headers["x-total-count"]  / 20 

간단하다.
total-count 부분을 숫자로 만들어주면 된다. 그리고 data?.headers["x-total-count"] 가 undefined일 수 도 있으니 이것도 고려해주면 된다.

 const totalPagesNumber = data?.headers["x-total-count"] && parseInt(data?.headers["x-total-count"])  / 20 

아쉬운점

폴더구조

현재 폴더구조는 대략 아래와 같다.

뭐 나쁘진 않다. 근데 component위주로 project를 시작했던터라 component별로 관련 util,type,hook이 한곳에 묶여있었으면 어땠을까 하는 아쉬움이 있다.

그래서 담엔 component안에 관련파일을 다 보관하면 어떨까 싶다. 예를 들어, atoms안에 button 컴포넌트같은 경우 아래와 같이 파일을 만드는 것이다.

components                           
├─ atoms                             
│  ├─ Button    
│  │  │  ├─ hooks
│  │  │  ├─ quries
│  │  │  ├─ types
│  │  │  ├─ main
│  │  │  │  ├─ Button.stories.tsx          
│  │  │  │  ├─ Button.styles.ts            
│  │  │  │  ├─ Button.tsx                  
│  │  │  │  └─ index.ts                    

그럼 button과 관련된 다른 파일을 찾으러 다닐때 이곳저곳 옮겨다니지 않고 component안에만 들어가면 되는 것이다.

또한 component안에서 또 pages별로 atomic design을 분류했다면 좀 더 간소화 되지 않았을까 싶다. atom안에 폴더가 수십개가 될 수 있으니 말이다.

비즈니스 로직과 완벽하게 분리되어있지 않음

DropDown 컴포넌트를 살펴보자.


const DropDown = ({
  data,
  label,
  updateQueryParamsOnDropDownChange,
  queryParams: { broker_id, is_active, status },
}: DropDownType) => {
  const [selectVal, setSelectVal] = useState('');

  const handleChange = (e: SelectChangeEvent<any>) => {
    const { name, value } = e.target;
    updateQueryParamsOnDropDownChange(name, value);
  };

  useEffect(() => {
    switch (label) {
      case 'broker_id':
        setSelectVal(broker_id || '');
        break;
      case 'is_active':
        setSelectVal(is_active || '');
        break;
      case 'status':
        setSelectVal(status || '');
        break;

      default:
        console.log('invalid drop name');
        break;
    }
  }, [broker_id, is_active, status, label]);
  
 return <Component/>
   
}

결국 다른 데이터가 들어오면 에러를 뿜어내거나 원하는대로 동작하지 않을것이다. 음... MUI처럼 완전히 분리하는 방법이 뭐가 있을까??

성능 개선

페이지를 랜더링할때 react + vite를 썼을때보다 많이 느렸다... prefetch로 서버에서 data를 받아오기도 하고, middleware로 cookie유무를 확인하는 것도 있어서 느려진것 같다. 그리고 webpack이 vite보다 느리다.

페이지가 별로 없으니 SSG로 만들면 더 빠를라나 모르겠다.

앞으로 해야 할 일

  • 아쉬운 점을 개선
  • 사용자목록, 사용자상세 페이지 구현
  • aws의 ec2를 이용해서 json server와 함께 서비스배포

느낀점

뿌듯

일단 굉장히 뿌듯하다. 아키텍쳐를 손본다는게 쉬운일은 아닌데, 새로 배운 디자인 시스템을 도입하여 아키텍쳐를 어느정도 개성하였고 스토리북으로 문서화, 테스트 까지 경험해보았다.

관심사 분리

자주 쓰는 것은 분리해서 함수나 변수로 만들면 굉장히 편하다는 것이다. 사실 이전에 잘 체감하지 못했는데 이번에 constant나 globalThemes를 써보면서 확실히 느꼈다.

되도록이면 라이브러리에 의존하지 않기

MUI, react-hook-form같은 라이브러리는 언뜻 보기에 편리해 보이지만 에러가 발생하면 디버깅하기 까다롭다. 왜냐면 내가 만든것이 아니기 때문. 그리고 MUI같은 경우 기존에 제공하는 옵션에서 벗어나서 스타일을 바꾸려할경우 폰트가 깨진다거나 크기가 늘어나거나 하는 문제가 생긴다. 그래서 그런가 styled-component와 부드럽게 융합되지 못하는것 같은 느낌을 받았다.

그래서 되도록이면 내가 처음부터 끝까지 완벽히 이해할 수 있도록, 그리고 변경하기 쉽도록 만드는것이 제일 베스트라는 것을 알았다. 그리고 알고리즘 연습도 되고 나름 재밌다. Pagination을 만들면서 재밌었다!

profile
'과연 이게 최선일까?' 끊임없이 생각하기

0개의 댓글