본문 바로가기
프로젝트

[ 프로젝트(vanilla-javascript) ] 이거 먹어봄? have-u-tried-this 프로젝트

by YWTechIT 2021. 12. 28.
728x90

✍🏽 프로젝트 살펴보기

deploy: https://elice-kdt-sw-1st-vm02.koreacentral.cloudapp.azure.com/
README: https://kdt-gitlab.elice.io/sw-001-project/team2/have-u-tried-this

🙋🏾 어떤 프로젝트인가요..?

항상 혼자 프로젝트를 만들다가 처음으로 팀 프로젝트를 진행했다. 프로젝트 명은 have-u-tried-this인데 국내 지역별로 맛집을 공유할 수 있는 프로젝트이다. 다른 사이트와의 차이점이 있다면 식당별로 정리되어있지 않고 자치구까지 찾아볼 수 있는 점과 단순히 식당이 어떤지 리뷰하는 것이 아니고 소셜 기능을 통해 소통할 수 있는 공간을 마련했다.


🙋🏾 내가 담당한 기능은..?

 

이미지 업로드와 드래그 앤 드롭 기능을 담당했다. 추가로 단순히 이미지만 올리고 끝나는 것이 아니라 사진에 저장되어있는 EXIF 데이터를 읽고 해당 사진에 geolocation 정보가 들어있다면 위도와 경도의 좌표로 행정구역 정보를 받을 수 있는 카카오API와 연동하여 자동으로 사진 촬영 위치에 value를 채워주는 기능이었다. EXIF(EXchangealbe Image File format)는 교환 이미지 파일 형식인데, JPEG, TIFF 6.0, RIFF, WAV 파일 포맷에서 이용되며, 사진에 대한 정보를 포함하는 메타데이터를 의미한다. 일본 전자산업진흥협회에서 개발되었다고 한다. (자세한 내용은 하단 reference 참조)

 

처음 더미 이미지로 테스트했을 때 EXIF 데이터가 정상적으로 추출되지 않아서 코드를 잘못 작성한 줄 알았는데, 카카오톡이나 디스코드와 같이 SNS를 한번 거쳐간 사진은 개인정보 보호를 위해 EXIF 데이터를 삭제한다고 한다. 그래서 google cloud에 이미지를 올리고 PC로 다운로드를 한 다음 진행했다. MAC기준으로 이미지 파일 우측 버튼 클릭 후 정보 가져오기를 클릭했을 때 추가 정보에 다음과 같은 내용이 포함되어있다면 EXIF 데이터가 정상적으로 들어가 있는 것이다.

 

 

EXIF에서 geolocation 값을 추출했다면 해당 값을 그대로 카카오 API에 넣으면 되는 것이 아니라 좌표를 십진수로 변경해야 한다. 우리가 알고 있는 좌표 위도: 33° 27' 42.258" 북, 경도: 126° 18' 42.672" 동와 같은 형태는 도진수이고 위도: 33.46173889, 경도: 126.31185278와 같은 형태는 십진수이다. 좌표 변환하는 방법은 하단 reference 참조하거나 다음 포스팅을 참고하자.

 

프로젝트 발표를 보면서 다른 팀들은 대부분 구현하지 않았는데 drag&drop을 구현했다. 구현은 MDN을 참고했다. (하단 reference 참조) 그리고 파일 업로드 유효성을 통해 20MB 이하의 사진만 업로드할 수 있고 파일 확장자는 apng, bmp, jpeg, pjpeg, png, tiff, webp로 업로드 할 수 있게 한정지 었다. 나중에는 파일 업로드 시 Content-type(응답 내에 있는 헤더) 속성을 프락시로 이용해 우회하여 원하지 않는 파일을 올리는 기법까지 고려하고 싶다.

 

여담으로 예시로 올린 햄버거 집은 피즈 애월인데 제주 한달살이 했을 당시 먹어본 햄버거 중에 제일 맛있었다. 랜디스 도넛 제주직영점 바로 옆에 있는데 랜디스 도넛을 먹기 위해 웨이팅을 하는데 줄어들 생각을 하지 않는다면 햄버거를 먹으면서 기다리자. 든든하다. 광고 아니다. 내 돈 내산이다.


728x90

🙋🏾 프로젝트를 진행하며 어려운 점과 그것을 어떻게 해결했는지..?

이번 프로젝트에서 가장 어려운 점은 DOM관리였다. 이전까지 혼자 진행했던 프로젝트는 React를 기반으로 진행했지만 지금은 바닐라 자바스크립트를 사용했다. React의 특징 중 하나인 V-DOMDOM관리를 수동으로 하나하나 작업하지 않고 자동화하고 추상화해 준덕분에 편한 기능만 익숙해진 나머지 DOM을 직접 일일이 관리하려니까 어려움이 많았고 처음에는 template engine을 통해 html코드를 내려주는 방법을 생각했는데 결론적으로 SPA로 진행하다 보니 바뀐 DOM을 계속 파악하고 있어야 하는 점이 불편했다. 더불어 코드도 방대해졌다.

 

DOM관리는 innerHTML -> createElement -> Factory function(hyperscript) 순으로 점차 발전했다.

 

처음 innerHTML을 사용했을 때의 장점은 HTML 마크업 문자열이기 때문에 간단하게 DOM조작이 가능했고 구현이 간단하고 직관적이었다. 그러나 HTML 코드에 JS 악성코드가 심어져 있다면 파싱 과정에서 그대로 실행되기 때문에 위험하다. (XSS) 예를 들어 <script> alert(\"hi\")</script>처럼 script태그는 HTML5에서 사용 못하게 막아뒀다고 해도 <img src='' onerror='alert(\"hi\")'>와 같은 코드는 script가 들어가지도 않았는데 alert창이 띄워진다. 또한 기존 노드가 존재하고 자식 노드를 추가할 때 원래 있던 기존 노드까지 새로 만들어 렌더링 한다. 즉, 바뀌지 않아도 되는 노드까지 모두 제거하고 처음부터 새롭게 자식 노드를 생성하여 DOM에 반영하는데 이는 효율적이지 않다. 마지막으로 새로운 요소를 삽입할 때 삽입될 위치를 지정할 수 없다.

 

두 번째 createElement를 사용했을 때의 장점은 innerHTML에서 발생할 수 있는 XSS를 예방할 수 있고, DOM조작을 원한대로 할 수 있다. 그러나 유지보수가 어렵고, 코드가 직관적이지 않아 가독성이 떨어진다.

 

그래서 첫 번째 방법과 두 번째 방법의 절충안인 hyperscript구조를 채택했다. 이는 나중에 배울 React에서 사용하는 JSX 문법과 매우 유사하다. (정확하게 hyperscriptelement에 미리 정의된 attribute에 대해 CSS선택기를 사용할 수 있는 추가 태그 구문을 지원하지만 JSX에서는 그렇지 않다. (reference 5 참조.)) 이를 사용함으로써 순수 DOM을 생성하고, 코드의 가독성과 재사용성이 크게 향상됐다.

// we write this
<div id="foo">
    Hello!
    <br />
</div>

// transpile to this
h('div', { id: 'foo' },
    'Hello',
    h('br')
)

// which returns this
{
    nodeName: 'div',
    attributes: { id: 'foo' },
    children: [
        'Hello!',
        { nodeName: 'br' }
    ]
}

기존에 사용한 innerHTML, createElement, hyperscript를 사용한 코드를 보자 어떤 코드가 가독성이 제일 좋은가?

// innerHTML
const container = `
  <main class="container">
    <form class="image-form" method="post" action="/api/posts">
      <input class="image-form__name" name="name" placeholder="이름을 작성해주세요">
      <button class="image-form__submit">제출</button>
    </form>
  </main>
`

// createElement
const container = document.createElement("section");
container.classList.add("container");
const form = document.createElement("form");
form.classList.add("image-form");
form.method="post";
form.action="/api/posts";
const name = document.createElement("input");
name.classList.add("image-form__name");
const btn = document.createElement("button");
btn.classList.add("image-form__submit");
btn.innerText="제출"

form.append(name, btn);
container.append(form);

// hyperscript
const container = el(
  "section",
  { className: "container" },
  el(
    "form",
    {
      className: "image-form",
      method: "POST",
      action: "/api/posts"
    },
    el(
      "input",
      {
        className: "image-form__name",
        name="name",
        placeholder="이름을 입력해주세요."
      }
    ),
    el(
      "button",
      {
        className: "image-form__submit",
      }
    )
  )
)

우리가 참고했던 hyperscript 코드에는 event 관련 처리가 없었는데 팀장님께서 event 관련 처리 코드를 작성해주셔서 해결할 수 있었다. 그리고 class 대신 className을 사용했는데 그 이유는 JS에서 class가 예약어이기 때문이다.

// hyperscript skeleton code
export default function el(nodeName, attributes, ...children) {
  const node =
    nodeName === "fragment"
      ? document.createDocumentFragment()
      : document.createElement(nodeName);

  Object.entries(attributes).forEach(([key, value]) => {
    if (key === "events") {
      Object.entries(value).forEach(([type, args]) => {
        if (Array.isArray(args)) {
          node.addEventListener(type, ...args);
        } else {
          node.addEventListener(type, args);
        }
      });

      return;
    }

    if (key in node) {
      try {
        node[key] = value;
      } catch (err) {
        node.setAttribute(key, value);
      }
    } else {
      node.setAttribute(key, value);
    }
  });

  children.forEach((childNode) => {
    if (!childNode) return;

    if (typeof childNode === "string") {
      if (childNode.includes("\n")) {
        node.innerText = childNode;
      } else {
        node.appendChild(document.createTextNode(childNode));
      }
    } else {
      node.appendChild(childNode);
    }
  });

  return node;
}

🙋🏾 다음에 뭘 하고 싶은지..?

코드가 점차 구조화되면서 바닐라 자바스크립트가 점점 재밌다는 생각이 들었다. 다음엔 바닐라 자바스크립트에 타입 스크립트를 얹어서 프로젝트를 만들어보고 싶고 매번 상태 관리를 꼭 사용해야하는것은 아니지만 상태관리를 연습해볼 겸 redux를 한번 적용해보고 싶다. 그리고 이번 팀 프로젝트를 통해 뼈저리게 느낀 통일된 코드 컨벤션, 통일된 code style의 중요성인데, 다음 프로젝트 때는 나의 코드로 `ESlint`, `Prettier`를 적용하고 싶고, 마지막으로 `React` 프로젝트는 `CRA`의 도움을 받지 않고 직접 `Webpack`, `Babel` 설정을 나의 코드로 작성하고 싶다.


✍🏽 마치며..

이렇게 해서 2주라는 짧은 시간에 처음으로 팀 프로젝트를 진행했는데, 왜 개발자가 협업이 중요한지 알 것 같다. 프런트엔드에서 보낸 데이터가 제대로 넘어갔는지 확인하기 위해 백엔드와 함께 살펴보기도 하고, 반대로 백엔드에서 보낸 데이터가 프런트로 제대로 넘어왔는지 확인하는 과정, 작은 기능을 수정하더라도 팀원들의 의견을 듣고 더 나은 방향으로 나아가기 위한 토의, commit하는 과정에서 conflict가 났는지 확인하고 앞으로 branch를 생성할지 말지 코드 통일은 어떻게 통일할지 등등 작은 일에도 소통이 되어야 올바른 방향으로 나아갈 수 있기 때문이다. 이번 프로젝트를 통해 바닐라 자바스크립트 문법, 폴더 분리, 모듈화, 웹팩, 컴포넌트, SPA 등을 배울 수 있었고 팀에게 감사하다는 말을 전하고 싶다.

 

reference

  1. exif(교환 이미지 파일 형식): 위키백과
  2. exif-js: 깃허브
  3. 도진수 좌표를 십진수 좌표로 변환: getting-to-know-photos
  4. drag & drop: MDN
  5. script elements inserted using innerHTML do not execute when they are inserted: w3.org
  6. Preact: Into the void 0: JSConf EU 2017
  7. How to Write Your Own JavaScript DOM Element Factory: Kyle Shevlin
반응형

'프로젝트' 카테고리의 다른 글

[ 프로젝트(MERN) ] 깃팜(GitFarm) 프로젝트  (0) 2022.03.19
[ 프로젝트(React) ] 병과 테스트  (0) 2021.07.04

댓글