6개월 프로젝트로 Node.js를 처음 하면서 만났던 여러 문제들을 처리한 내용을 공유합니다.

약 4개월 정도는 backend 개발을 했던 것 같고, 이후 다른 시스템과 함께 맞춰서 프로젝트를 마무리 했던 것 같습니다.

backend에서는 기본적인 CRUD 기능과 유효성 처리, jwt 인증, 파일 업로드(이어올리기, siging작업 등), FOTA 캠페인 진행 등이 주요 내용입니다.

프로젝트 기술 선택

  • Framework: express
  • DB: MySQL

먼저 Node.js에서 유명한 프레임워크로 express와 nest.js가 있습니다.

먼저 Spring Backend에 대한 경험으로 nest.js에 대해 고민했지만, npm trends 비교 및 express가 개발 속도 및 현재까지 사용한 사용자도 많기에 express로 선택했습니다.

images

ORM은 어느 정도의 진입장벽이 있다고 판단하여 typeorm의 기본적인 쿼리로 정도만 넣었으며, native query를 이용하여 동작하도록 설정 후 이후 리팩토링을 했습니다.

개발

돌이켜보면 처음 한달에 삽질을 많이하고 그 뒤로는 어느 정도 프로젝트의 구조가 잡히니 기능 구현에 큰 어려움은 없었습니다.
이후 다른 시스템 지원이 어려웠던 것 같았습니다. c, c++ 프로젝트 ㅠ..

1월 1주차

express 프로젝트 생성을 합니다. 음?? 그런데 기존에 찾아봤던 Node.js의 구조와 다르더군요. 무슨 bin폴더에서 www파일을 이용해서 빌드가 되는데 좀 마음에 안든다. VS Code에서 디버깅은 어떻게 해야하지?

1월 2주차

javascript는 대충 알겠다. 타입이 없어서 좀 마음에 안들지만 디버깅 주도 개발 하면 금방 개발이 되니 좋은것도 같다. Type ORM과 MySQL을 함께 쓸 때 native 쿼리를 쓸 경우는 어떻게 설정하고 release()는 꼭 해줘야 하네. 기본적인 CRUD는 어느 정도 처리할 수 있겠다.

1월 3주차

  • Javascript에서 Typescript로 코드를 변경했다.
  • tsconfig를 통해 Typescript를 쓸 수 있나 보다.
  • enum을 지양하네? 이유가 뭘까? 관련 링크
  • 이 시기부터 이펙티브 타입스크립트 책을 함께 읽었습니다.
  • any 타입에 대한 이해 및 MVC 스타일로 작성할 때 DTO와 Entity에 대한 처리 방법(class-transformer)
  • 반복적인 API 생성 및 유효성 처리 작업
  • PM2 및 DB 커넥션 이슈 등을 만나게 됨
  • 대용량 파일 처리 시 동작 방식에 대한 이해(비동기로 많이 쪼개서 작업하는게 빠르다?)
  • busboy 라이브러리의 close와 finish를 같이 쓰면 linux에서 빌드 시 오류가 발생할 수 있다.
    • req.end 관련 문제 같음

프로젝트 진행하면 만난 문제점들

  • express와 nest.js 중 어떤게 프로젝트에 좋을지

    • 기간이 짧은 경우 express를 사용하는게 좋지만, 유지보수 측면에서는 nest.js가 더 좋을 것도 같다. 해당 프레임워크 선택은 프로젝트 규모와 기능에 대한 검토가 필요함(빠르게 개발은 express가 좋다고 느낌)
  • 프로젝트 구조는 어떻게 설계하는게 좋을지

    • 초반에 express-generator를 이용하여 프로젝트 생성(www폴더가 처음엔 뭔지 몰랐는데 이후엔 좀 거부감..)
    • 자바스크립트를 이용해 기능별로 패키지 분리
    • 이후 타입스크립트로 변경하여 전체 구조 변경(기본적인 nodejs backend 처럼 변경)
    • router와 비슷한 개념을 controller로 파일명 변경(controller에서는 request 유효성 처리 및 response 템플릿 처리)
    • service에서 여러 repository와 연동 및 비즈니스로직 데이터 가공(서비스 로직에 비즈니스로직이 들어가면 테스트코드 작성이 어렵다.)
    • repository에서 orm 쿼리 또는 native쿼리로 service처리
  • DB연동 및 typeorm 적용 여부

    • express는 mongusdb와 궁합이 좋다고 하지만 현재 프로젝트 기간 및 no-sql보다는 mysql를 선택
    • typeorm entity와 관련하여 synchronize 기능 제공(스프링은 ddl-auto 같다)
    • 스프링 처럼 repository 로 orm 스럽게도 구현이 가능하지만 일정 및 n+1 및 fetch? 이슈 등이 있을 거라 생각하여 일단 간단한 쿼리는 orm 복잡한 쿼리는 native로 처리 후 orm 스럽게 리팩토링 진행 예정
  • 타입스크립트에서 오버로딩은 안되는건가?

    • 타입스크립트에서는 타입과 런타임의 동작이 무관하기 때문에, 함수 오버로딩은 불가
  • swagger 문서화 처리

    • express로 사용하는 경우 swagger에 관련된 코드를 그냥 다른 파일로 빼는게 가독성 측면에서 좋아보임
    • 초반에 거부감이 많이 많이 들었지만, 좀 하다 보면 그래도 할만한 느낌 nest.js를 쓰면 코드 어노테이션으로 처리 가능
  • 코드 실행 중 잘못된 호출 또는 로직 수행중에 exception 발생하면 서버가 죽음

    • 추후 PM2로 여러 스레드 작업 및 docker로 분리함
  • typeorm 사용 방법 및 nativequery와 함께 사용하는 방법

  • 타입스크립트 적용 방법

  • C++ 소스코드를 typescript에 선언하고 사용하는 방법

    • node-addon
  • 대용량 파일을 업로드하고 다운로드 하는 방법 및 문제점

    • 파일 업로드 시 busboy를 이용해 chunk값으로 이어올리기 기능 구현
  • request에 대한 유효성 체크 방법

    • npm 중 class-validation과 express-validation이 있다. class-valdaion의 경우 request dto에 어노테이션 을 선언해서 사용하고, express-validation은 라우터에서 처리한다. npm trends를 보면 class-validation을 더 많이 사용함
  • response 응답값에 대한 template 생성 및 유효성 처리를 공통적으로 하는 방법

    • 유효성이 실패한 경우 다양한 형태가 존재하며, 이것에 대한 함수를 나눠 처리함
  • async await의 올바른 사용법과 트랜잭션 처리

    • 쿼리 또는 함수의 응답값이 필요한 경우 await를 이용해 처리함
  • 미들웨어와 유틸함수의 차이점

  • 추후 배포할 때 어떻게 하는게 좋을지 고민 필요

  • 대용량 파일 업로드(10GB 이상) 시 해당 파일 암호화 hash 방법 고민 필요

    • fs.createReadStream을 이용하여 처리

    • createFileSync의 경우 아래와 같은 에러 발생

      1
      2
      3
      4
      5
      
      error: [package/file-upload: File size (12884910080) is greater than 2 GB   
      {"code":"ERR_FS_FILE_TOO_LARGE","stack":"RangeError [ERR_FS_FILE_TOO_LARGE]:  
      File size (12884910080) is  greater than 2 GB\n  at new NodeError (node:internal/errors:372:5)\n  
      at tryCreateBuffer (node:fs:420:13)\n    at Object.readFileSync (node:fs:465:14)\n  
      at PackageService...
      
  • 파일 업로드 이후 메타데이터를 추가해야하는데 어떻게 해야 할지 고민 필요

    • 요청에 따라 KISA SHA 256을 적용(Javascript 예제가 없어서 Java 또는 C++을 이용예정), 다른 암호화 라이브러리로 해도 될 것 같은데 확인 필요
    • cryto를 이용해서 hash처리(signing과 관련된 내용 확인 필요)
    • createReadStream이 비동기이기 때문에 이를 위해 new Promise로 적용
    • 파일 업로드와 동시에 hash 처리 하는 방법도 고민 필요(이러면 중간에 끊기면 hash를 어떻게 처리할지 고민 필요)
  • Typescript에서 문자열 바이트코드 변환 확인

    • TextEncoder() 함수 사용
  • any 타입에 대해서

    • any 타입을 사용하면 타입 체커와 타입스크립트 언어 서비스를 무력화시킨다. any 타입은 진짜 문제점을 감추며, 개발 경험을 나쁘게 하며, 코드의 신뢰도를 떨어뜨린다.
  • Worker Thread

    • 기존 프로젝트에서 C++ 로 처리해야 하는 로직이 있어 addon된 함수를 worker thread 방식으로 적용
    • Node는 요청을 받으면 현재 요청에 대한 다른 요청을 받을 수 없다. 이러한 처리를 효율적으로 하기 위해 Node는 이벤트 루프와 콜백 함수를 통해 비동기로 동작한다.
    • console.log는 메인쓰레드에서 동작하므로 얘로 테스트 하면 안됨
  • 파일 대용량 db업로드시

    • body Parser = 기본 허용치 100kb 이거를 늘려주면 된다.

      1
      2
      
      app.use(express.json({ limit: '50mb' }));
      app.use(express.urlencoded({ limit: '50mb', extended: true }));
      
  • 쿼리 동작중에 Cannot enqueue Query after fatal error.이런 에러가 발생

    • db의 시간 만료를 늘려주면 해결된다는 경우도 있습니다.링크
    • 위와 같이 늘려줬었는데 종종 발생하여 반대로 시간 만료를 극단적으로 줄여서 에러를 확인할 수 있었습니다.
    • 문제는 typeorm과 nativequery를 쓰기위해 repository 파일을 참조하는게 문제의 원인이었습니다.

조금 더 고민해야 할 것

  • 대용량 파일 다운로드 시 효율적인 방법
    • header의 값에 Range를 할당하여 클라이언트에서 여러 호출을 하도록 지정할 수 있음참고