bandal.dev

2023년 회고

올 해 부턴 매년 회고를 남겨보기로 했다.

21
회고

회사에 입사한지도 이제 만 2년이 되어간다.
블로그에 마지막 글을 작성한지 벌써 3개월이 지났다.
핑계를 대자면 요즘은 새로운 개인 공부로 코드를 작성하거나, 책을 읽거나, 위스키에 시간을 쓰고 있다.
그래도 이렇게 회고록을 남기며 글을 갱신해보려고 한다.

240213-173713

블로그의 조회수는 어느덧 15K를 넘겼다. 한참 블로그를 작성할땐 매일 조회수가 얼마나 올랐나 확인했었는데, 오히려 블로그 작성이 뜸해진 10월부터 조회수가 늘어났다.

프론트 엔드 개발자로 나아가고 있는데 대부분 조회수는 백엔드와 인프라 부분이다. 프론트엔드 관련 매력적인 글을 작성해서 벨런스를 맞춰야겠다.

새로운 프로젝트의 시작

2023년의 프로젝트는 내가 프론트엔드 개발자로써 코드를 작성하는 레벨을 바꿔준 프로젝트였다.
새로운 프로젝트는 사내 솔루션의 모니터링 기능을 제공하여 운영/관리 효율을 높이는 것이 목표였다.

프로젝트 인원 구성의 특징

새로운 프로젝트를 작성하기에 앞서, 프로젝트 인원별 역할의 특이한 점이 있다.

  1. 개발자가 기획도 하고 개발도 한다.

해당 프로젝트를 구현하려면 사내 솔루션을 직접 고객사에 배포해보면서,
외부에서 솔루션의 데이터를 어떻게 호출하는지, 내부 솔루션간 네트워크 통신은 어떻게 이루어지는지 등을 알고있어야한다.

비록 솔루션을 배포해본 경험은 한번 뿐이였지만, 일단 솔루션의 구조를 어느정도 파악하고 있었기에, 기획 단계에서 여러가지 방안을 제시할 수 있었다.
거기에 더해서, 다른 모니터링 솔루션의 아키텍처와 UI를 분석해보고, 고객사나 회사의 시니어 분들을 통해 평소 필요하다 생각하셨던 기능들을 조사해보며 기획을 진행했다.

  1. 프론트엔드와 백엔드를 구분하지 않는다.

개발자에게 클라이언트/서버에 대한 전반적인 도메인/프로젝트 지식 이해를 장려했던 것 같다.

난 프론트엔드 개발을 메인으로, 다른 개발자는 백엔드 개발을 메인으로 담당하였지만,
서로 작성한 코드를 상대에게 알려주고,
서로 작성해야할 부분을 공유하였으며,
서로 작성한 코드를 리뷰하며 진행하였다.

서로 어떤 업무를 수행하고 있는지 긴밀하게 공유하며 프로젝트를 진행해서, 각 FE, BE 대한 이해도가 높아질 수 있었던 것 같다.

프로젝트의 진행 방식

기본적으로 작은 규모의 기획, 이를 바탕으로 개발하는 방식으로 진행되었다.
순서대로 기술하자면,

  1. 구현할 기능을 작성한다.

큰 틀의 필요한 기획을 전달받으면, 그 안에서 세부적인 기능들을 정의한다.
주로 다른 모니터링 툴을 참고하여 세부 기능을 작성하곤 했었다.

  1. 디자이너와 UI를 작성한다.

기획을 바탕으로 화면 설계서를 작성하고, 디자이너와 공유하여 Figma 툴을 통해 완성된 디자인을 전달받는다.

  1. 바로 개발을 시작한다.

하지만 디자인 작업이 완료될 때 까지 기다릴 수는 없었다.
따라서 프론트엔드를 주로 담당했던 나는, 디자인 부분만 제외하고 기능 동작 구현을 위주로 바로 개발을 시작했다.

기능을 구현하면서 API 명세를 같이 만들고, 디자인이 오면 기능만 구현했던 컴포넌트에 디자인을 적용하는 방식으로 유기적으로 협업하며 개발을 진행했다.

그래서 뭘 만들어봤나?

  • 프론트엔드
    • Vanilla JS 기반 사내 디자인 시스템을 React로 재구현
    • ECharts 라이브러리를 활용한 차트 스트리밍 대시보드 구현
    • React Query + Error Boundary를 통한 API 캐싱 및 예외 처리 로직 구현
  • 백엔드
    • J2EE Servlet 기반 API 핸들러 구현
    • API 명세 작성
    • Enum 기반의 공통 API 응답 코드 정의
    • 일부 서비스 로직 구현 및 DB 쿼리 작성 등등..

그 중 가장 기억에 남는건?

Vanilla JS 기반 사내 디자인 시스템을 React로 재구현

막연하게 코드를 작성하던 시절에서, 구조를 고민하고 패턴을 적용하며 코드를 작성하게 된 계기가 되었다.

기존의 사내 디자인 시스템은 Vanilla JS로 작성되었고, Storybook을 통해 문서화하고 있었다.
하지만, 신규 프로젝트는 React를 도입하였다보니, Storybook과 기존 코드를 참고하며 React로 재구현해야 했다.

초기엔 막연하게 컴포넌트를 구현했더니, 새로운 요구사항이 생기면 기존에 만들어둔 구조를 변경하기 일쑤였고, 이를 사용하던 다른 곳에 영향을 주는 경우가 많았다.
컴포넌트가 다른 곳에서 어떻게 사용할지 예상하는 것은 쉽지 않았다..

그래서, 오픈 소스로 Best Practice가 있는데 비슷한 기능을 구현해야할 경우 유명한 오픈소스 라이브러리를 조사해보며 사용자들에게 어떻게 편리하게 사용할 수 있도록 만들었나 참고하였다.
컴포넌트를 어떤식으로 설계했고, 필수 props와 option props를 어떻게 나누었는지 등을 참고하여 구현하였다.

그럼에도 특수한 요구사항이 생기게되고, 문제를 반복하고 있었는데..

그 때 도움이 되어준게 아래의 우테코 영상이였다.

영상을 보고 바로 컴포넌트 IoC 패턴을 도입해봤다.
기본 컴포넌트를 구현하고, 내부에서 필요한 로직을 Custom Hook으로 분리하여 사용하는 방식이였다.

interface DrawerClose {
    onClose: () => void;
}
 
interface DrawerProps extends DrawerClose {
    isExpanded: boolean;
}
 
interface DrawerContext {
    drawerState: [DrawerClose[], Dispatch<DrawerClose[]>];
}
 
const drawerContext = createContext<DrawerContext>( {} as DrawerContext );
 
export default function Drawer( props: PropsWithChildren<DrawerProps> )
{
    const {
        isExpanded,
        children,
        onClose,
    } = props;
 
    const drawerRef = useRef<HTMLDivElement>( null );
 
    return <Modal type="drawer">
        { isExpanded && <>
            <>...기본 구성요소들</>
            {children}
            <Shortcut keyCode="Escape"
                event={onClose} />
        </>
        }
    </Modal>;
}
 
export const useDrawer = () =>
{
    const { drawerState } = useContext( drawerContext );
 
    const [isExpanded, setIsExpanded] = useState( false );
 
    const onOpen = () =>
    {
        if( isExpanded ) return;
 
        // ...open 로직
    };
 
    const onClose = () =>
    {
        // ...close 로직
    };
 
    return {
        isExpanded,
        onOpen,
        onClose,
    };
};
 
export const DrawerProvider = ( props: PropsWithChildren ) =>
{
    const drawerState = useState<DrawerClose[]>( [] );
 
    return <drawerContext.Provider value={
        {
            drawerState,
        }
    }>
        {props.children}
    </drawerContext.Provider>;
};

컴포넌트를 사용할 때, 추가적인 로직이 필요하면 Custom Hook으로 받은 메인 로직과 합쳐서 사용하면 되었다.

export default function App()
{
    const { isExpanded, onOpen, onClose } = useDrawer();
 
    const handleOpen = () =>
    {
        // ...추가 로직
        onOpen();
    };
 
    return <>
        <Button onClick={handleOpen}>Click Me</Button>
        <Drawer isExpanded={isExpanded}
            onClose={onClose}>
            <>...Content에 채워넣을 것</>
        </Drawer>
        </Button>
    </>;
}

Modal을 통하는 컴포넌트를 구현할 때가 가장 머리가 아팠는데, 위 패턴 적용 이후엔 어느정도 해결되었고,
이 때 부터 좋은 코드를 많이 봐두면 좋은 코드를 쓰게 된다는 생각으로, 평소 오픈 소스나 빅테크의 기술 영상과 블로그를 많이 찾아보게 되었다.

React Query + Error Boundary를 통한 API 캐싱 및 예외 처리 로직 구현

다양항 요구사항들을 처리하며 복잡해지던 코드 구조를 React Query와 ErrorBoundary + Suspense 패턴을 알게되고, 코딩 스타일이 많이 개선되었다.

관제센터에 띄우기 위한 목적이었기 때문에, 사용자의 조작이 없더라도 화면이 멈추면 안됐고, 고객사마다 다양한 환경으로 인해, 예상못한 에러를 핸들링할 수 있는게 중요했다.

기존에는 React Query의 isLoading을 통해 로딩 상태를 관리하고, 에러 핸들링은 isError, error를 열어보거나, Response 받은 data에서 에러 메시지를 추출하여 사용하였다.

그러다보니 코드가 길어지고, 명령형으로 작성하고, 난잡해지는 코드가 많았다.

import { useQuery } from 'react-query';
 
function MyComponent() {
  const { data, isLoading, isError, error } = useQuery('myData', fetchData);
 
  if (isLoading) {
    return <Spinner />;
  }
 
  if (isError) {
 
    // API에서 가공된 error 코드/메시지를 줬을 때
    if( data?.error ) {
      return <UserFallback error={data.error} />;  
    }
 
    return <UserFallback error={error} />;
  }
 
  return <DataComponent data={data} />;
}
 
async function fetchData() {
  // 데이터 요청 로직
}

위에 처럼 작성하던 코드를 개선하던 과정이 있었는데, 관련해서 포스팅했었던 글을 남겨둔다.

회사 DB에 OOM 발생시키기

아마 평생 잊을 수 없을 것 같다..

모니터링을 위해 기록된 로그성 DB 데이터가 너무 많이 쌓이니, 특정 주기마다 오래된 데이터를 삭제하는 로직을 구현했었다.
그것은 DELETE FROM TABLE WHERE DATE < ? 쿼리였는데.. 그땐 DB 트랜젝션에 대한 이해가 부족했었다.
DELETE 쿼리는 rollback을 위해 트랜잭션이 쌓이게 되었고, 트랜잭션의 수가 많아지자 여럿이 공유하던 DB에서 OOM이 발생한 것이다.

수석님들과 CIO님까지 모여서 원인을 찾기 시작했고, 그 원인이 내가 만들었던 쿼리라는 것을 알게 되었을 때.. 후회하긴 늦었었다 ㅎ..
덕분에 쿼리를 날리기 전에 한번 더 고민해보는 습관을 경험을 통해 몸소 배우게 되었다.

다시는 이런 사고를 치지 말자..

아쉬웠던 경험

좀 더 고민하고 조사하고 체계적으로 진행했더라면 더 좋은 제품을 만들 수 있었을 것 같다.

빠르게 완성본을 보여주기 위해 작은 기획과 개발의 반복으로 큰 틀 없이 진행되었고, 개발 중 새로 알게된 기술은 적용하기엔 시간이 부족했다.
그로 인해 이전 개발 내용이 엎어지거나, 코드 재사용이 불가능하거나, 중복 코드가 발생하기도 했다.

전반적으로 코드의 퀄리티가 떨어진다고 느꼇고, 제품 전반적인 품질 감소되어, 각 기능간에 매끄럽게 연결되지 못한 부분이 많이 보였다.
또한 큰 그림 없이 작은 기획에만 집중하다보니 새로운 기획 아이디어가 고갈되었고, 결국 추가로 개발은 잠정 중단되었다.

가장 큰 아쉬운 점은 WebSocket을 적용할 수 없기에 polling을 택했지만, STOMP라는 프로토콜을 알았더라면 실시간 데이터 처리에 더 좋았을 것 같다는 점이다.
만약 리펙토링을 과제로 갖고가게 된다면 STOMP를 가장 먼저 적용해보고 싶다.

향 후 프로젝트에선 큰 기획을 먼저 잡음으로써, 프로젝트의 목표를 명확히 정하고, 체계적인 프로세스를 통해 개발을 진행해야겠다.
또한 요구기한안에 끝내야하는 이상, 일단 완성하고 나중에 기획/디자이너분께 리펙토링 양해를 구하긴 어려웠다.
평상시에도 좋은 코드 퀄리티를 유지할 수 있도록 나 자신을 성장시켜야겠다.

서비스 기업에 가고싶어졌다.

사용자의 직접적인 피드백을 보고 싶어졌다.

지금 회사의 제품은 고객사에게 판매하고, 고객사의 폐쇄망에 배포하는 구조였기 때문에, 사용자의 피드백을 직접적으로 받을 수 없는 환경이였다.

웹을 개발할 때 UX를 고려하며 코딩을 하지만, 사용자의 피드백을 받지 못하니 불편한 부분이나, 요구사항을 파악하기가 어려웠고, 사용자의 요구사항을 파악하지 못하니, 사용자가 원하는 기능을 구현하기가 어려웠다.

그때문인가 혼자 매너리즘에 빠져가는 느낌도 드는 것 같았다.

서비스업은 Mixpanel이나 Google Analytics 등 다양한 툴을 통해 사용자의 행동을 분석해보거나, 리뷰를 찾아볼 수 있다는 점이 서비스 기업에 가고싶게 만들었다.
좋은 리뷰를 보고 성취감을 느끼거나, 건의 사항을 받고 개선해나가는 과정을 경험해보고 싶다.

서비스 기업은 웹 접근성, SEO, 반응형 웹을 요구하지만, 다 나에겐 부족한 부분들이였다.
지금의 회사에선 모두 불필요하고, 개발 시간만 늘릴 오버 엔지니어링이였기 때문이다.

하지만, 서비스 기업을 목표한 이상 이런 역량들을 키워나가야만 한다.

이력서 웹사이트 만들기

그 역량을 키우기 위한 목표로 직접 이력서 웹사이트를 만들었다.

기본 구조는 유명 구직 사이트의 템플릿을 참고하면서, 필요한 부분들을 추가하였고, 이력서를 PDF로 다운로드 받을 수 있도록 구현하였다.
익숙한 Tailwind를 사용하여 반응형 웹을 구현하였고, 시멘틱 태그와 aria, alt 속성을 적용하여 웹 접근성을 고려하였다.

다 만들고 보니 Lighthouse 올 100점이 나오고 폭죽 이펙트도 볼 수 있었다.

240213-173812

코딩 테스트 준비하기

코딩 테스트에 대한 준비가 하나도 안되어있었다.

과거 우연히 넣었던 기업에서 라이브 코딩 테스트를 보게되었는데, 문제를 하나도 풀지 못하고 끝마쳤다..
그 때 좌절감을 크게 맛보고 한동안 멘탈이 나가있었다..

라이브 코딩이 끝나고, 면접관께서 질문할게 있으면 하라고 하셨었는데, 라이브 코딩도 못풀고 질문은 뭐하러 그렇게 많이 했는지 모르겠다. ㅋㅋ;

그 계기로 집념이 생겼고, 코딩테스트를 준비하기 시작했다.

연습의 덕분에 최근엔 시리즈 C 기업의 코딩테스트도 무난히 통과할 수 있었다. (통과했지만, 면접은 ㅎㅎ;)

마무리

다음 목표는 서비스 기업에 입사하는 것이다.

하지만 요즘 채용시장이 얼어붙었다고한다. 개발자 커뮤니티를 찾아보니 경력 이직도 서류 100 ~ 200곳은 돌린다는데.. 아직 물 들어올 때가 아닌 듯 하다.

시장이 조금 풀릴 때 까지 웹앱을 하나 더 만들어보려한다.

공고를 조사해보니 최근 Next.js의 수요가 가장 높은 것 같다.
Next.js로 구현된 photoshot, cal.com, next-ai-news등 과 같은 유명 오픈 소스 프로젝트들을 분석해보며, 사이드 프로젝트를 만들어보려한다.

이 후 커뮤니티에 올려서 홍보해보면 겸사겸사 포트폴리오가 될 것 이고, 원하는 사용자의 직접적인 피드백도 받을 수 있을 것 같다.