코로 넘어져도 헤딩만 하면 그만

node express와 씨름한 기록 (feat.백엔드 쪽이 처음인 당신) 본문

Etc

node express와 씨름한 기록 (feat.백엔드 쪽이 처음인 당신)

꼬드리 2024. 5. 3. 18:16

 

이번에는 백엔드에 영 초짜인 사람이 node express를 깔면서 에러들과 씨름한 기록을 정리해보려 합니다.

핵심만 요약하자면 node와 express를 쓰는 중, 다음과 유사한 에러 문구들을 만난 분께 유용할 수 있습니다. 


''함수명' is assigned a value but never used. eslint@typescript-eslint/no-unused-vars
Cannot redeclare block-scoped variable '함수명'.ts(2451)
crawler.tsx(3, 16): '함수명' was also declared here.'

 

Error: Cannot find module '/home/user/twiroller/twiroller/backend/index.js'

 

Module '"/home/user/twiroller/twiroller/node_modules/@types/express/index"' can only be default-imported using the 'esModuleInterop' flagts(1259) index.d.ts(128, 1): This module is declared with 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.


당장 문제 해결이 급하다면 참고하세요.

 

1) TS를 쓴다면, ts 파일은 수동 컴파일한 다음 생성된 js파일로 서버를 돌려야 한다.(package.json에서 설정 바꿔 자동화 가능) 직접 ts 파일로 돌리고 싶을 시 ts-node를 따로 설치해야 됨. 

2) 파일 디렉토리 구조 확인. frontend 프로젝트를 하다가 backend 추가 시, 꼭 폴더로 영역 확실히 분리하여 작업할 것. 명령어 ls와 cd를 적극 활용. 해당 폴더의 내부로 이동해서 필요한 라이브러리나 모듈을 깔아야 한다. frontend / backend 각 폴더에 package.json과 node_modules가 따로 구성되어 있는지 확인. (frontend 영역에 express 깔아두고 왜 모듈 인식 안 되지...하는 저 같은 실수는 하지 마시길... package.json도 엉뚱한 쪽 파일을 건드리지 마세요...)

3) import 해오는 파일의 정확한 이름과 경로 재확인. 이렇게 해도 안 되면 CommonJS 방식으로 쓰던 import를 ES모듈식으로 한 번 바꿔보자.(이게 궁극적인 해결법이었는지는 아직도 좀 헷갈리긴 하네요. 아닐 것 같은데...혹시 모르니까...)

4) TS를 쓰며 3번의 ES모듈식까지 적용할시 tsconfig.json에 esModuleInterop true로 해주기. 해당 사안은 TypeScript가 "CommonJS" 모듈을 "ES 모듈"처럼 사용할 수 있게 하는 설정이라고 한다. 이 플래그를 활성화하면, "CommonJS" 스타일의 모듈을 "ES 모듈"처럼 default import로 가져올 수 있다. 


 

 

 

며칠 전 작은 리액트 기반 프로젝트를 꾸리던 중 서버를 다뤄야 될 일이 생겼습니다.

제가 사용한 라이브러리는 puppeteer로, 특정 사이트에 요청을 보내면 크롤링을 해주고 원하는 데이터를 긁어오게 됩니다. 이게 클라이언트 단에서 해결 가능한 구조인 줄 알았는데 결국 서버가 필요하더라구요. 그간 필요에 따라 프론트엔드 코드만 짜봤으니 서버와 마주치자 난감하죠. express는 제가 건드려본 일이... 예전에 아무것도 모르고 따라쳐본 코드 밖에 없는지라.

 

하지만 안 되면 되게 하라....  프로젝트의 핵심이자 꼭 필요한 라이브러리였기 때문에, 이번에도 혼자 맨땅에 헤딩하듯 도전했습니다. 기초적인 학습 단계라서 중간에는 Chat GPT선생의 도움도 많이 받았네요.

 

🚩오라, express여!

Express는 node.js를 위한 웹 프레임워크라고 공식 문서에 적혀 있습니다. 더 자세하게 말해보자면, 흔히 Node.js를 사용할 때 개발자들이 쉽게 백엔드 서버를 구축하는데에 쓰곤 합니다. 유명하고 널리 쓰여서 학습하기도 용이하죠.

서버가 필요하다고 하니 무작정 express를 install 합니다. 또 프로젝트 상단에 backend라는 폴더를 하나 만든 뒤, 저는 타입 스크립트 코드를 쓰고 있으니 내부에는 server.tsx를 생성했습니다. puppeteer을 사용해서 특정 사이트에서 데이터를 긁어오는 코드도 필요해보입니다. 따라서 backend 폴더 내에 scripts 폴더를 만들고 데이터를 크롤링 해오는 코드를 담을 crawler.tsx파일도 생성하기로 합니다. 완벽합니다. 

 

이제 8080 PORT를 열어 서버가 실행하면 되겠죠? 

결론적으로 기본적인 서버를 돌리는 아래와 같은 코드를 완성했습니다.

const express = require('express');
const { crawls } = require('./scripts/crawler');

const app = express();
const PORT = 8080; //포트 번호는 일단 8080번으로 해봅니다.

crawls(); //임시로 crawler.tsx의 함수를 실행시켜 잘 불러오는지 확인한다.

app.get('/', (req, res) => {
    res.send('hello!');
});

app.get('/crawl', async (req, res) => {
	res.send('성공적으로 동작 중');
    //후에 여기서 크롤링한 데이터가 처리될 것
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`); 
  //PORT 8080번에서 서버가 돌아가게 합니다.
  //성공적으로 돌아갈 시, 콘솔에 Server is running on http://localhost:8080이 뜹니다.
});
// crawl.tsx

const puppeteer = require('puppeteer');
//크롤링하여 데이터를 가져옵니다. 가져온 데이터를 return해줍니다.

async function crawls() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.naver.com/'); // 크롤링 할 사이트, 임시로 네이버 주소 입력  
  const data = await page.evaluate(() => {
    const element = document.querySelector('div');
    return element ? element.textContent : 'Element not found';
  });
  await browser.close();
  
  console.log("Crawled data:", data); //불러온 데이터를 직접 보기 위한 콘솔로그
  return data;
}

문제는 이때부터 발생했습니다. 부푼 마음으로 서버를 돌리기 위해 콘솔창에 node server.tsx하자마자, server.tsx의 {crawls}에 아래처럼 수상한 에러들이 등장하기 시작합니다. 크게 두 가지의 에러가 생깁니다.

'crawls' is assigned a value but never used. eslint@typescript-eslint/no-unused-vars
Cannot redeclare block-scoped variable 'crawls'.ts(2451)
crawler.tsx(3, 16): 'crawls' was also declared here.

1) crawls가 이미 crawler.tsx에 선언되어 있다면서 server.tsx의 import 위치에 빨간색 줄이 뜹니다.

2) server.tsx에서 crawls를 호출했는데, 오류 메세지에서는 선언만 되고 사용되지 않고 있다고 노란색 줄이 뜹니다. 

 

위 과정과 코드를 보자마자 무엇이 잘못인지 파악하셨다면, 당신은 멋진 숙련자입니다. 하지만 저는...express와의 꼬박 이틀에 걸친 씨름이 시작됩니다. 중간에 좋은 기회로 동료 개발자분과 함께 살펴보기도 했습니다. 

 

🚩동료 개발자가 발견한 첫번째 실수

1) 백엔드에서는 tsx가 아닌 server.ts를 쓴다

가장 먼저 tsx를 쓴 부분을 지적해주셨습니다. 습관적으로 프론트엔드 개발에서 쓰던 방식으로 파일을 생성한 것인데, 사실 tsx는 TypeScript로 작성된 JSX(자바스크립트 XML)를 의미합니다. 그러나 백엔드 개발에서는 순수 TypeScript(.ts 파일)를 사용하고, 프론트엔드와 달리 React UI 요소를 다루지 않기 때문에 tsx 형식이 필요하지 않다고 하였습니다. 따라서 tsx가 아닌 ts파일로 전부 바꾸어주었습니다.

 

2) 프론트엔드에서 쓰듯 import키워드로 불러오기 위해서는 package.json에 “type”: “module”, 

이전까지는 import 할 때 import 키워드를 쓰는 대신 require로 적었다고 합니다. 이것을 CommonJS방식이라고 하는데, 많은 백엔드 코드 설명 블로그 글에서도 아래 같은 코드를 찾아볼 수 있습니다. 저 역시 이렇게 작성을 했습니다. 

const moment = require("moment");

require라니? 프론트엔드에서 매번 import식으로 불러오던 방식이 익숙한 사람은 헷갈립니다. 하지만 방법이 있습니다.  "type": "module"을 package.json의 최상단에 추가하면, ES모듈식으로 해석하여 import 키워드로 사용할 수 있습니다. 단, 하나의 방식을 택하면 모든 파일에 그 방식으로 임포트하길 추천합니다. 

import moment from "moment";

이 설정은 Node.js가 ES 모듈을 위와 같이 사용하게 합니다. 익숙한 방식으로 불러올 수 있는 거죠. 함수가 두 번 선언되었다고 뱉는 에러를 해결하기 위해 혹시나 해서 모듈 방식도 한번 바꿔보았습니다. 그러나 아래 서술하듯, 여러 문제가 겹치면서 조금씩 코드를 고친 거라 이 방법이 진짜 도움을 준 건지는 모르겠습니다... 일단 import 키워드가 더 보기 좋으니 참고로 적어둡니다.

 

... 한 시간 가량 해결에 매달렸지만 에러는 해결하지 못했고, 모듈 충돌로 발생한 문제가 아닐까 추측하며 대화는 끝났습니다. 계속 안 되면 next에 다시 세팅해보라는 충고를 얻어 값진 대화였습니다. (게다가, 제가 홀로 모듈에 대해 곱씹다가 아래와 같이 '유레카!' 하고 깨닫는 결정적인 계기이기도 했으니까요.) 


🚩스스로 찾아보며 깨달은 두번째 실수

아무리 그래도 리액트를 쓰고 싶었습니다. css 적용까지 되어있는 상황에서 next로 옮기는 과정도 번거로웠고, 둘째는...이거 하나 해결하지 못하면 어떻게 다른 에러를 해결하겠는가? 라는 오기에 가까웠습니다. 모듈 문제일 수 있다는 조언에 멍하니 modules를 두어 번 지우고 깔던 중... 

... '이거 디렉토리 문제 아닌가?' 하는 생각이 번뜩 스쳐 지나갔습니다. 

 

1) 프로젝트 디렉토리 구조 체크

결론적으로 검색까지 하고 깨달은 것은 모듈을 찾지 못하는 에러의 많은 경우 디렉토리 구조 문제라는 점입니다. 기초적인 실수지만 서버가 처음인 프론트엔드 개발자라면 누구나 할 수 있죠. 치명적이고요. 디렉토리를 헷갈리면 많은 것이 꼬이게 됩니다. 엉뚱한 곳에 라이브러리를 설치하고, 모듈을 인식하지 못해 의미 없이 헛발질하게 됩니다. 

 

흔히 아래와 같이(필수x 대략적인 구조)백엔드와 프론트엔드는 하나의 루트 내에 다른 폴더로 구분되며 각각의 영역으로 이동해 필요한 패키지를 설치해야 합니다. 즉 node_modules와 package.json, 필요하다면 tsconfig.json이 두 폴더에 다 존재해야 하며, 필요하다면 각각의 폴더로 이동하여 필요한 모듈을 install해야 한다는 거죠.

/project-root
  ├── /backend            # 백엔드 관련 코드
  │   ├── /controllers    # 컨트롤러 로직
  │   ├── /models         # 데이터 모델
  │   ├── /routes         # 라우트 정의
  │   ├── /services       # 비즈니스 로직 및 서비스
  │   ├── /middlewares    # 미들웨어
  │   ├── /config         # 설정 파일
  │   ├── server.js       # 서버 시작 스크립트
  │   └── package.json    # 백엔드 패키지 정보
  ├── /frontend           # 프론트엔드 관련 코드
  │   ├── /src            # 소스 코드 (리액트, 앵귤러 등)
  │   ├── /public         # 정적 파일 (이미지, CSS, JS 등)
  │   ├── index.html      # 메인 HTML 파일
  │   └── package.json    # 프론트엔드 패키지 정보
  ├── /scripts            # 프로젝트 관리 스크립트
  ├── /tests              # 테스트 코드
  ├── .gitignore          # Git 무시 규칙
  └── README.md           # 프로젝트 설명

백엔드에 필요한 express를 프론트엔드 경로에서 깔고 있으면 안 된다는 뜻입니다...... 제가 그러고 있었으니까요. 프론트엔드 작업을 하다가 급하게 백엔드를 구축한 나머지, 두 영역이 분리되어 있다는 걸 완전히 잊은 겁니다. 

결국 파일 대이동을 시키며 위와 같이 다시 영역을 나눈 뒤, cd backend하여 백엔드에서 필요한 모듈을 전부 설치하고 package.json, tsconfig.json을 새로 생성했습니다. 또한 프론트엔드 쪽에 실수로 깔았던 백엔드 모듈들은 프론트 쪽의 package.json을 꼼꼼히 확인해가며 필요없는 건 다 uninstall합니다. 

뭔가 잘 풀려가는 것 같아요. 이후 추가적으로 나타난 에러들을 하나씩 잡기 시작합니다.

 

 

2) 백엔드에서 TS를 쓸 때는 컴파일 해주자. 또 서버 실행 시 파일은 ts가 아니라 js로 해주자.

백엔드에서 TypeScript를 사용한다면 JS로 컴파일을 꼭 해줘야 합니다. 컴파일을 하고 난 뒤에 만들어진 JS 코드로 서버를 돌리게 됩니다. 기본적인 컴파일 후 실행하기 명령어는 다음과 같습니다. 

npx tsc  # TypeScript 컴파일
node dist/server.js  # 컴파일된 JavaScript 파일 실행

 

🍬tsc --watch와 nodemon

더 수월하게 개발하고 싶다면 반드시 설정해야 하는 부분을 알려드리겠습니다. 백엔드 package.json에서 아래와 같이 추가해주면, 매번 컴파일하거나 매번 서버를 껐다 켜지 않아도 됩니다. 번거로운 과정에서 해방됩시다! 

  "scripts": {
    "build":"tsc",
    "watch":"tsc --watch",
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon dist/server.js" //ts가 아니라 js인 점 숙지.
  },

tsc --watch를 사용하면 매번 js로 수동 컴파일하던 작업에서 해방됩니다. 개발 시작마다 콘솔에 npm run watch를 써서 watch 모드로 실행해주세요.

nodemon은 우선 백엔드에서 install하고 사용합니다. 사용시 코드 수정되어도 서버 껐다 켤 필요가 없어집니다. 당연히 파일 이름에 따라 루트가 달라질 수 있으니 싹 복붙하지 마시고 파일명 꼭 확인해서 적용해주세요. npm run start하면 서버가 실행됩니다. 또 js가 아니라 ts로 썼을 경우 아래와 같은 에러가 났습니다. ts파일로 돌리려면 ts-node라는 것을 추가로 깔아주라고 합니다.

node:internal/modules/cjs/loader:1147
  throw err;
  ^

Error: Cannot find module '/home/user/twiroller/twiroller/backend/index.js'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1144:15)
    at Module._load (node:internal/modules/cjs/loader:985:27)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
    at node:internal/main/run_main_module:28:49 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

 

3) CommonJS에서 ES모듈 문법으로 바꿀 때는 import 주소를 잘 확인하자. 

동료 개발자께 조언을 얻은 김에 import 부분도 ES모듈식으로 계속 쓰기로 했습니다. 이전에는 제 디렉토리 실수로 프론트엔드의 package.json에 모듈 변경을 시도했었죠. 새로 만든 backend의 package.json에서 설정하기로 합니다. 

이전 조언대로 백엔드의 package.json 최상단에 "type":"module",를 추가 해주었습니다. 

{
  "type": "module", //타입 모듈 추가.
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build":"tsc",
    "watch":"tsc --watch",
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon dist/server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cheerio": "^1.0.0-rc.12",
    "express": "^4.19.2",
    "puppeteer": "^22.7.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "typescript": "^5.4.5"
  }
}

 

Module '"/home/user/twiroller/twiroller/node_modules/@types/express/index"' can 
only be default-imported using the 'esModuleInterop' flagts(1259)
index.d.ts(128, 1): This module is declared with 'export =', 
and can only be used with a default import when using the 'esModuleInterop' flag.

또 타입 스크립트를 쓸 경우, 위와 같은 에러가 발생한다면 tsconfig.json에 가서 해당 항목을 true로 추가합니다. "ES 모듈"과 "CommonJS 모듈" 간의 호환성 이슈로 발생하는 에러라고 하네요. 

"compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist",
    "esModuleInterop": true,  // ES 모듈과 CommonJS 모듈 간의 호환성을 위해 이 플래그를 추가합니다.
    "strict": true
  }

 

 

결론적으로 이제는 서버가 잘 뜨고 모듈을 잘 인식하는 결과를 맛볼 수 있었습니다.

서버가 잘 돌아가고 있으며 크롤링한 데이터를 불러옵니다.

 

✨에러를 해결하며 배운 점을 정리하자면 이렇습니다.

1. 디렉토리 구조를 잘 짜자! 특히 리액트에서 백엔드와 프론트엔드를 함께 쓸 때는...

2. 백엔드에서 TS를 쓸 때는 tsc 명령어를 사용해 JS파일로 컴파일을 해준 뒤, 해당 js 파일로 서버를 돌린다.

3. CommonJS와 ES모듈은 다르지만 충분히 ES모듈로 변경할 수 있다.

 

 

여기까지입니다. tsconfig와 package.json, 그리고 모듈과 디렉토리에 대해 꼼꼼하게 뜯어보는 과정을 거쳤습니다. 에러 메세지를 침착하게 읽는 습관도 들였구요. 끝까지 해결 안 되면 Next.js로 넘어가서 재생성하려 했는데 다행입니다.

 

이제 편한 맘이 되어 다시 기능 구현으로 들어가 봅니다🤩


이 포스팅은 아래와 같은 분들을 위해 작성되었습니다.

  • 아무것도 모르고 express에 도전했다가 온갖 에러와 맞닥뜨린 프론트엔드 개발자
  • 리액트와 타입 스크립트로 개발을 하던 중 급하게 혼자 서버를 만들고 있는 사람
  • 백엔드에서도 import 키워드로 불러오고 싶어진 ES모듈 문법에 익숙한 사람

 

Comments