본문 바로가기

일상

2024 연말 재활 프로젝트

이 프로젝트를 만든 이유

 

퇴사 후 나는 가고 싶었던 곳으로 여행도 가고 배워보고 싶었던 수영도 등록해 보는 등 그 동안 시간이 없어서 못했던 일들을 해보며 지쳤던 심신을 달랬다. 쉬는 동안에도 감을 잃어버리지 않게 간간히 코딩테스트 했지만 아무래도 직접 실무를 뛸 때보다 부족하다 생각을 해왔다. 그래서 간단하게 손풀기용 프로젝트를 하나 만들었다.

사용한 기술 스택
프로젝트 생성

 

프로젝트를 기획하고 가장 먼저 한 일은 프로젝트를 생성하고 프로젝트를 Git 원격 저장소에 올리는 일이었다.

먼저 vite를 설치하고 프로젝트를 생성한다. 터미널창에 프로젝트 생성 명령어를 입력하면 옵션을 선택하도록 뜨는데 나는 React + Typescript를 선택했다.

// vite 설치
npm create vite@latest
// vite 프로젝트 생성
npm create vite@latest [프로젝트 명]

 

그 후 생성된 프로젝트를 열고 라이브러리 설치를 하고 프로젝트가 잘 실행되나 확인했다.

// 라이브러리 설치
npm install
// 프로젝트 실행
npm run dev

 

그 다음 내 Github 계정에 새 리포지터리를 만들고 생성한 프로젝트와 연동을 해줬다.

연동 방법은 간단하다 먼저 프로젝트에서 로컬 저장소를 만든다. 그 후 원격 저장소와 연동하고 로컬 저장소에 내용을 전부 커밋한 뒤 원격 저장소로 푸시를 날리면 된다.

// 로컬 저장소 생성
git init
// Github 원격 저장소와 연동
git remote add origin [Github 원격 저장소 주소]
// 로컬 저장소에서 변경된 내용 전부 스테이징으로 이동
git add .
// 로컬 저장소에서 커밋하기
git commit -m [커밋 메시지]
// 원격 저장소로 커밋 push
git push origin main

 

공공데이터포탈에서 Open API 사용

 

처음에는 api 없이 mock 데이터만으로 구현하려 했는데 axios나 react query도 재활이 필요할 것 같고 open api도 한번 사용해보고 싶어 공공데이터포탈에서 open api 하나를 활용신청 하였다. 신청한 api는 "시흥도시공사_태양광발전생산정보조회서비스" 였는데 공공데이터포탈에서 최근에 방문한 지역을 검색했을때 가장 첫번째로 조회된 api 였기때문이다. 신청 후 바로 승인났고 발급받은 인증키로 api를 호출했을때 정상적으로 호출이 되었다.
문제는 이 다음 이었는데 데이터 포멧이 xml만 지원이 되었던 것이었다. xml 형식을 다뤄본적이 없어 당황했지만 xml을 json으로 바꿔주는 라이브러리가 있을거라 판단하였고 다행히 적당한 라이브러리를 찾아 적용하여 json 형태로 변환하기까지 성공했다.  

{
    "declaration": {
        "attributes": {
            "version": "1.0",
            "encoding": "utf-8",
            "standalone": "yes"
        }
    },
    "elements": [
        {
            "type": "element",
            "name": "response",
            "elements": [
                {
                    "type": "element",
                    "name": "header",
                    "elements": [
                        {
                            "type": "element",
                            "name": "resultCode",
                            "elements": [
                                {
                                    "type": "text",
                                    "text": "00"
                                }
                            ]
                        },
(생략)

 

위와 같은 json 형식으로 변환하였지만 쓸데없이 많은 depth가 생겨버려 필요한 값을 가져다 쓰기 어려운 구조가 되어버렸다.

때문에 나는 재귀함수를 사용해 위 데이터를 간소화 하는 코드를 추가했다.

 

코드은 간단하다 매개변수 elements의 첫번째 depth의 "text" 요소(elements[0].text)가 있을 경우 그 값을 반환하고 없을 경우 reduce를 사용하여 elements를 object 형태로 재구축한다. 이때 key값은 "name" 요소를 넣고 value값에는 재귀함수를 넣어 결과적으로는 "text" 요소를 반환할 수 있도록 한다. 또 value 값 형식이 object가 아닌 array가 될 수 있기에 arrForm에 array 형식으로 처리할 부분의 "name"을 추가하였다. 

// src\services\getSolarPowerProdList.ts
// xml-js로 바꾼 json을 간소화
const setSimpleToJson = (
  { elements }: { elements: any },
  arrForm: string[]
) => {
  if (!elements) return;

  return !elements[0]?.text
    ? elements.reduce((tot: Record<string, any>, el: any) => {
        tot[el.name] = arrForm.includes(el.name) // items는 배열로 표시해야 함으로 예외처리
          ? el.elements.map((v: any) => setSimpleToJson(v, arrForm))
          : setSimpleToJson(el, arrForm);
        return tot;
      }, {})
    : elements[0]?.text;
};

 

setSimpleToJson 함수를 사용하여 간소한 결과

 

위 코드를 작성하면서 문제가 하나 더 발견되는데 바로 일일 요청 가능 횟수가 있단 점이었다.

정확한 횟수는 모르겠으나 호출 횟수가 50회쯤이 넘어가면 "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR" 란 에러 메시지만 오면서 간헐적으로 데이터가 반환되는 상태가 되어버린다...(OMG)

다른 api로 교체할까 생각했지만 만들어놓은 함수가 아까워 그냥 mock 데이터를 만들어 구현하기로 했다.

CSS 라이브러리 사용

 

이 프로젝트에서는 Material ui를 사용했다. 사용한 이유는 가장 익숙한 CSS 라이브러리 이기도 했지만 어느 정도 디자인이 입혀져 있어 디자인 공수가 덜든다는 점과 아이콘이나 테이블 등의 기능을 무료로 제공된다는 점 때문에 사용했다. 

기본으로 제공되는 CSS 옵션으로만 사용하려 했으나 레이아웃 위치를 잡기 위해 필요한 flex 속성부터 지원을 안해주었다. (Antd에는 Flex 라는 컴포넌트가 따로 있더만..-_-;) 어쩔수 없이 커스텀을 해야 했고 코드가 늘어나기 시작했다.

<Box sx={{ 
    width: "100%",
    height: "100%",
    display: "flex",
    flexDirection: "column",
    alignContent: "start",
}}>
	<Box sx={{
          display: "flex",
          width: "100%",
          height: 70,
          background: blue[500],
          alignItems: "center",
          gap: "10px",
        }}>
	</Box>
</Box>

 

나는 위 예시처럼 길어지는 코드를 해결하기 위해 Styled Components로 CSS를 분리하려 했다.

import S from "./main.css";

<S.MainBox>
  <S.HeaderBox></S.HeaderBox>
</S.MainBox>
// main.css.ts
const MainBox = styled(Box)({
  width: "100%",
  height: "100%",
  display: "flex",
  flexDirection: "column",
  alignContent: "start",
});

const HeaderBox = styled(Box)({
  display: "flex",
  width: "100%",
  height: 70,
  background: blue[500],
  alignItems: "center",
  gap: "10px",
});

 

Styled Components로 HTML 코드의 가독성은 지켜졌으나 다른 문제가 발생했다.
MUI를 인라인 방식이 아니라 Styled Components로 감쌀 경우 CSS 적용 우선순위가 밀려버려 일부 스타일이 적용이 안되는 것이다. (예를 들어 Typography를 Styled Components로 감쌀 경우 폰트 굵기나 사이즈 조정이 안됨, MUI 기본 CSS의 우선순위가 더 높기 때문) "!important" 선언을 사용하면 스타일 적용이 되지만 일일이 선언을 해줘야한다는 문제가 있다.

고민한 결과 나는 코드 가독성을 생각해 어느 정도  Styled Components를 사용하지만 그 외에는 classname 사용하는 방식으로 인라인 적용이 되게끔 사용을 했다.

// main.tsx
<S.MainBox> // styled로 감싼 부분
  <S.HeaderBox> 
    <LightModeIcon sx={S.sunIcon} /> // classname 방식을 사용한 부분
    <Typography sx={S.headerTitle}>시흥 태양광발전소 정보 조회</Typography> 
  </S.HeaderBox>
  <S.SubContentsBox>
 (중략)
// main.css.ts
const MainBox = styled(Box)({
  width: "100%",
  height: "100%",
  display: "flex",
  flexDirection: "column",
  alignContent: "start",
});

const HeaderBox = styled(Box)({
  display: "flex",
  width: "100%",
  height: 70,
  background: blue[500],
  alignItems: "center",
  gap: "10px",
});

export default {
  MainBox,
  HeaderBox,
  sunIcon: {
    color: "#fff",
    fontSize: "50px",
    marginLeft: "10%",
  },
  headerTitle: {
    color: "#fff",
    fontWeight: 700,
    fontSize: "30px",
  },
};

 

결과