Experiencing the Current FE Development Environment While Building a Coding Playground Part 1
2021. 2. 15.

(Updated 2022-03-04: Configuration code for webpack-dev-server, babel, and jest has been modified.)
Sometimes when developing, you want to quickly write code in a specific environment and check the results. This happens when studying libraries, frameworks, or languages, or when you want to quickly experiment with ideas or prototype them. If a vanilla JavaScript environment is sufficient, creating a directory, a js file, and an html file, then opening an editor and loading the html file in a browser completes the basic setup. But you know what? Even setting up this vanilla JavaScript environment can be annoying sometimes. If this basic setup is cumbersome, imagine the complex environments used in actual work. It's too much hassle to add ESLint, Webpack, and Babel every time. So, sometimes we awkwardly borrow space by creating temporary files in a project with a similar environment to what we currently need.
Services like CodeSandbox emerged in response to these needs, but they don't allow development in our familiar editors. This is a significant drawback for those sensitive to their editors. Inputting or editing within them can be somewhat inconvenient.
I found this inconvenience quite severe. While scaffolding tools like yeoman exist, the hassle of creating additional projects each time was annoying, and managing them became difficult as the number grew. The disk space consumed by duplicate node_modules
installations was also significant. Anyway, creating Node projects for whatever reason often led to management difficulties.
One day, while studying Lerna, it occurred to me. What if I created a kind of development playground for each environment, making it easy to pull out an experimental setup? Lerna, a popular tool these days, seemed helpful for this. So I tried it immediately. And I was quite satisfied. This article is written about two months after I started using such a personal playground.
This article will explain the process and tools used to build a personal development playground. While doing so, it will naturally introduce the current FE development environment. It would be great to follow along hands-on with the article. By the time you finish reading, you'll have a taste of the foundational environments of modern FE development. For those in a hurry, I'll leave a link to the repository at the end of Part 2. You can clone it directly from the repo and use it. I named the project "hatchery." Yes, like the Zerg hatchery. Sounds plausible, right?
I frequently need about three types of development environment sets:
- Basic Environment: webpack, babel, eslint, prettier, jest, testing-library
- TypeScript Development Environment: (Basic Environment), typescript
- React + TypeScript Development Environment: (TypeScript Environment), react, styled-component
As you can see from the list, these development environments are not independent but overlap. Even if you create a Vue environment here, it will overlap with the TypeScript development environment. Because of this overlap, the more duplicated dependency modules (i.e., node_modules
content), the more burdensome the disk space becomes.

So, we'll make the dependency modules, specifically the devDependency
modules, shared instead of installing them redundantly. Lerna will make this easy. Since we won't be deploying, we'll install everything as devDependencies. This way, all dependencies will be shared. Nice, right?
Lerna (lerna)?
While this article isn't intended to cover Lerna in detail, I'll walk you through some basic usage. First, a brief introduction to Lerna: think of it as a tool that allows you to manage multiple independent projects within a single git repository. Usually, these are NPM distribution units. In other words, you can manage multiple NPM projects in one repository. It's also used when managing a single large project by breaking it down into several small, independent module projects based on responsibility. Separating modules into distinct projects offers many advantages over distinguishing them by directories or files, but it also introduces inconveniences. Lerna virtually eliminates these inconveniences.
Why is Lerna needed to create a playground? As mentioned earlier, I need three types of development environments, maybe more later. Lerna is unparalleled for managing multiple projects (i.e., development environments) in a single repository, especially for sharing dependencies. It makes it easy to manage three or more development environments while sharing dependencies from one place. This is good not only for management but also for disk space, right? That's why we use Lerna. Although Lerna wasn't created specifically for separating development environments, so what? A tool is just a tool…
Creating the Basic Playground Environment
The actual order I worked in differs slightly, but for convenience, let's install Lerna first. I'll assume a project has already been created with npm init
.
Install Lerna.
npm i -D lerna
Make this project managed by Lerna.
npx lerna init --independent
The independent
option means managing the internal packages (here, each development environment) independently with individual versions. That seems appropriate for us now, right? When the init
command runs, a lerna.json
file and a packages
directory are created. You can consider the basic Lerna environment set up.
Now, what we need to do is create the first stage of our development environment: the basic JavaScript development environment.
npx lerna create pgjs
Since it's a playground, I'll prefix each environment with pg
, hence pgjs
. When executed, npm init
runs internally, so you need to enter the project information again. This package is a sub-project managed within the playground project. After entering the appropriate information and creating the project, the package is generated inside the packages
directory.
Content corresponding to the basic template is pre-created, but I don't like the default directory structure provided. I looked for a way to change the package's default template, but it seems there isn't one yet. Since others besides me seem to need it, I thought about contributing this time, but as always, it was just talk.
The directory structure might be a matter of taste, but please try to match mine for now. I deleted the __tests__
folder. I changed the lib
directory to src
. And inside src
, there was a pgjs.js
file, which I changed to index.js
.
Thus, the foundational work for the JS development environment is complete.
ESLint
First, install ESLint. ESLint is almost standard now, so most people probably use it. It's the nagging helper that monitors whether you're coding well. You can install it using npm by moving to the packages/pgjs
directory and running npm i -D eslint
, but since we're managing with Lerna, let's use Lerna.
npx lerna add eslint packages/pgjs --dev
Using Lerna's add
command to install dependencies has several advantages, one of which is the ability to install dependencies for multiple packages at once. However, there's no particular advantage right now. From the package's perspective, there's not much difference from installing with npm. Also, install all dependencies as devDependencies
. The reason for this will become clear in Part 2.
Move to packages/pgjs
. Then create the ESLint basic configuration file.
npx eslint --init
Several options will appear; please select the following for now.
How would you like to use ESLint? · problems
What type of modules does your project use? · esm
Which framework does your project use? · none
Does your project use TypeScript? · No
Where does your code run? · browser
What format do you want your config file to be in? · JavaScript
This process creates the packages/pgjs/.eslintrc.js
configuration file.
Let's check if it's installed correctly.
npx eslint src
If you get an error like 'module' is not defined
, it's successful. This error is expected. We'll fix it later. Let's move on for now.
Prettier (Prettier)
Next, let's install Prettier. If you're not using Prettier yet, please use it. Use it twice. Let's stop wasting time changing styles. Especially during code reviews, style-related issues are tedious, right? They're not productive. Delegate such repetitive and simple tasks to machines. Now, the only style-related comment you might see in code reviews is something like, "Did you set up Prettier correctly?" It even has the side effect of checking for typos during parsing.
I'm still unsure if ESLint's auto-fix feature can replace Prettier. About 1.5 years ago, I tested applying auto-fix on every save to see if it could replace Prettier, but I remember finding it a bit inconvenient. I don't recall the exact reasons, but I remember thinking, "This won't work." This isn't to say ESLint's auto-fix isn't helpful. It's a useful feature, and I use it occasionally, but it was different from Prettier's functionality.
npx lerna add prettier packages/pgjs --dev
For the Prettier configuration, I'll just copy and paste what I use for now. Since we'll use the same settings for Prettier across all environment sets we create in the playground, we'll create the configuration file in the playground root, not in the packages/pgjs
path. There's no need to apply different Prettier settings for each environment.
Create a .prettierrc
file in the playground root.
{
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"vueIndentScriptAndStyle": false
}
For descriptions of each option, you can check the schema, but it's really poorly presented. For now, just use my settings and maybe tweak a few things that bother you later.
Prettier can be run from the terminal, but for convenient use, you need to set it up in your editor or IDE. You can refer to the official site information. It's usually easy to configure.
To test if the setup is correct, mess up the indentation in the packages/pgjs/index.js
file and save it. If Prettier is configured correctly, even if you make the code look terrible, it will beautifully format it perfectly. Now, when copy-pasting, you don't need to manually fix the code with backspace, tab, and space; just saving it will automatically format it appropriately for the situation. Productivity increases significantly. There's even a sense of satisfaction when object literals and other code snippets are neatly organized with a 촤자작
(onomatopoeia for quick formatting) upon saving.
Webpack (Webpack)
Next is Webpack. It's used for bundling, integrating necessary build tools, and running a server. Webpack has long become a tool that must be installed in almost any project. Alright, let's install Webpack. You know how, right? Install the latest version of Webpack (webpack 5).
npx lerna add webpack packages/pgjs --dev
npx lerna add webpack-cli packages/pgjs --dev
npx lerna add webpack-dev-server@4.0.0-beta.0 packages/pgjs --dev
You need to install three dependencies in total. webpack-dev-server
is a tool that easily creates a front-end server, but version 4.0, which supports Webpack 5, is still in beta. The beta tag might become unnecessary soon.
Okay, now that Webpack is installed, what's next? Create the Webpack configuration file webpack.config.js
. Create it in the packages/pgjs
path. I won't cover the details of the configuration file here. For now, just copy and paste this basic setup.
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env, argv) => {
let config = {
entry: ['./src/index.js'],
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [],
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'@src': path.resolve(__dirname, 'src/'),
},
},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
if (argv.mode === 'development') {
config = {
...config,
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Development',
showErrors: true,
}),
],
devServer: {
open: {
app: {
name: 'Google Chrome', // Varies by platform, 'chrome' on Windows
}
},
host: '0.0.0.0',
client: {
overlay: {
warnings: false,
errors: false,
},
}
},
devtool: 'eval-cheap-module-source-map',
};
}
return config;
};
The configuration file uses two plugins: html-webpack-plugin
and clean-webpack-plugin
. html-webpack-plugin
generates the index.html
used by the front-end server, and clean-webpack-plugin
clears the build directory. Let's install these too.
npx lerna add html-webpack-plugin packages/pgjs --dev
npx lerna add clean-webpack-plugin packages/pgjs --dev
The pasted Webpack configuration is very basic. It includes settings for the entry point, how to split chunks, and how to run the server. For more details on the configuration, refer to the official site. There's nothing specific to change right now.
First, let's check if the server starts. In the packages/pgjs
path, enter the following:
npx webpack serve --mode development
This command starts the development server in development mode. You can check the server at localhost:8080
in your browser, but nothing will appear yet, right? Let's modify the packages/pgjs/index.js
file. Delete all content and let's display an alert.
alert('OK');
If you have the server open in your browser, the page will automatically refresh the moment you save the file, and an alert box will pop up. If the alert appears correctly, the basic Webpack setup is complete.
Babel (Babel)
Now let's install our powerful friend, Babel. If you plan to use only vanilla JavaScript in the playground's development environment and limit yourself to specific browsers as needed, you might not need to install Babel. However, it's better to keep the environment as close to the actual work environment as possible, so installing Babel is recommended. Since I considered various framework environments, I had no choice but to use it.
Install Babel and necessary modules from the project root.
npx lerna add @babel/core packages/pgjs --dev
npx lerna add @babel/preset-env packages/pgjs --dev
npx lerna add core-js packages/pgjs --dev
The first is the Babel core module, and the second and third are installed for browser compatibility. Honestly, I wonder if browser compatibility is necessary for a playground, but since I might need to check some logic from actual work or test Babel itself, I try to keep the environment as close as possible to the one used in practice.
Alright, Babel is installed, so we need to configure it, right? Create a .babelrc
file in packages/pgjs
.
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3,
"debug": false
}
]
],
"plugins": [
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }]
]
}
Oops! After copy-pasting the configuration, I realized I missed one thing. We need to install a Babel plugin to fully use class fields, which are still in TC39 stage-3. JavaScript (ECMAScript) evolves and improves every year. I recommend staying interested and actively adopting new features. Specs at stage-3 are generally considered safe for production use. This is also a fundamental reason for using Babel :)
(Update 2022-03-04: Since class-properties became part of the ES2022 spec, it's included in the latest preset-env, so separate installation is no longer necessary. You can skip the step below.)
npx lerna add @babel/plugin-proposal-class-properties packages/pgjs --dev
Not only do we need Babel configuration, but to fully leverage Babel's advantages, we also need to set up Browserslist. Think of this as information about which browsers the code transformed by Babel should support. The configuration format is quite readable, so you'll get used to it quickly. For more details, please refer to the Browserslist Quick Guide (Note: this link is to an external Korean blog post).
Add the browserslist
setting to the packages/pgjs/package.json
file.
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
]
This means supporting browsers with global usage over 1%, the last two versions of each browser, and not supporting IE versions below 11. Babel, specifically preset-env
, uses this information to determine the content of the build output and the polyfills to pull from core-js
.
We can perform builds independently from the terminal using Babel CLI tools, but since we're using Webpack, let's integrate Babel with Webpack.
In the webpack.config.js
file, there's a section like this:
module: {
rules: []
},
We integrate Babel using loaders in that empty rules
array. Loaders are like plugins or extensions in Webpack that allow extending bundling functionality.
Let's install the Babel loader and configure it.
npx lerna add babel-loader packages/pgjs --dev
After installation, add the Babel loader to webpack.config.js
.
module: {
rules: [
{
test: /\.(ts|js)$/, // Since we'll add TypeScript later, ts files are also sent to Babel.
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
}
]
},
To check if Babel is applied correctly, modify the packages/pgjs/src/index.js
file as follows:
class MyClass {
#pField = 'hello';
getPField() {
return this.#pField;
}
}
console.log(new MyClass().getPField());
Then, either start the server and check in a browser that doesn't support class fields, or examine the code in the bundled file. But that's cumbersome, right? It probably worked fine. If there were no build errors, you can just move on.
If ESLint is properly set up in your editor, you'll likely see that ESLint doesn't correctly recognize the private field syntax #
used in the packages/pgjs/src/index.js
file. This is because ESLint doesn't know about the syntax extended by Babel. This issue can be resolved using @babel/eslint-parser
.
npx lerna add @babel/eslint-parser packages/pgjs --dev
First, install it, then add the parser option at the top of .eslintrc.js
.
module.exports = {
parser: '@babel/eslint-parser', // Add this
env: {
browser: true,
node: true, // Add this
…
Also, add node: true
to the env
to allow understanding of the CommonJS module system. Without this, you'll get a 'module' is not defined
error in the configuration file.
Jest (Jest)
For unit testing tools, I started with Mocha, used the Jasmine + Karma combination for a long time, and settled on Jest about 1-2 years ago. All three are good tools. It's not that Jest is an evolution beyond the others. You can choose based on the environment and the developer's preference or philosophy towards testing. My team and I chose Jest after review.
npx lerna add jest packages/pgjs --dev
Now that Jest is installed, configuration is needed again. In the basic JS environment, there's nothing special to configure. Create a jest.config.js
file in the packages/pgjs
path.
module.exports = {
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'json'],
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
},
watchPathIgnorePatterns: ['/node_modules/'],
};
The configuration is simple. The moduleFileExtensions
option defines the module file extensions to use. moduleNameMapper
defines an alias like @src
to easily access the path containing the files to be tested. Finally, watchPathIgnorePatterns
excludes unnecessary directories from the watch target. The watch feature automatically reruns tests when code is modified. For additional settings, refer to the official site. Now, shall we run it?
Let's modify the packages/pgjs/src/index.js
file again.
class MyClass {
#pField = 'hello';
getPField() {
return this.#pField;
}
}
export default MyClass; // Here
We exported MyClass
so it can be used in the test file. Now let's create a test case for MyClass
. Create a file named index.test.js
in the same directory as index.js
.
import MyClass from '@src/index';
describe('MyClass', () => {
it('could returns pField value using getPField() ', () => {
expect(new MyClass().getPField()).toEqual('hello');
});
});
With Jest, you can write test cases using the it
function and test
. They are essentially the same. Choose based on how the test and description should interact. Think of the it
in the it
function as referring to the description in the describe
function. Therefore, write the description for the it
function assuming the subject it
is already present.
Now let's run the tests from the packages/pgjs
path.
npx jest
The results appear immediately upon execution.
PASS src/index.test.js
MyClass
✓ could returns pField value using getPField() (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.949 s, estimated 1 s
Cool, right? You can also test the DOM, but DOM-related tests are better done with the additional tool testing-library. We'll cover this again when setting up React.
Ah! Since Jest uses the Node environment, it cannot directly test the actual DOM. Therefore, it tests using a virtual DOM like jsdom
. It handles this automatically, so you don't need to do anything specific. However, being a fake DOM, sometimes specific APIs like event object information or Range might not work correctly. I mention this because I struggled with this for a few days once. Most browser APIs are mocked correctly, but if you encounter a situation where your code seems flawless, it's worth suspecting this. But usually, it's my mistake. :)
Wrapping Up
Alright, with the Jest setup complete, we've created the basic JavaScript environment. For now, it's more like installing and configuring a modern development environment piece by piece rather than a playground, but we're not done yet. In Part 2, we'll add TypeScript and React development environments based on this basic JavaScript setup. Using Lerna, we can easily reduce the duplication of dependency modules. We'll also make it possible to reduce duplication in various configuration files. And we need to think about how to utilize it as a playground, right? The real gems are in Part 2. For now, let's wrap up Part 1 here and finish in Part 2.
with kakaopay
Recommend Post
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.