공부하면서 궁금했던 점들을 찾아보고 기록한 내용입니다. 틀린 내용이 있을 수 있습니다.
요즘 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라는 폴더가 있다.
scripts 폴더를 살펴보면, start.js와 build.js, eject.js 파일들이 존재하는 것을 확인할 수 있다. 또한, 기본적으로 webpack에서 제공되는 설정이 아닌, node api를 이용하여 웹팩을 실행하고 환경을 구성하는 것을 확인할 수 있었다.
📌 관련 코드는 해당 파일에서 직접 확인하는 방식으로 작성합니다.
start.js를 한번 살펴보자.
start.js의 기본적인 로직의 흐름은 다음과 같았다.
checkBrowswer
함수를 통해 브라우저를 확인한다.choosePort
를 통해 동적으로 port를 설정한다.내가 작성한 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를 동적으로 정의하고 있기 때문에, 해당 함수를 살펴보기로 한다.
webpackDevserverUtils.js에서 390번째 라인의 choosePort가 하는 역할을 살펴보자.
모든 로직을 이해하는 것은 어렵지만, 이해한 부분 까지만 설명하자면,
choosePort가 하는 역할은 크게 다음의 역할만 하고 있었고, 실질적으로 detect
라는 외부 패키지에 관련 유틸을 의존하고 있었다.
따라서 detect 함수가 하는 역할이 무엇인지 알기 위해, detect 패키지로 이동해 보기로 한다.
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 파일을 살펴보면, 세 개의 함수로 구성이 되어있어서 생각보다 빠르게 파악할 수 있었다.
tryListen
함수는 내부에 handleError
라는 함수가 정의되어 있고, listen
이라는 함수를 호출한다.handleError
는 port와 max port를 다시 설정하여, tryListen을 재귀적으로 호출한다. 에러가 발생하지 않을 때까지 시도하는 듯 하다.// 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);
});
}
.on
을 통해 error를 감지하게 되고, 에러처리를 하게 된다.create-react-app에서 yarn start를 통해 번들링을 시도할 때, 포트가 이미 사용중인 포트라면 다음과 같은 방법을 거쳐서 사용되지 않는 포트를 찾아 연결을 시도한다.
이해할 때는 조금 어려웠는데, 적어두고 보니까 생각보다 간단하다..
서버에 대한 이해의 필요성
프론트엔드를 다루고 있는 개발자도, 서버에 대한 이해가 있으면 플러스가 될 요인이 너무나도 많음을 알았다.
레포지토리에서 다른 사람이 작성한 코드를 읽는 연습을 계속 하다 보니 확실히 코드를 읽는 속도나 이해도가 증가하는 듯하다.
아직 풀리지 않은 의문들
코드의 흐름은 대략적으로 파악할 수 있었지만, 세부 구현이나, 서버에 대한 이해가 부족하기 때문에 이 부분은 왜 이렇게 구현을 했지? 이렇게 구현해야만 하는 이유가 있나?에 대해서 아직 속 시원하게 해결되지 않았다. 특히, detect-port.js
의 listen
함수를 세 번이나 중첩하여 호출하는 이유에 대해서 완벽하게 이해하지 못했다. 이 부분은 차근 차근 서버에 대한 공부를 진행하면서 이해해보기로…
처음에는 이 부분이 궁금했던 이유가, 내가 만든 리액트 환경 설정인 react-typescript-boilerplate의 편의성을 어떻게 하면 더 높일 수 있을까 고민하면서 느꼈던 궁금증이었고, create-react-app의 코드를 분석하면서 어떠한 방식으로 이를 해결했는지 찾아본 것이었다.
그러나 찾아보면서 이 부분을 아직까지 내 환경에 적용하기에는 여러 문제가 있다고 생각했다.
webpack 공식 문서에 따르면, node api를 이용하여 개발할 경우 다음과 같은 차이점이 있다고 한다.
Node.js API는 모든 보고 및 오류 처리가 수동으로 처리되어야 하므로, webpack이 컴파일 부분만 수행하고 빌드 또는 개발 프로세스는 사용자 정의하는 경우 유용합니다. 이러한 이유로 stats
설정 옵션은webpack()
호출에 영향을 미치지 않습니다.
아직 node.js에 대한 이해가 부족할 뿐더러, webpack의 모든 보고와 오류 처리를 수동으로 진행함을 감수하면서 편의성을 챙기는 것이 너무 기회비용이 크지 않을까? 라는 생각도 있다. 또한 이렇게 에러 처리까지 진행하면 사실상 create-react-app과 다를 바 없는 코드량을 작성해야 하는 문제점도 있다.
따라서 이해는 하되, 도입하는 것은 보류가 내 결론이다.
react-typescript-boilerplate(직접 환경을 구성한 레포)