react 실행 환경을 구축하면서 느꼈던 궁금증 해소기(create-react-app은 어떻게 자동으로 port를 자동으로 잡아줄까?)

Ben·2022년 11월 5일
0

Today I Learned

목록 보기
53/57

공부하면서 궁금했던 점들을 찾아보고 기록한 내용입니다. 틀린 내용이 있을 수 있습니다.

개요

떠오르는 궁금증

요즘 react boilerplate를 만드는데 재미를 느끼고 있다. react-boilerplate를 만드는 이유는 다음과 같다.

  • 밑바닥부터 만들면서 번들링 과정에 대해서 이해할 수 있다.
  • 원하는 대로 커스터마이징이 가능하다.
    • yarn eject라는 옵션을 통해 옵션을 밖으로 꺼내고 커스터마이징이 가능 하긴 하지만, 접근성이 아무래도 낮을 수밖에 없다.
  • react-scripts는 이전에 사용되는 문법이 많았다.
    • webpack5를 사용하고 있지만 webpack4에서 사용하고 있는 require.resolve() 이 많았다. 성능 상에서 어떠한 영향을 미치는지 까지는 잘 모르겠지만, 나중에 커스터마이징을 시도할 때 신경 써야 할 부분일 수 있다.

다만 react 환경을 구축함에 있어서, create-react-app을 사용하는 것에 비해 큰 이점을 느끼기 위해서는, 개발 편의성에 대한 부분을 고려해야 된다고 생각한다. 리액트 환경을 구축하는 사람들은 개발자들이다. 개발자들은 개발 편의성이 높은 패키지를 사용하고자 할 것이기 때문이다.

create-react-app과 react 환경을 직접 구축하여 사용할 때 느꼈던 가장 큰 차이점은, 내가 구축한 react 환경은 port가 이미 사용 중일 때 제대로 구축이 되지 않는 문제가 있지만, create-react-app를 사용하면 해당 포트가 이미 사용 중이라면, 자동으로 사용 중이지 않은 포트 중 하나를 선택하여 번들링을 해준다.

이런 기능을 자체적으로 구축한 react 환경에 도입을 해보고 싶어서, 어떠한 방식으로 구현을 했는지 알고, 실현 가능성이 있는지 확인하기 위하여 create-react-app 내부 구현을 살펴보기로 했다.

패키지 살펴보기

create-react-app 레포지토리의 package 폴더를 보면, 다양한 패키지들이 있는 것을 확인할 수 있다.

create-react-app으로 프로젝트를 만들면, package.json의 start 명령어가 react-scripts start로 되어 있는 것으로 보아, 뭔가 실행 단계가 react-scripts 폴더에 있을 것이라는 생각이 들었다.

react-scripts에서 확인할 만한 폴더는 scripts 란 폴더와, config라는 폴더가 있다.

  • config: webpack을 설정하는 데 필요한 환경 설정들 (webapck.config.js와 webpackDevServer.config.js)을 모아둔 폴더
  • scripts: start, build, eject 등 react-scripts에서 사용되는 명령어들 스크립트가 저장된 폴더

scripts 폴더를 살펴보면, start.js와 build.js, eject.js 파일들이 존재하는 것을 확인할 수 있다. 또한, 기본적으로 webpack에서 제공되는 설정이 아닌, node api를 이용하여 웹팩을 실행하고 환경을 구성하는 것을 확인할 수 있었다.

scripts/start.js

📌 관련 코드는 해당 파일에서 직접 확인하는 방식으로 작성합니다.

start.js를 한번 살펴보자.

start.js의 기본적인 로직의 흐름은 다음과 같았다.

  1. checkBrowswer 함수를 통해 브라우저를 확인한다.
  2. choosePort를 통해 동적으로 port를 설정한다.
  3. config 폴더에 정의해둔 환경을 통해 webpack 환경과 webpack dev server 환경을 구현한다.
  4. 실행하면서 발생하는 관련 에러들을 처리한다.

내가 작성한 webpack.config.js는 내부 환경 내에 devserver라는 property를 통해 webpack dev server 환경을 구현했었다.

// webpack.config.js in react-typescript-boilerplate repository

module.exports = (webpackEnv, argv) => {
  return {
    resolve: {
      // ...
    },
    entry: // entry here
    output: {
      // ...
    },
    module: {
      // ...
    },
    plugins: [
      // ...
    ],
    devServer: {
      port: 3000,
      hot: true,
      host: 'localhost',
      historyApiFallback: true,
    },
  };
};

devServer 프로퍼티에 port와 host를 정의할 때 어떤 비동기적인 로직을 통해 정의할 방법은 딱히 보이지 않았다. webpack-dev-server 관련 문서에도 어떤 동적으로 포트를 정의할 수 있는 방법에 대해서 언급하지 않았었다.

따라서 node api를 이용하여 우선 port가 점유되지 않았는지 확인하고, 다른 포트를 선택하는 과정을 비동기적으로 거친 뒤, webpack devserver를 실행하여 해당 포트를 넣어주는 과정을 거치는 듯 하다. 이것이 node api를 이용하는 이유 중 하나인 것 같다. 높은 수준으로 커스터마이징이 가능 하다는 점

다시 start.js 코드로 돌아와서, choosePort라는 유틸이 port를 동적으로 정의하고 있기 때문에, 해당 함수를 살펴보기로 한다.

react-dev-utils/webpackDevserverUtils.js/choosePort

webpackDevserverUtils.js에서 390번째 라인의 choosePort가 하는 역할을 살펴보자.

모든 로직을 이해하는 것은 어렵지만, 이해한 부분 까지만 설명하자면,

  • detect라는 외부 함수를 통해 비동기적으로 port를 선택한다.
  • port 선택에 성공했다면, 메시지를 출력하고 port를 반환한다.
  • port 선택에 실패했다면, 에러메시지를 출력한다.

choosePort가 하는 역할은 크게 다음의 역할만 하고 있었고, 실질적으로 detect라는 외부 패키지에 관련 유틸을 의존하고 있었다.

따라서 detect 함수가 하는 역할이 무엇인지 알기 위해, detect 패키지로 이동해 보기로 한다.

detect-port/lib/detect-port.js

detect-port를 npm에 검색해보면, 주간 다운로드 횟수가 500만이나 되는 굉장히 인기 있는 패키지임을 확인할 수 있었다. detect-port-alt - npm (npmjs.com)

detect-port 레포지토리의 READEME.md를 보면, 다음과 같이 사용할 수 있다고 되어 있다.

const detect = require('detect-port');
/**
 * use as a promise
 */

detect(port)
  .then(_port => {
    if (port == _port) {
      console.log(`port: ${port} was not occupied`);
    } else {
      console.log(`port: ${port} was occupied, try port: ${_port}`);
    }
  })
  .catch(err => {
    console.log(err);
  });

detect라는 함수가 자체적으로 아직 사용되지 않고 있는 port를 반환하는 것 같다. 한번 살펴보자.

detect-port.js 파일을 살펴보면, 세 개의 함수로 구성이 되어있어서 생각보다 빠르게 파악할 수 있었다.

  • port를 바탕으로 maxport를 설정하여, tryListen이라는 함수로 넘겨준다. 또한, 모든 과정이 끝나고 최종적으로 realPort를 resolve한다. (비동기적으로 realPort 정보를 넘겨준다.)
  • tryListen 함수는 내부에 handleError 라는 함수가 정의되어 있고, listen이라는 함수를 호출한다.
    • handleError는 port와 max port를 다시 설정하여, tryListen을 재귀적으로 호출한다. 에러가 발생하지 않을 때까지 시도하는 듯 하다.
    • 여기서 궁금한 점은, 유저가 설정한 hostname이 있을 때는 listen이라는 함수를 한 번만 호출하는데, hostname이 없을 때는 listen 함수를 콜백을 통해 최종적으로 세 번을 호출하는 사실을 알 수 있다. 아직 이 부분은 이해가 되지 않아, 조금 더 이해하는데 시간이 걸릴 듯 하다.
    • 최종적으로 문제가 없다면 realPort를 반환한다.
// detect-port.js

function listen(port, hostname, callback) {
  const server = new net.Server();

  server.on('error', err => {
    debug('listen %s:%s error: %s', hostname, port, err);
    server.close();
    if (err.code === 'ENOTFOUND') {
      debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port);
      return callback(null, port);
    }
    return callback(err);
  });

  server.listen(port, hostname, () => {
    port = server.address().port;
    server.close();
    debug('get free %s:%s', hostname, port);
    return callback(null, port);
  });
}
  • listen 함수는 net(node.js built-in package)를 이용하여 서버를 구성한다.
    • net 모듈은 TCP(transmission control protocol)을 사용하여 데이터를 주고받도록 하는 모듈이다.
    • 서버 개념에 대해 아직 잘 모르고, 왜 tcp를 사용했는지에 대한 의도를 잘 파악하지 못하여, 일단은 아, 이렇게 구현을 했구나 정도로만 넘어가고자 한다.
  • server.on을 통해 이벤트를 감지하고, server.listen을 통해 연결을 감지한다.
  • port = server.address().port 함수를 통해 실제로 서버가 해당 포트를 통해 잘 연결되었는지 확인한다. 이 과정에서 에러가 발생한다면, .on을 통해 error를 감지하게 되고, 에러처리를 하게 된다.

정리(TL;DR)

create-react-app에서 yarn start를 통해 번들링을 시도할 때, 포트가 이미 사용중인 포트라면 다음과 같은 방법을 거쳐서 사용되지 않는 포트를 찾아 연결을 시도한다.

  1. 처음에 연결하고자 하는 hostname과 port를 이용하여 해당 포트가 사용 중인지, 사용 중이 아닌 지 확인한다.
    1. 만약 사용 중이라면, 적절한 포트를 찾아 사용 중이 아닌 포트를 찾을 때까지 연결을 시도한다.
    2. 만약 사용 중이 아니라면, 적절한 포트를 찾았으므로 비동기적으로 반환한다.
  2. create-react-app에서는 콜백 함수를 통해 반환 받은 사용 중이지 않은 포트로 연결을 시도한다. 포트를 찾을 때까지 webpack dev server 연결이 이루어지면 안 되므로, 콜백 함수 내부에서 webpack dev server를 호출한다.

이해할 때는 조금 어려웠는데, 적어두고 보니까 생각보다 간단하다..

간단 회고

느낀 점

서버에 대한 이해의 필요성

프론트엔드를 다루고 있는 개발자도, 서버에 대한 이해가 있으면 플러스가 될 요인이 너무나도 많음을 알았다.

레포지토리에서 다른 사람이 작성한 코드를 읽는 연습을 계속 하다 보니 확실히 코드를 읽는 속도나 이해도가 증가하는 듯하다.

아쉬운 점

아직 풀리지 않은 의문들

코드의 흐름은 대략적으로 파악할 수 있었지만, 세부 구현이나, 서버에 대한 이해가 부족하기 때문에 이 부분은 왜 이렇게 구현을 했지? 이렇게 구현해야만 하는 이유가 있나?에 대해서 아직 속 시원하게 해결되지 않았다. 특히, detect-port.jslisten 함수를 세 번이나 중첩하여 호출하는 이유에 대해서 완벽하게 이해하지 못했다. 이 부분은 차근 차근 서버에 대한 공부를 진행하면서 이해해보기로…

앞으로 시도할 것들

  • 여유가 있을 때, node 서버에 대해서 공부하기
  • 웹의 전반적인 이해를 통해 프론트엔드 역량을 향상시키자.
  • 네트워크 기본서나, 아티클을 통해 서버에 대한 이해를 빠르게 학습하는 것이 좋을 듯하다.

시도해볼 만한가?

처음에는 이 부분이 궁금했던 이유가, 내가 만든 리액트 환경 설정인 react-typescript-boilerplate의 편의성을 어떻게 하면 더 높일 수 있을까 고민하면서 느꼈던 궁금증이었고, create-react-app의 코드를 분석하면서 어떠한 방식으로 이를 해결했는지 찾아본 것이었다.

그러나 찾아보면서 이 부분을 아직까지 내 환경에 적용하기에는 여러 문제가 있다고 생각했다.

  1. 일단 코드의 흐름은 이해했지만, 구현 부분까지 완벽히 내 것으로 만들지는 못했다.
  2. create-react-app이 아닌, 개인 환경을 구축한 이유 중 하나가 자유로운 커스터마이징과 간편한 구축 방법인데, 일단 node api를 사용하게 되면 webpack에서 제공하는 여러 편의 기능을 이용할 수 없었다. 이를 커버하기 위해서는 react-scripts가 제공하는 수준의 고도화를 진행해야 하는데, 현재로서는 해당 수준으로 프로젝트를 끌어올릴 자신이 없다.

webpack 공식 문서에 따르면, node api를 이용하여 개발할 경우 다음과 같은 차이점이 있다고 한다.

Node.js API는 모든 보고 및 오류 처리가 수동으로 처리되어야 하므로, webpack이 컴파일 부분만 수행하고 빌드 또는 개발 프로세스는 사용자 정의하는 경우 유용합니다. 이러한 이유로 stats
설정 옵션은 webpack() 호출에 영향을 미치지 않습니다.

아직 node.js에 대한 이해가 부족할 뿐더러, webpack의 모든 보고와 오류 처리를 수동으로 진행함을 감수하면서 편의성을 챙기는 것이 너무 기회비용이 크지 않을까? 라는 생각도 있다. 또한 이렇게 에러 처리까지 진행하면 사실상 create-react-app과 다를 바 없는 코드량을 작성해야 하는 문제점도 있다.

따라서 이해는 하되, 도입하는 것은 보류가 내 결론이다.

출처

react-typescript-boilerplate(직접 환경을 구성한 레포)

Node Interface | 웹팩

Net | Node.js v19.0.1 Documentation

node.js net module :: net으로 서버 열기

profile
New Blog -> https://portfolio-mrbartrns.vercel.app

0개의 댓글