clean-code-typescript
ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ํ ํด๋ฆฐ์ฝ๋
clean-code-javascript์์ ์๊ฐ์ ๋ฐ์์ต๋๋ค.
๋ชฉ์ฐจ
- ์๊ฐ
- ๋ณ์
- ํจ์
- ๊ฐ์ฒด์ ์๋ฃ๊ตฌ์กฐ
- ํด๋์ค
- SOLID
- ํ ์คํธ
- ๋์์ฑ
- ์๋ฌ ์ฒ๋ฆฌ
- ์์
- ์ฃผ์
- ๋ฒ์ญ
- ๋ฒ์ญ์ ๋์์ ์ฃผ์ ๋ถ๋ค
์๊ฐ
Robert C. Martin์ ์ฑ ์ธ ํด๋ฆฐ ์ฝ๋์ ์๋ ์ํํธ์จ์ด ๊ณตํ ๋ฐฉ๋ฒ๋ก ์ ํ์ ์คํฌ๋ฆฝํธ์ ์ ์ฉํ ๊ธ์ ๋๋ค. ์ด ๊ธ์ ์คํ์ผ ๊ฐ์ด๋๊ฐ ์๋๋๋ค. ์ด ๊ธ์ ํ์ ์คํฌ๋ฆฝํธ์์ ์ฝ๊ธฐ ์ฝ๊ณ , ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๋ฉฐ, ๋ฆฌํฉํ ๋ง ๊ฐ๋ฅํ ์ํํธ์จ์ด๋ฅผ ์์ฑํ๊ธฐ ์ํ ๊ฐ์ด๋์ ๋๋ค.
์ฌ๊ธฐ ์๋ ๋ชจ๋ ๊ท์น์ ์๊ฒฉํ๊ฒ ๋ฐ๋ฅผ ํ์๋ ์์ผ๋ฉฐ, ๋ณดํธ์ ์ผ๋ก ํต์ฉ๋๋ ๊ท์น์ ์๋๋๋ค. ์ด ๊ธ์ ํ๋์ ์ง์นจ์ผ ๋ฟ์ด๋ฉฐ, ํด๋ฆฐ ์ฝ๋์ ์ ์๊ฐ ์๋ ๊ฐ ๊ฒฝํํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ ๋ฆฌํ ๊ฒ์ ๋๋ค.
์ํํธ์จ์ด ๊ณตํ ๊ธฐ์ ์ ์ญ์ฌ๋ 50๋ ์ด ์กฐ๊ธ ๋์๊ณ , ๋ฐฐ์์ผ ํ ๊ฒ์ด ์ฌ์ ํ ๋ง์ต๋๋ค. ์ํํธ์จ์ด ์ค๊ณ๊ฐ ๊ฑด์ถ ์ค๊ณ๋งํผ ์ค๋๋์์ ๋๋ ์๋ง๋ ์๋ ๊ท์น๋ค๋ณด๋ค ์๊ฒฉํ ๊ท์น์ ๋ฐ๋ผ์ผ ํ ๊ฒ์ ๋๋ค. ํ์ง๋ง ์ง๊ธ์ ์ด ์ง์นจ์ ๋น์ ๊ณผ ๋น์ ํ์ด ์์ฑํ๋ ํ์ ์คํฌ๋ฆฝํธ ์ฝ๋์ ํ์ง์ ํ๊ฐํ๋ ๊ธฐ์ค์ผ๋ก ์ผ์ผ์ธ์.
ํ ๊ฐ์ง ๋ ๋ง์๋๋ฆฌ์๋ฉด, ์ด ๊ท์น๋ค์ ์๊ฒ ๋๋ค ํด์ ๋น์ฅ ๋ ๋์ ๊ฐ๋ฐ์๊ฐ ๋๋ ๊ฒ์ ์๋๋ฉฐ ์ฝ๋๋ฅผ ์์ฑํ ๋ ์ค์๋ฅผ ํ์ง ์๊ฒ ํด์ฃผ๋ ๊ฒ์ ์๋๋๋ค. ์ ์ ์ ํ ๊ฐ ์ต์ข ์ ๊ฒฐ๊ณผ๋ฌผ๋ก ๋น์ด์ง๋ ๊ฒ์ฒ๋ผ ๋ชจ๋ ์ฝ๋๋ค๋ ์ฒ์ ์์ฑํ ์ฝ๋๋ก ์์๋ฉ๋๋ค. ๊ฒฐ๊ตญ์ ๋๋ฃ๋ค๊ณผ ๋ฆฌ๋ทฐํ๋ฉด์ ๊ฒฐ์ ์ด ์ ๊ฑฐ๋ฉ๋๋ค. ๋น์ ์ด ์ฒ์ ์์ฑํ ์ฝ๋์ ๊ฐ์ ์ด ํ์ํ ๋ ์์ฑ ํ์ง ๋ง์ธ์. ๋์ ์ฝ๋๊ฐ ๋ ๋์์ง๋๋ก ๋๋ค๊ธฐ์ธ์!
๋ณ์
์๋ฏธ์๋ ๋ณ์ ์ด๋ฆ์ ์ฌ์ฉํ์ธ์
์ฝ๋ ์ฌ๋์ผ๋ก ํ์ฌ๊ธ ๋ณ์๋ง๋ค ์ด๋ค ์ ์ด ๋ค๋ฅธ์ง ์ ์ ์๋๋ก ์ด๋ฆ์ ๊ตฌ๋ณํ์ธ์.
Bad:
function between<T>(a1: T, a2: T, a3: T): boolean {
return a2 <= a1 && a1 <= a3;
}
Good:
function between<T>(value: T, left: T, right: T): boolean {
return left <= value && value <= right;
}
๋ฐ์ํ ์ ์๋ ๋ณ์ ์ด๋ฆ์ ์ฌ์ฉํ์ธ์
๋ฐ์ํ ์ ์๋ ์ด๋ฆ์ ๊ทธ ๋ณ์์ ๋ํด์ ๋ฐ๋ณด ๊ฐ์ด ์๋ฆฌ๋ฅผ ๋ด ํ ๋ก ํ ์๋ฐ์ ์์ต๋๋ค.
Bad:
type DtaRcrd102 = {
genymdhms: Date;
modymdhms: Date;
pszqint: number;
}
Good:
type Customer = {
generationTimestamp: Date;
modificationTimestamp: Date;
recordId: number;
}
๋์ผํ ์ ํ์ ๋ณ์๋ ๋์ผํ ๋จ์ด๋ฅผ ์ฌ์ฉํ์ธ์
Bad:
function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;
Good:
function getUser(): User;
๊ฒ์ํ ์ ์๋ ์ด๋ฆ์ ์ฌ์ฉํ์ธ์
์ฝ๋๋ฅผ ์ธ ๋๋ณด๋ค ์ฝ์ ๋๊ฐ ๋ ๋ง๊ธฐ ๋๋ฌธ์ ์ฐ๋ฆฌ๊ฐ ์ฐ๋ ์ฝ๋๋ ์ฝ์ ์ ์๊ณ ๊ฒ์์ด ๊ฐ๋ฅํด์ผ ํฉ๋๋ค. ํ๋ก๊ทธ๋จ์ ์ดํดํ ๋ ์๋ฏธ์๋ ๋ณ์ ์ด๋ฆ์ ์ง์ง ์์ผ๋ฉด ์ฝ๋ ์ฌ๋์ผ๋ก ํ์ฌ๊ธ ์ด๋ ค์์ ์ค ์ ์์ต๋๋ค. ๊ฒ์ ๊ฐ๋ฅํ ์ด๋ฆ์ ์ง์ผ์ธ์. TSLint์ ๊ฐ์ ๋๊ตฌ๋ ์ด๋ฆ์ด ์๋ ์์๋ฅผ ์๋ณํ ์ ์๋๋ก ๋์์ค๋๋ค.
Bad:
// 86400000์ด ๋๋์ฒด ๋ญ์ง?
setTimeout(restart, 86400000);
Good:
// ๋๋ฌธ์๋ก ์ด๋ฃจ์ด์ง ์์๋ก ์ ์ธํ์ธ์.
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
์๋๋ฅผ ๋ํ๋ด๋ ๋ณ์๋ฅผ ์ฌ์ฉํ์ธ์
Bad:
declare const users: Map<string, User>;
for (const keyValue of users) {
// users ๋งต์ ์ํ
}
Good:
declare const users: Map<string, User>;
for (const [id, user] of users) {
// users ๋งต์ ์ํ
}
์์ํ๋ ์ด๋ฆ์ ์ฌ์ฉํ์ง ๋ง์ธ์
๋ช
์์ ์ธ ๊ฒ์ด ์์์ ์ธ ๊ฒ๋ณด๋ค ์ข์ต๋๋ค.
๋ช
๋ฃํจ์ ์ต๊ณ ์
๋๋ค.
Bad:
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Good:
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
๋ถํ์ํ ๋ฌธ๋งฅ์ ์ถ๊ฐํ์ง ๋ง์ธ์
ํด๋์ค/ํ์ /๊ฐ์ฒด์ ์ด๋ฆ์ ์๋ฏธ๊ฐ ๋ด๊ฒจ์๋ค๋ฉด, ๋ณ์ ์ด๋ฆ์์ ๋ฐ๋ณตํ์ง ๋ง์ธ์.
Bad:
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Good:
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
short circuiting์ด๋ ์กฐ๊ฑด๋ฌธ ๋์ ๊ธฐ๋ณธ ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ธ์
๊ธฐ๋ณธ ๋งค๊ฐ๋ณ์๋ short circuiting๋ณด๋ค ๋ณดํต ๋ช ๋ฃํฉ๋๋ค.
Bad:
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// ...
}
Good:
function loadPages(count: number = 10) {
// ...
}
์๋๋ฅผ ์๋ ค์ฃผ๊ธฐ ์ํด enum
์ ์ฌ์ฉํ์ธ์
์๋ฅผ ๋ค์ด ๊ทธ๊ฒ๋ค์ ๊ฐ ์์ฒด๋ณด๋ค ๊ฐ์ด ๊ตฌ๋ณ๋์ด์ผ ํ ๋์ ๊ฐ์ด ์ฝ๋์ ์๋๋ฅผ ์๋ ค์ฃผ๋๋ฐ์ enum
์ ๋์์ ์ค ์ ์์ต๋๋ค.
Bad:
const GENRE = {
ROMANTIC: 'romantic',
DRAMA: 'drama',
COMEDY: 'comedy',
DOCUMENTARY: 'documentary',
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// Projector์ ์ ์ธ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// ์คํ๋์ด์ผ ํ๋ ๋ก์ง
}
}
}
Good:
enum GENRE {
ROMANTIC,
DRAMA,
COMEDY,
DOCUMENTARY,
}
projector.configureFilm(GENRE.COMEDY);
class Projector {
// Projector์ ์ ์ธ
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// ์คํ๋์ด์ผ ํ๋ ๋ก์ง
}
}
}
ํจ์
ํจ์์ ๋งค๊ฐ๋ณ์๋ 2๊ฐ ํน์ ๊ทธ ์ดํ๊ฐ ์ด์์ ์ ๋๋ค
ํจ์ ๋งค๊ฐ๋ณ์์ ๊ฐ์๋ฅผ ์ ํํ๋ ๊ฒ์ ํจ์๋ฅผ ํ ์คํธํ๊ธฐ ์ฝ๊ฒ ๋ง๋ค์ด์ฃผ๊ธฐ ๋๋ฌธ์ ๋๋ผ์ธ ์ ๋๋ก ์ค์ํฉ๋๋ค. ํจ์ ๋งค๊ฐ๋ณ์๊ฐ 3๊ฐ ์ด์์ธ ๊ฒฝ์ฐ, ๊ฐ๊ธฐ ๋ค๋ฅธ ์ธ์๋ก ์ฌ๋ฌ ๋ค๋ฅธ ์ผ์ด์ค๋ฅผ ํ ์คํธํด์ผ ํ๋ฏ๋ก ๊ฒฝ์ฐ์ ์๊ฐ ๋งค์ฐ ๋ง์์ง๋๋ค.
ํ ๊ฐ ํน์ ๋ ๊ฐ์ ๋งค๊ฐ๋ณ์๊ฐ ์ด์์ ์ธ ๊ฒฝ์ฐ๊ณ , ๊ฐ๋ฅํ๋ค๋ฉด ์ธ ๊ฐ๋ ํผํด์ผ ํฉ๋๋ค. ๊ทธ ์ด์์ ๊ฒฝ์ฐ์๋ ํฉ์ณ์ผ ํฉ๋๋ค. ๋ ๊ฐ ์ด์์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์ง ๊ฒฝ์ฐ, ํจ์๊ฐ ๋ง์ ๊ฒ์ ํ ๊ฐ๋ฅ์ฑ์ด ๋์์ง๋๋ค. ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ, ๋๋ถ๋ถ ์์ ๊ฐ์ฒด๋ ํ๋์ ๋งค๊ฐ๋ณ์๋ก ์ถฉ๋ถํ ๊ฒ์ ๋๋ค.
๋ง์ ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค๋ฉด ๊ฐ์ฒด ๋ฆฌํฐ๋ด์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์.
ํจ์๊ฐ ๊ธฐ๋ํ๋ ์์ฑ์ ๋ช ํํ๊ฒ ํ๊ธฐ ์ํด, ๊ตฌ์กฐ ๋ถํด ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด ๊ตฌ๋ฌธ์ ๋ช ๊ฐ์ ์ฅ์ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค:
-
์ด๋ค ์ฌ๋์ด ํจ์ ์๊ทธ๋์ณ(๋งค๊ฐ๋ณ์์ ํ์ , ๋ฐํ๊ฐ์ ํ์ ๋ฑ)๋ฅผ ๋ณผ ๋, ์ด๋ค ์์ฑ์ด ์ฌ์ฉ๋๋์ง ์ฆ์ ์ ์ ์์ต๋๋ค.
-
๋ช ๋ช ๋ ๋งค๊ฐ๋ณ์์ฒ๋ผ ๋ณด์ด๊ฒ ํ ๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
-
๋ํ ๊ตฌ์กฐ ๋ถํด๋ ํจ์๋ก ์ ๋ฌ๋ ๋งค๊ฐ๋ณ์ ๊ฐ์ฒด์ ํน์ ํ ์์ ๊ฐ์ ๋ณต์ ํ๋ฉฐ ์ด๊ฒ์ ์ฌ์ด๋ ์ดํํธ๋ฅผ ๋ฐฉ์งํ๋๋ฐ ๋์์ ์ค๋๋ค. ์ ์์ฌํญ: ๋งค๊ฐ๋ณ์ ๊ฐ์ฒด๋ก๋ถํฐ ๊ตฌ์กฐ ๋ถํด๋ ๊ฐ์ฒด์ ๋ฐฐ์ด์ ๋ณต์ ๋์ง ์์ต๋๋ค.
-
ํ์ ์คํฌ๋ฆฝํธ๋ ์ฌ์ฉํ์ง ์์ ์์ฑ์ ๋ํด์ ๊ฒฝ๊ณ ๋ฅผ ์ฃผ๋ฉฐ, ๊ตฌ์กฐ ๋ถํด๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฒฝ๊ณ ๋ฅผ ๋ฐ์ง ์์ ์ ์์ต๋๋ค.
Bad:
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
Good:
function createMenu(options: { title: string, body: string, buttonText: string, cancellable: boolean }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
ํ์ ์จ๋ฆฌ์ด์ค๋ฅผ ์ฌ์ฉํด์ ๊ฐ๋ ์ฑ์ ๋ ๋์ผ ์ ์์ต๋๋ค:
type MenuOptions = { title: string, body: string, buttonText: string, cancellable: boolean };
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
ํจ์๋ ํ ๊ฐ์ง๋ง ํด์ผํฉ๋๋ค
์ด๊ฒ์ ์ํํธ์จ์ด ๊ณตํ์์ ๋จ์ฐ์ฝ ๊ฐ์ฅ ์ค์ํ ๊ท์น์ ๋๋ค. ํจ์๊ฐ ํ ๊ฐ์ง ์ด์์ ์ญํ ์ ์ํํ ๋ ์์ฑํ๊ณ ํ ์คํธํ๊ณ ์ถ๋ก ํ๊ธฐ ์ด๋ ค์์ง๋๋ค. ํจ์๋ฅผ ํ๋์ ํ๋์ผ๋ก ์ ์ํ ์ ์์ ๋, ์ฝ๊ฒ ๋ฆฌํฉํ ๋งํ ์ ์์ผ๋ฉฐ ์ฝ๋๋ฅผ ๋์ฑ ๋ช ๋ฃํ๊ฒ ์ฝ์ ์ ์์ต๋๋ค. ์ด ๊ฐ์ด๋์์ ์ด ๋ถ๋ถ๋ง ์๊ธฐ๊ฒ์ผ๋ก ๋ง๋ค์ด๋ ๋น์ ์ ๋ง์ ๊ฐ๋ฐ์๋ณด๋ค ์์ค ์ ์์ต๋๋ค.
Bad:
function emailClients(clients: Client[]) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good:
function emailClients(clients: Client[]) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
ํจ์๊ฐ ๋ฌด์์ ํ๋์ง ์ ์ ์๋๋ก ํจ์ ์ด๋ฆ์ ์ง์ผ์ธ์
Bad:
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// ๋ฌด์์ด ์ถ๊ฐ๋๋์ง ํจ์ ์ด๋ฆ๋ง์ผ๋ก ์ ์ถํ๊ธฐ ์ด๋ ต์ต๋๋ค
addToDate(date, 1);
Good:
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
ํจ์๋ ๋จ์ผ ํ๋์ ์ถ์ํํด์ผ ํฉ๋๋ค
ํจ์๊ฐ ํ ๊ฐ์ง ์ด์์ ์ถ์ํํ๋ค๋ฉด ๊ทธ ํจ์๋ ๋๋ฌด ๋ง์ ์ผ์ ํ๊ฒ ๋ฉ๋๋ค. ์ฌ์ฌ์ฉ์ฑ๊ณผ ์ฌ์ด ํ ์คํธ๋ฅผ ์ํด์ ํจ์๋ฅผ ์ชผ๊ฐ์ธ์.
Bad:
function parseCode(code: string) {
const REGEXES = [ /* ... */ ];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
Good:
const REGEXES = [ /* ... */ ];
function parseCode(code: string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) => {
// parse...
});
}
function tokenize(code: string): Token[] {
const statements = code.split(' ');
const tokens: Token[] = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});
return tokens;
}
function parse(tokens: Token[]): SyntaxTree {
const syntaxTree: SyntaxTree[] = [];
tokens.forEach((token) => {
syntaxTree.push( /* ... */ );
});
return syntaxTree;
}
์ค๋ณต๋ ์ฝ๋๋ฅผ ์ ๊ฑฐํด์ฃผ์ธ์
์ฝ๋๊ฐ ์ค๋ณต๋์ง ์๋๋ก ์ต์ ์ ๋คํ์ธ์. ์ค๋ณต๋ ์ฝ๋๋ ์ด๋ค ๋ก์ง์ ๋ณ๊ฒฝํ ๋ ํ ๊ณณ ์ด์์ ๋ณ๊ฒฝํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ข์ง ์์ต๋๋ค.
๋น์ ์ด ๋ ์คํ ๋์ ์ด์ํ๋ฉด์ ์ฌ๊ณ ๋ฅผ ์ถ์ ํ๋ค๊ณ ์์ํด๋ณด์ธ์: ๋ชจ๋ ํ ๋งํ , ์ํ, ๋ง๋, ์๋ ๋ฑ. ๊ด๋ฆฌํ๋ ๋ชฉ๋ก์ด ์ฌ๋ฌ๊ฐ์ผ ๋ ํ ๋งํ ๋ฅผ ๋ฃ์ ์๋ฆฌ๋ฅผ ์ ๊ณตํ ๋๋ง๋ค ๋ชจ๋ ๋ชฉ๋ก์ ์์ ํด์ผ ํฉ๋๋ค. ๊ด๋ฆฌํ๋ ๋ชฉ๋ก์ด ๋จ ํ๋์ผ ๋๋ ํ ๊ณณ๋ง ์์ ํ๋ฉด ๋ฉ๋๋ค!
๋น์ ์ ์ข ์ข ๋ ๊ฐ ์ด์์ ์ฌ์ํ ์ฐจ์ด์ ์ด ์กด์ฌํ๋ค๊ณ ์๊ฐํด์ ๊ฑฐ์ ๋น์ทํ ์ฝ๋๋ฅผ ์ค๋ณต ์์ฑํฉ๋๋ค. ํ์ง๋ง ๊ทธ ๋ช๊ฐ์ง ๋ค๋ฅธ ๊ฒ์ผ๋ก ์ธํด ๊ฐ์ ์ญํ ์ ํ๋ ๋ ๊ฐ ์ด์์ ํจ์๋ฅผ ๋ง๋ค๊ฒ ๋ฉ๋๋ค. ์ค๋ณต๋ ์ฝ๋๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ ์กฐ๊ธ์ฉ ๋ค๋ฅธ ์ญํ ์ ํ๋ ๊ฒ์ ๋ฌถ์์ผ๋ก์จ ํ๋์ ํจ์/๋ชจ๋/ํด๋์ค๋ก ์ฒ๋ฆฌํ๋ ์ถ์ํ๋ฅผ ๋ง๋๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
์ถ์ํ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ํ๋ ๊ฒ์ ์ค์ํ๋ฉฐ, ์ด๊ฒ์ SOLID ์์น์ ๋ฐ๋ฅด๋ ์ด์ ์ด๊ธฐ๋ ํฉ๋๋ค. ์ฌ๋ฐ๋ฅด์ง ์์ ์ถ์ํ๋ ์ค๋ณต๋ ์ฝ๋๋ณด๋ค ๋์๋ฏ๋ก ์ฃผ์ํ์ธ์! ์ข์ ์ถ์ํ๋ฅผ ํ ์ ์๋ค๋ฉด ๊ทธ๋ ๊ฒ ํ๋ผ๋ ๋ง์ ๋๋ค! ๋ฐ๋ณตํ์ง ๋ง์ธ์. ๊ทธ๋ ์ง ์์ผ๋ฉด ํ๋๋ฅผ ๋ณ๊ฒฝํ ๋๋ง๋ค ์ฌ๋ฌ ๊ณณ์ ๋ณ๊ฒฝํ๊ฒ ๋ ๊ฒ์ ๋๋ค.
Bad:
function showDeveloperList(developers: Developer[]) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Good:
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
๋น์ ์ ์ค๋ณต๋ ์ฝ๋์ ๋ํด์ ๋นํ์ ์ผ๋ก ์๊ฐํด์ผ ํฉ๋๋ค. ๊ฐ๋์ ์ค๋ณต๋ ์ฝ๋์ ๋ถํ์ํ ์ถ์ํ๋ก ์ธํ ๋ณต์ก์ฑ ๊ฐ์ ๋ง๋ฐ๊ฟ์ด ์์ ์ ์์ต๋๋ค. ์๋ก ๋ค๋ฅธ ๋ ๊ฐ์ ๋ชจ๋์ ๊ตฌํ์ด ์ ์ฌํด ๋ณด์ด์ง๋ง ์๋ก ๋ค๋ฅธ ๋๋ฉ์ธ์ ์กด์ฌํ๋ ๊ฒฝ์ฐ, ์ฝ๋ ์ค๋ณต์ ๊ณตํต๋ ์ฝ๋์์ ์ถ์ถํด์ ์ค๋ณต์ ์ค์ด๋ ๊ฒ๋ณด๋ค ๋์ ์ ํ์ผ ์ ์์ต๋๋ค. ์ด ๊ฒฝ์ฐ์ ์ถ์ถ๋ ๊ณตํต์ ์ฝ๋๋ ๋ ๋ชจ๋ ์ฌ์ด์์ ๊ฐ์ ์ ์ธ ์์กด์ฑ์ด ๋ํ๋๊ฒ ๋ฉ๋๋ค.
Object.assign
ํน์ ๊ตฌ์กฐ ๋ถํด๋ฅผ ์ฌ์ฉํด์ ๊ธฐ๋ณธ ๊ฐ์ฒด๋ฅผ ๋ง๋์ธ์
Bad:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({ body: 'Bar' });
Good:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// ...
}
createMenu({ body: 'Bar' });
๋์์ผ๋ก, ๊ธฐ๋ณธ ๊ฐ์ ๊ตฌ์กฐ ๋ถํด๋ฅผ ์ฌ์ฉํด์ ํด๊ฒฐํ ์ ์์ต๋๋ค:
type MenuConfig = { title?: string, body?: string, buttonText?: string, cancellable?: boolean };
function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
// ...
}
createMenu({ body: 'Bar' });
์ฌ์ด๋ ์ดํํธ์ undefined
ํน์ null
๊ฐ์ ๋ช
์์ ์ผ๋ก ๋๊ธฐ๋ ์์์น ๋ชปํ ํ๋์ ํผํ๊ธฐ ์ํด์ ํ์
์คํฌ๋ฆฝํธ ์ปดํ์ผ๋ฌ์๊ฒ ๊ทธ๊ฒ์ ํ๋ฝํ์ง ์๋๋ก ์ค์ ํ ์ ์์ต๋๋ค. ํ์
์คํฌ๋ฆฝํธ์์ --strictNullChecks
์ต์
์ ํ์ธํ์ธ์.
ํจ์ ๋งค๊ฐ๋ณ์๋ก ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ์ง ๋ง์ธ์
ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ํด๋น ํจ์๊ฐ ํ ๊ฐ์ง ์ด์์ ์ผ์ ํ๋ค๋ ๊ฒ์ ๋ปํฉ๋๋ค. ํจ์๋ ํ ๊ฐ์ง์ ์ผ์ ํด์ผํฉ๋๋ค. boolean ๋ณ์๋ก ์ธํด ๋ค๋ฅธ ์ฝ๋๊ฐ ์คํ๋๋ค๋ฉด ๊ทธ ํจ์๋ฅผ ์ชผ๊ฐ๋๋ก ํ์ธ์.
Bad:
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good:
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
์ฌ์ด๋ ์ดํํธ๋ฅผ ํผํ์ธ์ (ํํธ 1)
ํจ์๋ ๊ฐ์ ๊ฐ์ ธ์์ ๋ค๋ฅธ ๊ฐ์ ๋ฐํํ๋ ๊ฒ ์ด์ธ์ ๋ค๋ฅธ ๊ฒ์ ํ ๊ฒฝ์ฐ ์ฌ์ด๋ ์ดํํธ๋ฅผ ๋ฐ์์ํฌ ์ ์์ต๋๋ค. ์ฌ์ด๋ ์ดํํธ๋ ํ์ผ์ ์ด๋ค๊ฑฐ๋, ์ ์ญ ๋ณ์๋ฅผ ์กฐ์ํ๋ค๊ฑฐ๋, ๋ปํ์ง ์๊ฒ ๋ฏ์ ์ฌ๋์๊ฒ ๋น์ ์ ์ ์ฌ์ฐ์ ์ก๊ธํ ์ ์์ต๋๋ค.
๋น์ ์ ๊ฐ๋ ํ๋ก๊ทธ๋จ์์ ์ฌ์ด๋ ์ดํํธ๋ฅผ ๊ฐ์ง ํ์๊ฐ ์์ต๋๋ค. ์ด์ ์ ์ฌ๋ก์์์ ๊ฐ์ด ๋น์ ์ ํ์ผ์ ์จ์ผํ ๋๊ฐ ์์ต๋๋ค. ๋น์ ์ด ํ๊ณ ์ถ์ ๊ฒ์ ์ด๊ฒ์ ์ค์ํํ๋ ๊ฒ์ ๋๋ค. ํน์ ํ์ผ์ ์ฐ๊ธฐ ์ํด ๋ช ๊ฐ์ ํจ์์ ํด๋์ค๋ฅผ ๋ง๋ค์ง ๋ง์ธ์. ๊ทธ๊ฒ์ ํํ๋ ์๋น์ค๋ฅผ ๋จ ํ๋๋ง ๋ง๋์ธ์.
์ค์ํ ๊ฒ์ ์ด๋ ํ ๊ตฌ์กฐ๋ ์์ด ๊ฐ์ฒด ์ฌ์ด์ ์ํ๋ฅผ ๊ณต์ ํ๊ฑฐ๋ ์ด๋ค ๊ฒ์ ์ํด์๋ ์ง ๋ณ๊ฒฝ๋ ์ ์๋ ๋ฐ์ดํฐ ํ์ ์ ์ฌ์ฉํ๊ฑฐ๋ ์ฌ์ด๋ ์ดํํธ๊ฐ ์ผ์ด๋๋ ๊ณณ์ ์ค์ํ ํ์ง ์๋ ๊ฒ๊ณผ ๊ฐ์ ์ํ ์์๋ฅผ ํผํ๋ ๊ฒ์ ๋๋ค. ๋ง์ฝ ๊ทธ๋ ๊ฒ ํ ์ ์๋ค๋ฉด, ๋น์ ์ ๋๋ถ๋ถ์ ๋ค๋ฅธ ํ๋ก๊ทธ๋๋จธ๋ค๋ณด๋ค ๋์ฑ ํ๋ณตํ ๊ฒ์ ๋๋ค.
Bad:
// ์๋์ ํจ์์์ ์ฐธ์กฐํ๋ ์ ์ญ ๋ณ์์
๋๋ค.
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64();
// ์ด ์ด๋ฆ์ ์ฌ์ฉํ๋ ๋ค๋ฅธ ํจ์๊ฐ ์๋ค๋ฉด, ๊ทธ๊ฒ์ Base64 ๊ฐ์ ๋ฐํํ ๊ฒ์
๋๋ค
console.log(name); // 'Robert C. Martin'์ด ์ถ๋ ฅ๋๋ ๊ฒ์ ์์ํ์ง๋ง 'Um9iZXJ0IEMuIE1hcnRpbg=='๊ฐ ์ถ๋ ฅ๋จ
Good:
const name = 'Robert C. Martin';
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
์ฌ์ด๋ ์ดํํธ๋ฅผ ํผํ์ธ์ (ํํธ 2)
์๋ฐ์คํฌ๋ฆฝํธ์์ ์์๊ฐ์ ๊ฐ์ ์ํด ์ ๋ฌ๋๊ณ ๊ฐ์ฒด/๋ฐฐ์ด์ ์ฐธ์กฐ์ ์ํด ์ ๋ฌ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๊ฐ์ฒด์ ๋ฐฐ์ด์ ๊ฒฝ์ฐ ์ด๋ค ํจ์๊ฐ ์ผํ ์ฅ๋ฐ๊ตฌ๋ ๋ฐฐ์ด์ ๋ณ๊ฒฝํ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง๊ณ ์๋ค๋ฉด, ๊ตฌ๋งคํ๋ ค๋ ์์ดํ
์ด ์ถ๊ฐ๋จ์ผ๋ก์จ cart
๋ฐฐ์ด์ ์ฌ์ฉํ๋ ๋ค๋ฅธ ํจ์๋ ์ด ์ถ๊ฐ์ ์ํฅ์ ๋ฐ์ ์ ์์ต๋๋ค. ์ด๊ฒ์ ์ฅ์ ์ด ๋ ์๋ ์์ง๋ง ๋จ์ ์ด ๋ ์๋ ์์ต๋๋ค. ์ต์
์ ์ํฉ์ ์์ํด๋ณด๊ฒ ์ต๋๋ค:
์ฌ์ฉ์๋ ๋คํธ์ํฌ ์์ฒญ์ ์์ฑํ๊ณ ์๋ฒ์ cart
๋ฐฐ์ด์ ์ ์กํ๋ purchase
ํจ์๋ฅผ ํธ์ถํ๋ โ๊ตฌ๋งคโ ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค. ๋คํธ์ํฌ ์ฐ๊ฒฐ ๋ถ๋ ๋๋ฌธ์ purchase
ํจ์๋ ์์ฒญ์ ์ฌ์๋ํด์ผ ํฉ๋๋ค. ๋คํธ์ํฌ ์์ฒญ์ด ์์๋๊ธฐ ์ ์ ์ฌ์ฉ์๊ฐ ์ํ์ง ์์ ์์ดํ
์ ์ค์๋ก โ์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐํ๊ธฐโ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ๋คํธ์ํฌ ์์ฒญ์ด ์์๋๋ฉด, purchase
ํจ์๋ addItemToCart
ํจ์๊ฐ ๋ณ๊ฒฝํ ์ผํ ์ฅ๋ฐ๊ตฌ๋ ๋ฐฐ์ด์ ์ฐธ์กฐํ๊ณ ์๊ธฐ ๋๋ฌธ์ purchase
ํจ์๋ ์ค์๋ก ์ถ๊ฐ๋ ์์ดํ
์ ๋ณด๋ผ ๊ฒ์
๋๋ค.
ํ๋ฅญํ ํด๋ฒ์ addItemToCart
ํจ์์์ cart
๋ฐฐ์ด์ ๋ณต์ ํ๊ณ ๊ทธ๊ฒ์ ์์ ํ๊ณ ๊ทธ ๋ณต์ ํ ๊ฐ์ ๋ฐํํ๋ ๊ฒ์
๋๋ค. ์ด๋ ์ผํ ์ฅ๋ฐ๊ตฌ๋ ๋ฐฐ์ด์ ์ฐธ์กฐํ๊ณ ์๋ ๊ฐ์ ๋ค๊ณ ์๋ ์ด๋ค ๋ค๋ฅธ ํจ์๋ ๋ค๋ฅธ ๋ณ๊ฒฝ์ ์ํด ์ํฅ์ ๋ฐ์ง ์๋ ๊ฒ์ ๋ณด์ฅํฉ๋๋ค.
์ด ์ ๊ทผ๋ฒ์ ๋ํ ๋ ๊ฐ์ง ์ฃผ์์ฌํญ:
-
์ค์ ๋ก๋ ์ ๋ ฅ๋ ๊ฐ์ฒด๊ฐ์ ๋ณ๊ฒฝํ๊ธฐ๋ฅผ ์ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ ์ ์์ต๋๋ค. ํ์ง๋ง ์ด๋ฌํ ํ๋ก๊ทธ๋๋ฐ ๊ด๋ก๋ฅผ ์ ํํ ๋ ๋น์ ์ ์ด๋ฌํ ๊ฒฝ์ฐ๊ฐ ๋งค์ฐ ๋๋ฌผ๋ค๋ ๊ฒ์ ์๊ฒ ๋ ๊ฒ์ ๋๋ค. ๋๋ถ๋ถ์ ์ฌ์ด๋ ์ดํํธ๊ฐ ์๋๋ก ๋ฆฌํฉํ ๋ง๋ ์ ์์ต๋๋ค! (์์ ํจ์๋ฅผ ํ์ธํด์ฃผ์ธ์)
-
ํฐ ๊ฐ์ฒด๋ฅผ ๋ณต์ ํ๋ ๊ฒ์ ์ฑ๋ฅ ๊ด์ ์์ ๋น์ฉ์ด ๋์ ์ ์์ต๋๋ค. ๋คํํ๋ ์ด๋ฌํ ํ๋ก๊ทธ๋๋ฐ ์ ๊ทผ๋ฒ์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ํ๋ฅญํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๊ธฐ ๋๋ฌธ์ ํฐ ๋ฌธ์ ๋ ์๋๋๋ค. ์ด๋ ์๋์ผ๋ก ๊ฐ์ฒด์ ๋ฐฐ์ด์ ๋ณต์ ํด์ฃผ๋ ๊ฒ๋งํผ ๋ฉ๋ชจ๋ฆฌ ์ง์ฝ์ ์ด์ง ์๊ฒ ํด์ฃผ๊ณ ๋น ๋ฅด๊ฒ ๋ณต์ ํด์ค๋๋ค.
Bad:
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({ item, date: Date.now() });
};
Good:
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return [...cart, { item, date: Date.now() }];
};
์ ์ญ ํจ์๋ฅผ ์์ฑํ์ง ๋ง์ธ์
์ ์ญ์ ๋๋ฝํ๋ ๊ฒ์ ์๋ฐ์คํฌ๋ฆฝํธ์์ ๋์ ๊ด์ต์
๋๋ค. ์๋ํ๋ฉด ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ถฉ๋๋ ์ ์๊ณ ๋น์ ์ API์ ์ฌ์ฉ์๋ ์์ฉ์์ ์์ธ๊ฐ ๋ฐ์ํ ๋๊น์ง ์ ํ ๋ชจ๋ฅผ ๊ฒ์ด๊ธฐ ๋๋ฌธ์
๋๋ค. ํ ์์ ๋ฅผ ์๊ฐํด๋ณด๊ฒ ์ต๋๋ค: ๋น์ ์ด ์๋ฐ์คํฌ๋ฆฝํธ ๋ค์ดํฐ๋ธ ๋ฐฐ์ด ๋ฉ์๋๋ฅผ ํ์ฅํด์ ๋ ๋ฐฐ์ด ์ฌ์ด์ ๋ค๋ฅธ ์ ์ ๋ณด์ฌ์ฃผ๋ diff
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๊ณ ์ถ๋ค๋ฉด ์ด๋จ๊น์? Array.prototype
์ ๋น์ ์ ์๋ก์ด ํจ์๋ฅผ ์์ฑํ ๊ฒ์
๋๋ค. ํ์ง๋ง ๋์ผํ ๊ธฐ๋ฅ์ ์ํํ๊ณ ์๋ ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ถฉ๋๋ ์ ์์ต๋๋ค. ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ๋ฐฐ์ด์์ ์ฒซ ๋ฒ์งธ ์์์ ๋ง์ง๋ง ์์ ์ฌ์ด์ ๋ค๋ฆ๋ง ์ฐพ๊ธฐ ์ํด diff
ํจ์๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์ด๋จ๊น์? ์ด๊ฒ์ด ๋จ์ง ํด๋์ค๋ฅผ ์ฌ์ฉํด์ ์ ์ญ Array
๋ฅผ ์์ํ๋ ๊ฒ์ด ๋ ์ข์ ์ด์ ์
๋๋ค.
Bad:
declare global {
interface Array<T> {
diff(other: T[]): Array<T>;
}
}
if (!Array.prototype.diff) {
Array.prototype.diff = function <T>(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
Good:
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
๋ช ๋ นํ ํ๋ก๊ทธ๋๋ฐ๋ณด๋ค ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ์งํฅํ์ธ์
๊ฐ๋ฅํ๋ค๋ฉด ์ด๋ฐ ๋ฐฉ์์ ํ๋ก๊ทธ๋๋ฐ์ ์งํฅํ์ธ์.
Bad:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Good:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);
์กฐ๊ฑด๋ฌธ์ ์บก์ํํ์ธ์
Bad:
if (subscription.isTrial || account.balance > 0) {
// ...
}
Good:
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0;
}
if (canActivateService(subscription, account)) {
// ...
}
๋ถ์ ์กฐ๊ฑด๋ฌธ์ ํผํ์ธ์
Bad:
function isEmailNotUsed(email: string): boolean {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Good:
function isEmailUsed(email): boolean {
// ...
}
if (!isEmailUsed(node)) {
// ...
}
์กฐ๊ฑด๋ฌธ์ ํผํ์ธ์
๋ถ๊ฐ๋ฅํด๋ณด์ผ ์ ์์ต๋๋ค. ์ฒ์ ์ด๋ฅผ ๋ณธ ๋๋ถ๋ถ์ ์ฌ๋๋ค์ โ๋์ฒด if
๋ฌธ ์์ด ๋ญ ํ ์ ์๋์?โ ๋ผ๊ณ ๋ฐ์ํฉ๋๋ค. ํ์ง๋ง ๋ง์ ๊ฒฝ์ฐ์ ๋คํ์ฑ์ ์ฌ์ฉํ๋ค๋ฉด ํด๊ฒฐํ ์ ์์ต๋๋ค. ๊ทธ ๋ค์ ๋ฐ์์ผ๋ก๋ โ์ข์์. ํ์ง๋ง ์ ๊ทธ๋์ผํ์ฃ ?โ ์
๋๋ค. ์ด์ ๋ํ ํด๋ต์ ์ฐ๋ฆฌ๊ฐ ์ด์ ์ ๋ฐฐ์ด ํด๋ฆฐ ์ฝ๋ ์ปจ์
์ค โํจ์๋ ํ ๊ฐ์ง ์ผ๋ง ํด์ผํฉ๋๋คโ ์
๋๋ค. if
๋ฌธ์ด ์๋ ํด๋์ค์ ํจ์๊ฐ ์๋ค๋ฉด, ๊ทธ ํจ์๋ ํ ๊ฐ์ง ์ด์์ ์ผ์ ํ๊ณ ์๋ค๋ ๊ฒ์
๋๋ค. ํจ์๋ ํ ๊ฐ์ง ์ผ๋ง ํด์ผํ๋ค๋ ๊ฒ์ ๊ธฐ์ตํ์ธ์.
Bad:
class Airplane {
private type: string;
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
default:
throw new Error('Unknown airplane type.');
}
}
private getMaxAltitude(): number {
// ...
}
}
Good:
abstract class Airplane {
protected getMaxAltitude(): number {
// shared logic with subclasses ...
}
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}
class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}
class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
ํ์ ์ฒดํน์ ํผํ์ธ์
ํ์ ์คํฌ๋ฆฝํธ๋ ์๋ฐ์คํฌ๋ฆฝํธ์ ์๊ฒฉํ ๊ตฌ๋ฌธ์ ์์ ์งํฉ์ด๋ฉฐ ์ธ์ด์ ์ ํ์ ์ธ ์ ์ ํ์ ๊ฒ์ฌ ๊ธฐ๋ฅ์ ์ถ๊ฐํฉ๋๋ค. ํ์ ์คํฌ๋ฆฝํธ์ ๊ธฐ๋ฅ์ ์ต๋ํ ํ์ฉํ๊ธฐ ์ํด ํญ์ ๋ณ์์ ํ์ , ๋งค๊ฐ๋ณ์, ๋ฐํ๊ฐ์ ํ์ ์ ์ง์ ํ๋๋ก ํ์ธ์. ๊ทธ๋ ๊ฒ ํ๋ฉด ๋ฆฌํฉํ ๋ง์ด ๋งค์ฐ ์ฌ์์ง๋๋ค.
Bad:
function travelToTexas(vehicle: Bicycle | Car) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(currentLocation, new Location('texas'));
}
}
Good:
type Vehicle = Bicycle | Car;
function travelToTexas(vehicle: Vehicle) {
vehicle.move(currentLocation, new Location('texas'));
}
ํ์ ์ด์์ผ๋ก ์ต์ ํํ์ง ๋ง์ธ์
ํ๋ ๋ธ๋ผ์ฐ์ ๋ ๋ฐํ์์์ ๋ง์ ์ต์ ํ๋ฅผ ์ํํฉ๋๋ค. ๋ง์ ์๊ฐ์ ์ต์ ํํ๋ ๋ฐ์ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด ์๊ฐ ๋ญ๋น์ ๋๋ค. ์ต์ ํ๊ฐ ๋ถ์กฑํ ๋ถ๋ถ์ ํ์ธํ ์ ์๋ ์ข์ ์๋ฃ๊ฐ ์์ต๋๋ค. ์ด๊ฒ์ ์ฐธ์กฐํ์ฌ ์ต์ ํ๊ฐ ๋ถ์กฑํ ๋ถ๋ถ๋ง ์ต์ ํํด์ค ์ ์์ต๋๋ค.
Bad:
// ์์ ๋ธ๋ผ์ฐ์ ์์๋ ์บ์๋์ง ์์ `list.length`๋ฅผ ์ฌ์ฉํ ๊ฐ ์ํ๋ ๋น์ฉ์ด ๋ง์ด ๋ค ๊ฒ์
๋๋ค.
// `list.length`์ ์ฌ๊ณ์ฐ ๋๋ฌธ์
๋๋ค. ํ๋ ๋ธ๋ผ์ฐ์ ์์๋ ์ด ๋ถ๋ถ์ด ์ต์ ํ๋ฉ๋๋ค.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (let i = 0; i < list.length; i++) {
// ...
}
ํ์ํ์ง ์๋ ์ฝ๋๋ ์ ๊ฑฐํ์ธ์
์ฌ์ฉํ์ง ์์ ์ฝ๋๋ ์ค๋ณต๋ ์ฝ๋๋งํผ ๋์ฉ๋๋ค. ๋น์ ์ ์ฝ๋์์ ์ด๊ฒ์ ์ ์งํ ์ด์ ๋ ์์ต๋๋ค. ํธ์ถ๋์ง ์์ ์ฝ๋๊ฐ ์๋ค๋ฉด ์ ๊ฑฐํ์ธ์! ์ง์ด ์ฝ๋๋ฅผ ๋ค์ ํ์ธํ ํ์๊ฐ ์๋ค๋ฉด ๋ฒ์ ๊ธฐ๋ก์์ ๋ณผ ์ ์์ต๋๋ค.
Bad:
function oldRequestModule(url: string) {
// ...
}
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
iterator
์ generator
๋ฅผ ์ฌ์ฉํ์ธ์
์คํธ๋ฆผ๊ณผ ๊ฐ์ด ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ ์ฝ๋ ์
์ ์ฌ์ฉํ ๋๋ generator
์ iterable
์ ์ฌ์ฉํ์ธ์.
๋ช ๊ฐ์ง์ ์ข์ ์ด์ ๊ฐ ์์ต๋๋ค:
- ํผํธ์ถ์๊ฐ ์ ๊ทผํ ์์ดํ
์๋ฅผ ๊ฒฐ์ ํ๋ค๋ ์๋ฏธ์์ ํผํธ์ถ์๋ฅผ
generator
๊ตฌํ์ผ๋ก๋ถํฐ ๋ถ๋ฆฌํ ์ ์์ต๋๋ค. - ์ง์ฐ ์คํ, ์์ดํ ์ ์๊ตฌ์ ์ํด ์คํธ๋ฆผ ์ฒ๋ฆฌ๋ ์ ์์ต๋๋ค.
for-of
๊ตฌ๋ฌธ์ ์ฌ์ฉํด ์์ดํ ์ ์ํํ๋ ๋ด์ฅ ์ง์์ด ์์ต๋๋ค.iterable
์ ์ต์ ํ๋iterator
ํจํด์ ๊ตฌํํ ์ ์์ต๋๋ค.
Bad:
function fibonacci(n: number): number[] {
if (n === 1) return [0];
if (n === 2) return [0, 1];
const items: number[] = [0, 1];
while (items.length < n) {
items.push(items[items.length - 2] + items[items.length - 1]);
}
return items;
}
function print(n: number) {
fibonacci(n).forEach(fib => console.log(fib));
}
// ํผ๋ณด๋์น ์ซ์์ ์ฒซ ๋ฒ์งธ 10๊ฐ ์ซ์๋ฅผ ์ถ๋ ฅํฉ๋๋ค.
print(10);
Good:
// ํผ๋ณด๋์น ์ซ์์ ๋ฌดํ ์คํธ๋ฆผ์ ์์ฑํฉ๋๋ค.
// `generator`๋ ๋ชจ๋ ์ซ์์ ๋ฐฐ์ด์ ์ ์งํ๊ณ ์์ง ์์ต๋๋ค.
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function print(n: number) {
let i = 0;
for (const fib of fibonacci()) {
if (i++ === n) break;
console.log(fib);
}
}
// ํผ๋ณด๋์น ์ซ์์ ์ฒซ ๋ฒ์งธ 10๊ฐ ์ซ์๋ฅผ ์ถ๋ ฅํฉ๋๋ค.
print(10);
map
, slice
, forEach
๋ฑ๊ณผ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ฐ๊ฒฐํจ์ผ๋ก์จ ๋ค์ดํฐ๋ธ ๋ฐฐ์ด์ ๋น์ทํ ๋ฐฉ๋ฒ์ผ๋ก iterable
๋ก ์์
ํ ์ ์๊ฒ ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ต๋๋ค.
iterable
์ ๋ฐ์ ๋ ์กฐ์์ ์ฌ๋ก๋ฅผ ์ํด itiriri๋ฅผ ํ์ธํด์ฃผ์ธ์. (๋๋ ๋น๋๊ธฐ iterable
์ ์กฐ์์ ์ํด์ itiriri-async๋ฅผ ํ์ธํด์ฃผ์ธ์.)
import itiriri from 'itiriri';
function* fibonacci(): IterableIterator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
itiriri(fibonacci())
.take(10)
.forEach(fib => console.log(fib));
๊ฐ์ฒด์ ์๋ฃ๊ตฌ์กฐ
getter
์ setter
๋ฅผ ์ฌ์ฉํ์ธ์
ํ์
์คํฌ๋ฆฝํธ๋ getter
/setter
๊ตฌ๋ฌธ์ ์ง์ํฉ๋๋ค.
ํ๋์ ์บก์ํํ ๊ฐ์ฒด์์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ทผํ๊ธฐ ์ํด getter
์ setter
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ฐ์ฒด์์ ์์ฑ์ ๋จ์ํ ์ฐพ๋ ๊ฒ๋ณด๋ค ๋ซ์ต๋๋ค.
โ์ ๊ทธ๋ ์ต๋๊น?โ ๋ผ๊ณ ๋ฌผ์ ์ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ ์ด์ ๊ฐ ์์ต๋๋ค:
- ๊ฐ์ฒด ์์ฑ์ ์ป๋ ๊ฒ ์ด์์ผ๋ก ๋ฌด์ธ๊ฐ๋ฅผ ๋ ํ๊ณ ์ถ์ ๋, ์ฝ๋ ์์์ ๊ด๋ จ๋ ๋ชจ๋ ์ ๊ทผ์๋ฅผ ์ฐพ๊ณ ๋ณ๊ฒฝํ์ง ์์๋ ๋ฉ๋๋ค.
set
์ ์ฌ์ฉํ ๋ ๊ฒ์ฆ ๋ก์ง์ ์ถ๊ฐํ๋ ๊ฒ์ด ๊ฐ๋จํฉ๋๋ค.- ๋ด๋ถ์ API๋ฅผ ์บก์ํํ ์ ์์ต๋๋ค.
- ๊ฐ์ ์กฐํํ๊ณ ์ค์ ํ ๋ ๋ก๊ทธ๋ฅผ ๊ธฐ๋กํ๊ณ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์ฝ์ต๋๋ค.
- ์๋ฒ์์ ๊ฐ์ฒด ์์ฑ์ ๋ถ๋ฌ์ฌ ๋ ์ง์ฐ ๋ก๋ฉํ ์ ์์ต๋๋ค.
Bad:
type BankAccount = {
balance: number;
// ...
}
const value = 100;
const account: BankAccount = {
balance: 0,
// ...
};
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
account.balance = value;
Good:
class BankAccount {
private accountBalance: number = 0;
get balance(): number {
return this.accountBalance;
}
set balance(value: number) {
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
this.accountBalance = value;
}
// ...
}
// ์ด์ `BankAccount`๋ ๊ฒ์ฆ ๋ก์ง์ ์บก์ํํฉ๋๋ค.
// ๋ช
์ธ๊ฐ ๋ฐ๋๋ค๋ฉด, ์ถ๊ฐ์ ์ธ ๊ฒ์ฆ ๊ท์น์ ์ถ๊ฐํ ํ์๊ฐ ์์ต๋๋ค.
// ๊ทธ ๋, `setter` ๊ตฌํ๋ถ๋ง ์์ ํ๋ฉด ๋ฉ๋๋ค.
// ๊ด๋ จ์๋ ๋ค๋ฅธ ์ฝ๋๋ ๋ณ๊ฒฝํ ํ์๊ฐ ์์ต๋๋ค.
const account = new BankAccount();
account.balance = 100;
private/protected ๋ฉค๋ฒ๋ฅผ ๊ฐ๋ ๊ฐ์ฒด๋ฅผ ์์ฑํ์ธ์
ํ์
์คํฌ๋ฆฝํธ๋ ํด๋์ค ๋ฉค๋ฒ๋ฅผ ์ํด public
๊ธฐ๋ณธ, protected
, private
์ ๊ทผ์๋ฅผ ์ง์ํฉ๋๋ค.
Bad:
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
Good:
class Circle {
constructor(private readonly radius: number) {
}
perimeter() {
return 2 * Math.PI * this.radius;
}
surface() {
return Math.PI * this.radius * this.radius;
}
}
๋ถ๋ณ์ฑ์ ์ ํธํ์ธ์
ํ์
์คํฌ๋ฆฝํธ์ ํ์
์์คํ
์ interface
/class
์ ๊ฐ๋ณ ์์ฑ์ readonly๋ก ํ์ํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ธฐ๋ฅ์ ์ธ ๋ฐฉ์์ผ๋ก ์์
ํ ์ ์์ต๋๋ค. (์์ํ์ง ์์ ๋ณ์กฐ๋ ์ํํฉ๋๋ค.)
๋์ฑ ๋์ ๋ฐฉ๋ฒ์ผ๋ก๋ ํ์
T
๋ฅผ ๊ฐ๊ณ mapped types๋ฅผ ์ฌ์ฉํ์ฌ ๋ชจ๋ ๊ฐ ์์ฑ์ ์ฝ๊ธฐ ์ ์ฉ์ผ๋ก ํ์ํ๋ Readonly
๋ด์ฅ ํ์
์ด ์กด์ฌํฉ๋๋ค. (mapped types๋ฅผ ํ์ธํ์ธ์.)
Bad:
interface Config {
host: string;
port: string;
db: string;
}
Good:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
๋ฐฐ์ด์ ๊ฒฝ์ฐ, ReadonlyArray<T>
๋ฅผ ์ฌ์ฉํด์ ์ฝ๊ธฐ ์ ์ฉ์ ๋ฐฐ์ด์ ์์ฑํ ์ ์์ต๋๋ค.
์ด๊ฒ์ push()
์ fill()
๊ณผ ๊ฐ์ ๋ณ๊ฒฝ์ ๋ง์ต๋๋ค. ํ์ง๋ง ๊ฐ ์์ฒด๋ฅผ ๋ณ๊ฒฝํ์ง ์๋ concat()
, slice()
๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
Bad:
const array: number[] = [ 1, 3, 5 ];
array = []; // ์๋ฌ
array.push(100); // ๋ฐฐ์ด์ ๋ณ๊ฒฝ๋ ๊ฒ์
๋๋ค.
Good:
const array: ReadonlyArray<number> = [ 1, 3, 5 ];
array = []; // ์๋ฌ
array.push(100); // ์๋ฌ
TypeScript 3.4 is a bit easier์์ ์ฝ๊ธฐ ์ ์ฉ์ ๋งค๊ฐ๋ณ์๋ฅผ ์ ์ธํ ์ ์์ต๋๋ค.
function hoge(args: readonly string[]) {
args.push(1); // ์๋ฌ
}
๋ฆฌํฐ๋ด ๊ฐ์ ์ํด const assertions๋ฅผ ์ฌ์ฉํ์ธ์.
Bad:
const config = {
hello: 'world'
};
config.hello = 'world'; // ๊ฐ์ด ๋ฐ๋๋๋ค
const array = [ 1, 3, 5 ];
array[0] = 10; // ๊ฐ์ด ๋ฐ๋๋๋ค
// ์ธ ์ ์๋ ๊ฐ์ฒด๊ฐ ๋ฐํ๋ฉ๋๋ค
function readonlyData(value: number) {
return { value };
}
const result = readonlyData(100);
result.value = 200; // ๊ฐ์ด ๋ฐ๋๋๋ค
Good:
// ์ฝ๊ธฐ ์ ์ฉ ๊ฐ์ฒด
const config = {
hello: 'world'
} as const;
config.hello = 'world'; // ์๋ฌ
// ์ฝ๊ธฐ ์ ์ฉ ๋ฐฐ์ด
const array = [ 1, 3, 5 ] as const;
array[0] = 10; // ์๋ฌ
// ์ฝ๊ธฐ ์ ์ฉ ๊ฐ์ฒด๋ฅผ ๋ฐํํ ์ ์์ต๋๋ค
function readonlyData(value: number) {
return { value } as const;
}
const result = readonlyData(100);
result.value = 200; // ์๋ฌ
ํ์ vs ์ธํฐํ์ด์ค
ํฉ์งํฉ ๋๋ ๊ต์งํฉ์ด ํ์ํ ๋ ํ์
์ ์ฌ์ฉํ์ธ์. extends
๋๋ implements
๊ฐ ํ์ํ ๋ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ์ธ์. ์๊ฒฉํ ๊ท์น์ ์์ง๋ง ๋น์ ์๊ฒ ๋ง๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์ธ์.
ํ์
์คํฌ๋ฆฝํธ์์ type
๊ณผ interface
์ฌ์ด์ ๋ค๋ฅธ ์ ์ ๋ํด์ ๋ ์์ธํ ์ค๋ช
์ ์ํ๋ค๋ฉด ์ด ๋ต๋ณ์ ์ฐธ๊ณ ํ์ธ์.
Bad:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
//...
type Shape = {
// ...
}
Good:
type EmailConfig = {
// ...
}
type DbConfig = {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
// ...
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
ํด๋์ค
ํด๋์ค๋ ์์์ผ ํฉ๋๋ค
ํด๋์ค์ ํฌ๊ธฐ๋ ์ฑ ์์ ์ํด ์ธก์ ๋ฉ๋๋ค. ๋จ์ผ ์ฑ ์ ์์น์ ๋ฐ๋ฅด๋ฉด ํด๋์ค๋ ์์์ผ ํฉ๋๋ค.
Bad:
class Dashboard {
getLanguage(): string { /* ... */ }
setLanguage(language: string): void { /* ... */ }
showProgress(): void { /* ... */ }
hideProgress(): void { /* ... */ }
isDirty(): boolean { /* ... */ }
disable(): void { /* ... */ }
enable(): void { /* ... */ }
addSubscription(subscription: Subscription): void { /* ... */ }
removeSubscription(subscription: Subscription): void { /* ... */ }
addUser(user: User): void { /* ... */ }
removeUser(user: User): void { /* ... */ }
goToHomePage(): void { /* ... */ }
updateProfile(details: UserDetails): void { /* ... */ }
getVersion(): string { /* ... */ }
// ...
}
Good:
class Dashboard {
disable(): void { /* ... */ }
enable(): void { /* ... */ }
getVersion(): string { /* ... */ }
}
// ๋ค๋ฅธ ํด๋์ค์ ๋จ์ ๋ฉ์๋๋ฅผ ์ด๋์ํด์ผ๋ก์จ ์ฑ
์์ ๋ถ์ฐ์ํค์ธ์
// ...
๋์ ์์ง๋์ ๋ฎ์ ๊ฒฐํฉ๋
์์ง๋๋ ํด๋์ค ๋ฉค๋ฒ๊ฐ ์๋ก์๊ฒ ์ฐ๊ด๋์ด ์๋ ์ ๋๋ฅผ ์ ์ํฉ๋๋ค. ์ด์์ ์ผ๋ก, ํด๋์ค ์์ ๋ชจ๋ ํ๋๋ ๊ฐ ๋ฉ์๋์ ์ํด ์ฌ์ฉ๋์ด์ผ ํฉ๋๋ค. ๊ทธ ๋ ์ฐ๋ฆฌ๋ ํด๋์ค๊ฐ ์ต๋ํ์ผ๋ก ์์ง๋์ด์๋ค๋ผ๊ณ ๋งํฉ๋๋ค. ์ด๊ฒ์ ํญ์ ๊ฐ๋ฅํ์ง๋ ์๊ณ ๊ถ์ฅํ์ง ์์ต๋๋ค. ๊ทธ๋ฌ๋ ์์ง๋๋ฅผ ๋์ด๋ ๊ฒ์ ์ ํธํด์ผ ํฉ๋๋ค.
๊ฒฐํฉ๋๋ ๋ ํด๋์ค๊ฐ ์ผ๋ง๋ ์๋ก์๊ฒ ๊ด๋ จ๋์ด์๊ฑฐ๋ ์ข ์์ ์ธ ์ ๋๋ฅผ ๋ปํฉ๋๋ค. ํ๋์ ํด๋์ค์ ๋ณ๊ฒฝ์ด ๋ค๋ฅธ ํด๋์ค์๊ฒ ์ํฅ์ ์ฃผ์ง ์๋๋ค๋ฉด ๊ทธ ํด๋์ค๋ค์ ๊ฒฐํฉ๋๋ ๋ฎ๋ค๊ณ ๋งํฉ๋๋ค.
์ข์ ์ํํธ์จ์ด ์ค๊ณ๋ ๋์ ์์ง๋์ ๋ฎ์ ๊ฒฐํฉ๋๋ฅผ ๊ฐ์ง๋๋ค.
Bad:
class UserManager {
// Bad: ๊ฐ private ๋ณ์๋ ๋ฉ์๋์ ํ๋ ํน์ ๋ ๋ค๋ฅธ ๊ทธ๋ฃน์ ์ํด ์ฌ์ฉ๋ฉ๋๋ค.
// ํด๋์ค๊ฐ ๋จ์ผ ์ฑ
์ ์ด์์ ์ฑ
์์ ๊ฐ์ง๊ณ ์๋ค๋ ๋ช
๋ฐฑํ ์ฆ๊ฑฐ์
๋๋ค.
// ์ฌ์ฉ์์ ํธ๋์ญ์
์ ์ป๊ธฐ ์ํด ์๋น์ค๋ฅผ ์์ฑํ๊ธฐ๋ง ํ๋ฉด ๋๋ ๊ฒฝ์ฐ,
// ์ฌ์ ํ `emailSender` ์ธ์คํด์ค๋ฅผ ์ ๋ฌํด์ผ ํฉ๋๋ค.
constructor(
private readonly db: Database,
private readonly emailSender: EmailSender) {
}
async getUser(id: number): Promise<User> {
return await db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await db.transactions.find({ userId });
}
async sendGreeting(): Promise<void> {
await emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
Good:
class UserService {
constructor(private readonly db: Database) {
}
async getUser(id: number): Promise<User> {
return await this.db.users.findOne({ id });
}
async getTransactions(userId: number): Promise<Transaction[]> {
return await this.db.transactions.find({ userId });
}
}
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {
}
async sendGreeting(): Promise<void> {
await this.emailSender.send('Welcome!');
}
async sendNotification(text: string): Promise<void> {
await this.emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...
}
}
์์(inheritance)๋ณด๋ค ์กฐํฉ(composition)์ ์ฌ์ฉํ์ธ์
Gang of Four์ ๋์์ธ ํจํด์ ๋์์๋ฏ์ด ํ ์ ์๋ ๋๋ก ์์๋ณด๋ค ์กฐํฉ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ์์๊ณผ ์กฐํฉ์ ์ฌ์ฉํด์ผ ํ๋ ์ข์ ์ด์ ๋ค์ ๊ฐ๊ฐ ๋ง์ต๋๋ค. ์ด ๊ตํ์์ ์ค์ํ ์ ์ ๋น์ ์ ๋ง์์ด ๋ณธ๋ฅ์ ์ผ๋ก ์์์ ์ถ๊ตฌํ๋ค๋ฉด, ์กฐํฉ์ด ๋น์ ์ ๋ฌธ์ ๋ฅผ ๋ ์ข๊ฒ ํด๊ฒฐํ ์ ์์์ง ๊ณ ๋ฏผํด๋ณด๋ผ๋ ๊ฒ์ ๋๋ค. ์ด๋ค ๊ฒฝ์ฐ์๋ ๋ ์ข์ ์ ์์ต๋๋ค.
๋น์ ์ โ์ธ์ ์์์ ์ฌ์ฉํด์ผ ํ ๊น์?โ ๋ผ๊ณ ์๋ฌธ์ ์ ๊ฐ์ง ๊ฒ์ ๋๋ค. ๊ทธ๊ฒ์ ๋น๋ฉดํ ๋ฌธ์ ์ ๋ฌ๋ ค ์์ต๋๋ค. ์กฐํฉ๋ณด๋ค ์์์ด ๋ ์ข์ ๊ฒฝ์ฐ๊ฐ ์๋์ ์์ต๋๋ค:
-
โhas-aโ ๊ด๊ณ๊ฐ ์๋ โis-aโ ๊ด๊ณ์ผ ๋ (์ฌ๋->๋๋ฌผ vs ์ฌ์ฉ์->์ฌ์ฉ์ ์ ๋ณด)
-
๊ธฐ๋ฐ์ด ๋๋ ํด๋์ค๋ก๋ถํฐ ์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํ ์ ์์ ๋ (์ฌ๋์ ๋ชจ๋ ๋๋ฌผ์ฒ๋ผ ์์ง์ผ ์ ์์ต๋๋ค.)
-
๊ธฐ๋ฐ์ด ๋๋ ํด๋์ค๋ฅผ ๋ณ๊ฒฝํ์ฌ ํ์๋ ํด๋์ค๋ฅผ ์ ์ฒด์ ์ผ๋ก ๋ณ๊ฒฝํ๋ ค๋ ๊ฒฝ์ฐ (๋ชจ๋ ๋๋ฌผ์ ์์ง์ผ ๋ ์นผ๋ก๋ฆฌ๊ฐ ์๋น๋ฉ๋๋ค.)
Bad:
class Employee {
constructor(
private readonly name: string,
private readonly email: string) {
}
// ...
}
// `Employee`๊ฐ ์ธ๊ธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ธฐ ๋๋ฌธ์ ๋์ ์์
๋๋ค. `EmployeeTaxData`๋ `Employee`์ ํ์
์ด ์๋๋๋ค.
class EmployeeTaxData extends Employee {
constructor(
name: string,
email: string,
private readonly ssn: string,
private readonly salary: number) {
super(name, email);
}
// ...
}
Good:
class Employee {
private taxData: EmployeeTaxData;
constructor(
private readonly name: string,
private readonly email: string) {
}
setTaxData(ssn: string, salary: number): Employee {
this.taxData = new EmployeeTaxData(ssn, salary);
return this;
}
// ...
}
class EmployeeTaxData {
constructor(
public readonly ssn: string,
public readonly salary: number) {
}
// ...
}
๋ฉ์๋ ์ฒด์ด๋์ ์ฌ์ฉํ์ธ์
์ด ํจํด์ ๋งค์ฐ ์ ์ฉํ๊ณ ๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ๊ณตํต์ ์ผ๋ก ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ด๊ฒ์ ๋น์ ์ ์ฝ๋๋ฅผ ํํ๋ ฅ์ด ์๊ฒ ํด์ฃผ๊ณ ๋ ์ฅํฉํ๊ฒ ํด์ค๋๋ค. ์ด๋ฌํ ์ด์ ๋ก ๋ฉ์๋ ์ฒด์ด๋์ ์ฌ์ฉํด์ ๋น์ ์ ์ฝ๋๊ฐ ์ผ๋ง๋ ๋ช ๋ฃํด์ง๋์ง ์ดํด๋ณด์๊ธธ ๋ฐ๋๋๋ค.
Bad:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): void {
this.collection = collection;
}
page(number: number, itemsPerPage: number = 100): void {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
}
orderBy(...fields: string[]): void {
this.orderByFields = fields;
}
build(): Query {
// ...
}
}
// ...
const queryBuilder = new QueryBuilder();
queryBuilder.from('users');
queryBuilder.page(1, 100);
queryBuilder.orderBy('firstName', 'lastName');
const query = queryBuilder.build();
Good:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string): this {
this.collection = collection;
return this;
}
page(number: number, itemsPerPage: number = 100): this {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
return this;
}
orderBy(...fields: string[]): this {
this.orderByFields = fields;
return this;
}
build(): Query {
// ...
}
}
// ...
const query = new QueryBuilder()
.from('users')
.page(1, 100)
.orderBy('firstName', 'lastName')
.build();
SOLID
๋จ์ผ ์ฑ ์ ์์น (SRP)
ํด๋ฆฐ ์ฝ๋์์ ๋งํ๋ฏ์ด, โํด๋์ค๋ฅผ ๋ณ๊ฒฝํ ๋๋ ๋จ ํ ๊ฐ์ง ์ด์ ๋ง ์กด์ฌํด์ผ ํฉ๋๋คโ. ์ฌํ๊ฐ ๋ ๊ฐ๋ฐฉ ํ๋์ ๋ง์ ๊ฒ์ ์ฑ๊ธฐ๋ ๊ฒ๊ณผ ๊ฐ์ด, ํด๋์ค๋ฅผ ๋ง์ ๊ธฐ๋ฅ์ผ๋ก ๊ฝ ์ฑ์ฐ๊ณ ์ถ์ ์ ํน์ด ์์ต๋๋ค. ์ด๋ฌํ ๋ฌธ์ ๋ ๋น์ ์ ํด๋์ค๊ฐ ๊ฐ๋ ์ ์ผ๋ก ์์ง๋ ฅ์ด ์์ง ์์ผ๋ฉฐ ๋ณ๊ฒฝ๋ ๋ง์ ์ด์ ๊ฐ ์กด์ฌํ๋ค๋ ๊ฒ์ ๋งํฉ๋๋ค. ํด๋์ค๋ฅผ ๋ณ๊ฒฝํ๋ ๋ง์ ์๊ฐ์ ์ค์ด๋ ๊ฒ์ ์ค์ํฉ๋๋ค. ์๋ํ๋ฉด ๋๋ฌด ๋ง์ ๊ธฐ๋ฅ์ด ํ ํด๋์ค์ ์๊ณ ๊ทธ ์์์ ํ๋์ ๊ธฐ๋ฅ์ ์์ ํ๋ค๋ฉด, ๋ค๋ฅธ ์ข ์๋ ๋ชจ๋์ ์ด๋ป๊ฒ ์ํฅ์ ์ค์ง ์ดํดํ๋ ๊ฒ์ด ์ด๋ ต๊ธฐ ๋๋ฌธ์ ๋๋ค.
Bad:
class UserSettings {
constructor(private readonly user: User) {
}
changeSettings(settings: UserSettings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good:
class UserAuth {
constructor(private readonly user: User) {
}
verifyCredentials() {
// ...
}
}
class UserSettings {
private readonly auth: UserAuth;
constructor(private readonly user: User) {
this.auth = new UserAuth(user);
}
changeSettings(settings: UserSettings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
๊ฐ๋ฐฉ ํ์ ์์น (OCP)
Bertrand Meyer๊ฐ ๋งํ๋ฏ์ด, โ์ํํธ์จ์ด ์ํฐํฐ(ํด๋์ค, ๋ชจ๋, ํจ์ ๋ฑ)๋ ์์์ ๊ฐ๋ฐฉ๋์ด ์์ต๋๋ค. ํ์ง๋ง ์์ ์๋ ํ์๋์ด ์์ต๋๋ค.โ ์ด๊ฒ์ด ๋ฌด์์ ์๋ฏธํ ๊น์? ์ด ์์น์ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ธฐ์กด์ ์ฝ๋๋ฅผ ๋ณ๊ฒฝํ์ง ์๊ณ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์๋๋ก ํ๋ ๊ฒ์ ๋ช ์ํฉ๋๋ค.
Bad:
class AjaxAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
if (this.adapter instanceof AjaxAdapter) {
const response = await makeAjaxCall<T>(url);
// response ๊ฐ์ ๋ณ๊ฒฝํ๊ณ ๋ฐํํฉ๋๋ค.
} else if (this.adapter instanceof NodeAdapter) {
const response = await makeHttpCall<T>(url);
// response ๊ฐ์ ๋ณ๊ฒฝํ๊ณ ๋ฐํํฉ๋๋ค.
}
}
}
function makeAjaxCall<T>(url: string): Promise<T> {
// ์๋ฒ์ ์์ฒญํ๊ณ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํฉ๋๋ค.
}
function makeHttpCall<T>(url: string): Promise<T> {
// ์๋ฒ์ ์์ฒญํ๊ณ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํฉ๋๋ค.
}
Good:
abstract class Adapter {
abstract async request<T>(url: string): Promise<T>;
// ํ์ ํด๋์ค์ ๊ณต์ ํ๋ ์ฝ๋ ...
}
class AjaxAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// ์๋ฒ์ ์์ฒญํ๊ณ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํฉ๋๋ค.
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super();
}
async request<T>(url: string): Promise<T>{
// ์๋ฒ์ ์์ฒญํ๊ณ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํฉ๋๋ค.
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {
}
async fetch<T>(url: string): Promise<T> {
const response = await this.adapter.request<T>(url);
// response ๊ฐ์ ๋ณ๊ฒฝํ๊ณ ๋ฐํํฉ๋๋ค.
}
}
๋ฆฌ์ค์ฝํ ์นํ ์์น (LSP)
๋งค์ฐ ๋จ์ํ ๊ฐ๋ ์ ๋ปํ๋ ์ด๋ ค์๋ณด์ด๋ ์ฉ์ด์ ๋๋ค. โ๋ง์ฝ S๊ฐ T์ ํ์ ํ์ ์ด๋ผ๋ฉด, T ํ์ ์ ๊ฐ์ฒด๋ S ํ์ ์ ๊ฐ์ฒด๋ก ๋์ฒด๋ ์ ์์ต๋๋ค. (์: S ํ์ ๊ฐ์ฒด๋ T ํ์ ๊ฐ์ฒด๋ก ์นํ๋ ์๋ ์์ต๋๋ค.) ์ด๋ ํ๋ก๊ทธ๋จ์ด ๊ฐ์ถ์ด์ผํ ์์ฑ(์ ํ์ฑ, ์ํ๋๋ ์์ ๋ฑ)์ ๋ณ๊ฒฝํ์ง ์์๋ ๋์ฒด๋ ์ ์์ต๋๋ค.โ ๋์ฑ ์ด๋ ค์๋ณด์ด๋ ์ ์์ ๋๋ค.
์ด๋ฅผ ์ํ ์ต๊ณ ์ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๋ง์ฝ ๋ถ๋ชจ ํด๋์ค์ ์์ ํด๋์ค๊ฐ ์๋ค๋ฉด, ๋ถ๋ชจ ํด๋์ค์ ์์ ํด๋์ค๋ ์๋ชป๋ ๊ฒฐ๊ณผ ์์ด ์๋ก ๊ตํํ์ฌ ์ฌ์ฉ๋ ์ ์์ต๋๋ค. ์ฌ์ ํ ํผ๋์ค๋ฌ์ธ ์ ์์ต๋๋ค. ๊ณ ์ ์ ์ธ ์ ์ฌ๊ฐํ-์ง์ฌ๊ฐํ ์์ ๋ฅผ ์ดํด๋ณด์ธ์. ์ํ์ ์ผ๋ก, ์ ์ฌ๊ฐํ์ ์ง์ฌ๊ฐํ์ ๋๋ค. ๊ทธ๋ฌ๋ ์์์ ํตํด โis-aโ ๊ด๊ณ๋ก ์ค๊ณํ๋ค๋ฉด, ๋น์ ์ ๊ณค๊ฒฝ์ ๋น ์ง ์ ์์ต๋๋ค.
Bad:
class Rectangle {
constructor(
protected width: number = 0,
protected height: number = 0) {
}
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
setWidth(width: number): this {
this.width = width;
return this;
}
setHeight(height: number): this {
this.height = height;
return this;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number): this {
this.width = width;
this.height = width;
return this;
}
setHeight(height: number): this {
this.width = height;
this.height = height;
return this;
}
}
function renderLargeRectangles(rectangles: Rectangle[]) {
rectangles.forEach((rectangle) => {
const area = rectangle
.setWidth(4)
.setHeight(5)
.getArea(); // BAD: `Square` ํด๋์ค์์๋ 25๋ฅผ ๋ฐํํฉ๋๋ค. 20์ด ๋ฐํ๋์ด์ผ ํฉ๋๋ค.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
abstract class Shape {
setColor(color: string): this {
// ...
}
render(area: number) {
// ...
}
abstract getArea(): number;
}
class Rectangle extends Shape {
constructor(
private readonly width = 0,
private readonly height = 0) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(private readonly length: number) {
super();
}
getArea(): number {
return this.length * this.length;
}
}
function renderLargeShapes(shapes: Shape[]) {
shapes.forEach((shape) => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
์ธํฐํ์ด์ค ๋ถ๋ฆฌ ์์น (ISP)
์ธํฐํ์ด์ค ๋ถ๋ฆฌ ์์น์ โํด๋ผ์ด์ธํธ๋ ์ฌ์ฉํ์ง ์๋ ์ธํฐํ์ด์ค์ ์์กดํ์ง ์๋๋คโ ๋ผ๋ ๊ฒ์ ๋๋ค. ์ด ์์น์ ๋จ์ผ ์ฑ ์ ์์น๊ณผ ๋ง์ ๊ด๋ จ์ด ์์ต๋๋ค. ์ด ๋ง์ ๋ป์ ํด๋ผ์ด์ธํธ๊ฐ ๋ ธ์ถ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ๋์ ์ ์ ์ฒด ํ์ด๋ฅผ ์ป์ง ์๋ ๋ฐฉ์์ผ๋ก ์ถ์ํ๋ฅผ ์ค๊ณํด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค. ๊ทธ๊ฒ์ ๋ํ ํด๋ผ์ด์ธํธ์๊ฒ ํด๋ผ์ด์ธํธ๊ฐ ์ค์ ๋ก ํ์ํ์ง ์์ ๋ฉ์๋์ ๊ตฌํ์ ๊ฐ์ํ๋ ๊ฒ๋ ํฌํจํฉ๋๋ค.
Bad:
interface SmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements SmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements SmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.');
}
}
Good:
interface Printer {
print();
}
interface Fax {
fax();
}
interface Scanner {
scan();
}
class AllInOnePrinter implements Printer, Fax, Scanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...
}
}
class EconomicPrinter implements Printer {
print() {
// ...
}
}
์์กด์ฑ ์ญ์ ์์น (DIP)
์ด ์์น์ ๋ ๊ฐ์ง ํ์์ ์ธ ์ฌํญ์ ๋ช ์ํฉ๋๋ค:
-
์์ ๋ ๋ฒจ์ ๋ชจ๋์ ํ์ ๋ ๋ฒจ์ ๋ชจ๋์ ์์กดํ์ง ์์์ผ ํฉ๋๋ค. ๋ ๋ชจ๋์ ๋ชจ๋ ์ถ์ํ์ ์์กดํด์ผํฉ๋๋ค.
-
์ถ์ํ๋ ์ธ๋ถ์ฌํญ์ ์์กดํ์ง ์์์ผ ํฉ๋๋ค. ์ธ๋ถ์ฌํญ์ ์ถ์ํ์ ์์กดํด์ผ ํฉ๋๋ค.
์ฒ์์ ๋ฐ๋ก ์ดํดํ๊ธฐ๋ ์ด๋ ค์ธ ์ ์์ต๋๋ค. Angular๋ฅผ ์ฌ์ฉํด๋ดค๋ค๋ฉด, ์์กด์ฑ ์ฃผ์ (DI)์ ํํ ์์์ ์ด ์์น์ ๊ตฌํ์ ํ์ธํด๋ดค์ ๊ฒ์ ๋๋ค. ๋์ผํ ๊ฐ๋ ์ ์๋์ง๋ง, DIP๋ ์์ ๋ ๋ฒจ์ ๋ชจ๋์ด ํ์ ๋ ๋ฒจ์ ๋ชจ๋์ ์ธ๋ถ์ฌํญ์ ์ ๊ทผํ๊ณ ์ค์ ํ์ง ๋ชปํ๋๋ก ์งํต๋๋ค. DI๋ฅผ ํตํด์๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์ฑ์ทจํ ์ ์์ต๋๋ค. ์ด๊ฒ์ ํฐ ์ฅ์ ์ ๋ชจ๋ ์ฌ์ด์ ๊ฒฐํฉ๋๋ฅผ ์ค์ผ ์ ์๋ค๋ ๊ฒ์ ๋๋ค. ๊ฒฐํฉ๋๋ ์ฝ๋๋ฅผ ๋ฆฌํฉํ ๋งํ๊ธฐ ์ด๋ ต๊ฒ ํ๊ธฐ ๋๋ฌธ์ ๋งค์ฐ ๋์ ๊ฐ๋ฐ ํจํด์ ๋๋ค.
DIP๋ ์ฃผ๋ก IoC ์ปจํ ์ด๋๋ฅผ ์ฌ์ฉํจ์ผ๋ก์จ ๋ฌ์ฑ๋ฉ๋๋ค. ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ํ ๊ฐ๋ ฅํ IoC ์ปจํ ์ด๋์ ์์ ๋ InversifyJs์ ๋๋ค.
Bad:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
class XmlFormatter {
parse<T>(content: string): T {
// XML ๋ฌธ์์ด์ T ๊ฐ์ฒด๋ก ๋ณํ
}
}
class ReportReader {
// BAD: ํน์ ์์ฒญ์ ๊ตฌํ์ ์์กดํ๋ ๊ฒ์ ๋ง๋ค์์ต๋๋ค.
// `parse` ๋ฉ์๋์ ์์กดํ๋ `ReportReader`๋ฅผ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
private readonly formatter = new XmlFormatter();
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader();
await report = await reader.read('report.xml');
Good:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
interface Formatter {
parse<T>(content: string): T;
}
class XmlFormatter implements Formatter {
parse<T>(content: string): T {
// XML ๋ฌธ์์ด์ T ๊ฐ์ฒด๋ก ๋ณํ
}
}
class JsonFormatter implements Formatter {
parse<T>(content: string): T {
// JSON ๋ฌธ์์ด์ T ๊ฐ์ฒด๋ก ๋ณํ
}
}
class ReportReader {
constructor(private readonly formatter: Formatter) {
}
async read(path: string): Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text);
}
}
// ...
const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');
// ๋๋ json ๋ณด๊ณ ์๊ฐ ํ์ํ ๊ฒฝ์ฐ
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
ํ ์คํธ
ํ ์คํธ๋ ๋ฐฐํฌ๋ณด๋ค ์ค์ํฉ๋๋ค. ํ ์คํธ๊ฐ ์๊ฑฐ๋ ๋ถ์กฑํ ๊ฒฝ์ฐ, ์ฝ๋๋ฅผ ๋ฐฐํฌํ ๋๋ง๋ค ๋น์ ์ ์ด๋ค ๊ฒ์ด ์๋ํ์ง ์์์ง ํ์คํ์ง ์์ ๊ฒ์ ๋๋ค. ์ ์ ํ ์์ ํ ์คํธ๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒ์ ๋น์ ์ ํ์๊ฒ ๋ฌ๋ ค์์ง๋ง, (๋ชจ๋ ๋ฌธ์ฅ๊ณผ ๋ธ๋์น์์) 100%์ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๊ฐ์ง๋ค๋ฉด ๋งค์ฐ ๋์ ์์ ๊ฐ๊ณผ ๋ง์์ ํํ๋ฅผ ์ป์ ๊ฒ์ ๋๋ค. ์ด๋ ํ๋ฅญํ ํ ์คํธ ํ๋ ์์ํฌ๋ฟ๋ง ์๋๋ผ, ์ข์ ์ปค๋ฒ๋ฆฌ์ง ๋๊ตฌ๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
ํ ์คํธ๋ฅผ ์์ฑํ์ง ์์ ์ด์ ๋ ์์ต๋๋ค. ํ์ ์คํฌ๋ฆฝํธ์ ํ์ ์ ์ง์ํ๋ ๋ง์ ์์ ์ข์ ์๋ฐ์คํฌ๋ฆฝํธ ํ ์คํธ ํ๋ ์์ํฌ๊ฐ ์์ผ๋ฏ๋ก ๋น์ ์ ํ์ด ์ ํธํ๋ ๊ฒ์ ์ฐพ์ ์ฌ์ฉํ์ธ์. ๋น์ ์ ํ์ ์ ํฉํ ํ ์คํธ ํ๋ ์์ํฌ๋ฅผ ์ฐพ์๋ค๋ฉด, ๋น์ ์ด ๋ง๋๋ ๋ชจ๋ ์๋ก์ด ๊ธฐ๋ฅ/๋ชจ๋์ ์ํ ํ ์คํธ๋ฅผ ํญ์ ์์ฑํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ์ธ์. ํ ์คํธ ๊ธฐ๋ฐ ๊ฐ๋ฐ(TDD)์ด ๋น์ ์ด ์ ํธํ๋ ๋ฐฉ๋ฒ์ด๋ผ๋ฉด, ๋งค์ฐ ์ข์ต๋๋ค. ํ์ง๋ง ์ค์ํ ๊ฑด ์ด๋ค ๊ธฐ๋ฅ์ ๋ง๋ค๊ฑฐ๋ ๊ธฐ์กด์ ๊ฒ์ ๋ฆฌํฉํ ๋งํ๊ธฐ ์ ์ ๋ชฉํํ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋ฌ์ฑํ๋ ๊ฒ์ ๋๋ค.
TDD์ ์ธ ๊ฐ์ง ๋ฒ์น
-
์คํจํ๋ ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์ ์๋ ์ค์ ์ฝ๋๋ฅผ ์์ฑํ์ง ๋ง์ธ์.
-
์ปดํ์ผ์ ์คํจํ์ง ์์ผ๋ฉด์ ์คํ์ด ์คํจํ๋ ์ ๋๋ก๋ง ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ์ธ์.
-
์คํจํ๋ ๋จ์ ํ ์คํธ๋ฅผ ํต๊ณผํ ์ ๋๋ก๋ง ์ค์ ์ฝ๋๋ฅผ ์์ฑํ์ธ์.
F.I.R.S.T ๊ท์น
๋ช ๋ฃํ ํ ์คํธ๋ ๋ค์ ๊ท์น์ ๋ฐ๋ผ์ผ ํฉ๋๋ค:
-
Fast ํ ์คํธ๋ ๋น๋ฒํ๊ฒ ์คํ๋๋ฏ๋ก ๋นจ๋ผ์ผ ํฉ๋๋ค.
-
Independent ํ ์คํธ๋ ์๋ก ์ข ์์ ์ด์ง ์์ต๋๋ค. ๋ ๋ฆฝ์ ์ผ๋ก ์คํํ๋ ์ง ์์ ์๊ด์์ด ๋ชจ๋ ์คํํ๋ ์ง ๋์ผํ ๊ฒฐ๊ณผ๊ฐ ๋์์ผ ํฉ๋๋ค.
-
Repeatable ํ ์คํธ๋ ์ด๋ค ํ๊ฒฝ์์๋ ๋ฐ๋ณต๋ ์ ์์ต๋๋ค. ํ ์คํธ๊ฐ ์คํจํ๋๋ฐ์ ์ด์ ๊ฐ ์์ด์ผ ํฉ๋๋ค.
-
Self-Validating ํ ์คํธ๋ ํต๊ณผ ํน์ ์คํจ๋ก ๋ตํด์ผ ํฉ๋๋ค. ํ ์คํธ๊ฐ ํต๊ณผ๋์๋ค๋ฉด ๋ก๊ทธ ํ์ผ์ ๋ณด๋ฉฐ ๋น๊ตํ ํ์๋ ์์ต๋๋ค.
-
Timely ๋จ์ ํ ์คํธ๋ ์ค์ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ์์ฑํด์ผ ํฉ๋๋ค. ์ค์ ์ฝ๋๋ฅผ ์์ฑํ ํ์ ํ ์คํธ๋ฅผ ์์ฑํ๋ค๋ฉด, ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ด ๋๋ฌด ๊ณ ๋จํ๊ฒ ๋๊ปด์ง ๊ฒ์ ๋๋ค.
ํ ์คํธ ํ๋์ ํ๋์ ๊ฐ๋ ์ ์์ฑํ์ธ์
๋ํ, ํ ์คํธ๋ ๋จ์ผ ์ฑ ์ ์์น์ ๋ฐ๋ผ์ผ ํฉ๋๋ค. ๋จ์ ํ ์คํธ ํ๋๋น ํ๋์ assert ๊ตฌ๋ฌธ์ ์์ฑํ์ธ์.
Bad:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;
date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
Good:
import { assert } from 'chai';
describe('AwesomeDate', () => {
it('handles 30-day months', () => {
const date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
});
it('handles leap year', () => {
const date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
});
it('handles non-leap year', () => {
const date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});
ํ ์คํธ์ ์ด๋ฆ์ ํ ์คํธ์ ์๋๊ฐ ๋๋ฌ๋์ผ ํฉ๋๋ค
ํ ์คํธ๊ฐ ์คํจํ ๋, ํ ์คํธ์ ์ด๋ฆ์ ์ด๋ค ๊ฒ์ด ์๋ชป๋์๋์ง ๋ณผ ์ ์๋ ์ฒซ ๋ฒ์งธ ํ์์ ๋๋ค.
Bad:
describe('Calendar', () => {
it('2/29/2020', () => {
// ...
});
it('throws', () => {
// ...
});
});
Good:
describe('Calendar', () => {
it('should handle leap year', () => {
// ...
});
it('should throw when format is invalid', () => {
// ...
});
});
๋์์ฑ
ํ๋ก๋ฏธ์ค vs ์ฝ๋ฐฑ
์ฝ๋ฐฑ์ ๋ช
๋ฃํ์ง ์๊ณ , ์ง๋์น ์์ ์ค์ฒฉ๋ ์ฝ๋ฐฑ ์ง์ฅ์ ์ ๋ฐํ ์ ์์ต๋๋ค.
์ฝ๋ฐฑ ๋ฐฉ์์ ์ฌ์ฉํ๊ณ ์๋ ๊ธฐ์กด์ ํจ์๋ฅผ ํ๋ก๋ฏธ์ค๋ฅผ ๋ฐํํ๋ ํจ์๋ก ๋ณํ์์ผ์ฃผ๋ ์ ํธ๋ฆฌํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ต๋๋ค.
(Node.js๋ฅผ ์ฌ์ฉํ๋ค๋ฉด util.promisify
๋ฅผ ํ์ธํด์ฃผ์ธ์. ์ผ๋ฐ์ ์ธ ๋ชฉ์ ์ด๋ผ๋ฉด pify, es6-promisify๋ฅผ ํ์ธํด์ฃผ์ธ์.)
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
get(url, (error, response) => {
if (error) {
callback(error);
} else {
writeFile(saveTo, response.body, (error) => {
if (error) {
callback(error);
} else {
callback(null, response.body);
}
});
}
});
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
if (error) {
console.error(error);
} else {
console.log(content);
}
});
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url)
.then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
ํ๋ก๋ฏธ์ค๋ ์ฝ๋๋ฅผ ๋์ฑ ๊ฐ๊ฒฐํ๊ฒ ํด์ฃผ๋ ๋ช๋ช์ ํฌํผ ๋ฉ์๋๋ฅผ ์ง์ํฉ๋๋ค:
ํจํด | ์ค๋ช |
---|---|
Promise.resolve(value) |
ํด๊ฒฐ(resolve)๋ ํ๋ก๋ฏธ์ค๋ก ๊ฐ์ ๋ณํํจ. |
Promise.reject(error) |
๊ฑฐ๋ถ(reject)๋ ํ๋ก๋ฏธ์ค๋ก ์๋ฌ๋ฅผ ๋ณํํจ. |
Promise.all(promises) |
์ ๋ฌ๋ ๋ชจ๋ ํ๋ก๋ฏธ์ค๊ฐ ์ดํํ ๊ฐ์ ๋ฐฐ์ด์ ์ดํํ๋ ์ ํ๋ก๋ฏธ์ค ๊ฐ์ฒด๋ฅผ ๋ฐํํ๊ฑฐ๋ ๊ฑฐ๋ถ๋ ์ฒซ ๋ฒ์งธ ํ๋ก๋ฏธ์ค์ ์ด์ ๋ก ๊ฑฐ๋ถํจ. |
Promise.race(promises) |
์ ๋ฌ๋ ํ๋ก๋ฏธ์ค์ ๋ฐฐ์ด์์ ๊ฐ์ฅ ๋จผ์ ์๋ฃ๋ ๊ฒฐ๊ณผ/์๋ฌ๋ก ์ดํ/๊ฑฐ๋ถ๋ ์ ํ๋ก๋ฏธ์ค ๊ฐ์ฒด๋ฅผ ๋ฐํํจ. |
Promise.all
๋ ๋ณ๋ ฌ์ ์ผ๋ก ์์
์ ์ํํ ํ์๊ฐ ์์ ๋ ์ ์ฉํฉ๋๋ค. Promise.race
๋ ํ๋ก๋ฏธ์ค๋ฅผ ์ํ ํ์์์๊ณผ ๊ฐ์ ๊ฒ์ ๊ตฌํํ๋ ๊ฒ์ ์ฝ๊ฒ ํ ์ ์๋๋ก ๋์์ค๋๋ค.
ํ๋ก๋ฏธ์ค๋ณด๋ค async
/await
๊ฐ ๋ ๋ช
๋ฃํฉ๋๋ค
async
/await
๊ตฌ๋ฌธ์ ์ฌ์ฉํ๋ฉด ์ฐ๊ฒฐ๋ ํ๋ก๋ฏธ์ค ๊ตฌ๋ฌธ๋ณด๋ค ํจ์ฌ ๋ ๋ช
๋ฃํ๊ณ ์ดํดํ๊ธฐ ์ฌ์ด ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. async
ํค์๋๊ฐ ์์ ๋ถ์ฌ์ง ํจ์๋ await
ํค์๋์์ ์ฝ๋์ ์คํ์ ๋ฉ์ถ๋ค๋ ๊ฒ์ ์๋ฐ์คํฌ๋ฆฝํธ ๋ฐํ์์๊ฒ ์๋ ค์ค๋๋ค.
Bad:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = util.promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
return get(url).then(response => write(saveTo, response));
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
.then(content => console.log(content))
.catch(error => console.error(error));
Good:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
async function downloadPage(url: string, saveTo: string): Promise<string> {
const response = await get(url);
await write(saveTo, response);
return response;
}
// somewhere in an async function
try {
const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
console.log(content);
} catch (error) {
console.error(error);
}
์๋ฌ ์ฒ๋ฆฌ
์๋ฌ๋ฅผ ๋์ง๋ ๊ฒ์ ์ข์ ๊ฒ์ ๋๋ค! ์๋ฌ๋ฅผ ๋์ง๋ค๋ ๊ฒ์ ๋ฐํ์์ด ๋น์ ์ ํ๋ก๊ทธ๋จ์์ ๋ญ๊ฐ ์๋ชป๋์์ ๋ ์๋ณํ๊ณ ํ์ฌ ์คํ์์ ํจ์ ์คํ์ ๋ฉ์ถ๊ณ , (๋ ธ๋์์) ํ๋ก์ธ์ค๋ฅผ ์ข ๋ฃํ๋ฉฐ, ์คํ ํธ๋ ์ด์ค๋ฅผ ์ฝ์์ ๋ณด์ฌ์ค์ผ๋ก์จ ๋น์ ์๊ฒ ํด๋น ์๋ฌ๋ฅผ ์๋ ค์ฃผ๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
throw
๋๋ reject
๊ตฌ๋ฌธ์์ ํญ์ Error
ํ์
์ ์ฌ์ฉํ์ธ์
ํ์
์คํฌ๋ฆฝํธ๋ฟ๋ง ์๋๋ผ ์๋ฐ์คํฌ๋ฆฝํธ๋ ์ด๋ค ๊ฐ์ฒด๋ ์ง ์๋ฌ๋ฅผ throw
ํ๋ ๊ฒ์ ํ์ฉํฉ๋๋ค. ๋ํ, ํ๋ก๋ฏธ์ค๋ ์ด๋ค ๊ฐ์ฒด๋ผ๋ ๊ฑฐ๋ถ๋ ์ ์์ต๋๋ค.
Error
ํ์
์๋ throw
๊ตฌ๋ฌธ์ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ฐ๋์งํฉ๋๋ค. ๋น์ ์ ์๋ฌ๊ฐ ์์ ์ฝ๋์ catch
๊ตฌ๋ฌธ์์ ์กํ ์ ์๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ฌธ์์ด ๋ฉ์์ง๊ฐ ์กํ๋ ๊ฒ์ ๋งค์ฐ ํผ๋์ค๋ฌ์ฐ๋ฉฐ ์ด๋ ๋๋ฒ๊น
์ ๋ ๊ณ ํต์ค๋ฝ๊ฒ ๋ง๋ญ๋๋ค.
์ด์ ๊ฐ์ ์ด์ ๋ก ๋น์ ์ Error
ํ์
์ผ๋ก ํ๋ก๋ฏธ์ค๋ฅผ ๊ฑฐ๋ถํด์ผํฉ๋๋ค.
Bad:
function calculateTotal(items: Item[]): number {
throw 'Not implemented.';
}
function get(): Promise<Item[]> {
return Promise.reject('Not implemented.');
}
Good:
function calculateTotal(items: Item[]): number {
throw new Error('Not implemented.');
}
function get(): Promise<Item[]> {
return Promise.reject(new Error('Not implemented.'));
}
// ๋๋ ์๋์ ๋์ผํฉ๋๋ค:
async function get(): Promise<Item[]> {
throw new Error('Not implemented.');
}
Error
ํ์
์ ์ฌ์ฉํ๋ ์ฅ์ ์ try/catch/finally
๊ตฌ๋ฌธ์ ์ํด ์ง์๋๊ณ ์์์ ์ผ๋ก ๋ชจ๋ ์๋ฌ๊ฐ ๋๋ฒ๊น
์ ๋งค์ฐ ๊ฐ๋ ฅํ stack
์์ฑ์ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์
๋๋ค.
๋ ํ๋์ ๋์์ ์์ต๋๋ค. throw
๊ตฌ๋ฌธ์ ์ฌ์ฉํ์ง ์๋ ๋์ , ํญ์ ์ฌ์ฉ์ ์ ์ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ ๊ฒ์
๋๋ค.
ํ์
์คํฌ๋ฆฝํธ๋ ์ด๊ฒ์ ํจ์ฌ ๋ ์ฝ๊ฒ ๋ง๋ญ๋๋ค.
์๋์ ์์ ๋ฅผ ํ์ธํ์ธ์:
type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;
function calculateTotal(items: Item[]): Failable<number, 'empty'> {
if (items.length === 0) {
return { isError: true, error: 'empty' };
}
// ...
return { isError: false, value: 42 };
}
์ด ์์ด๋์ด์ ์์ธํ ์ค๋ช ์ ์๋ฌธ์ ์ฐธ๊ณ ํ์ธ์.
catch
์ ์์ ์๋ฌ ์ฒ๋ฆฌ ๋ถ๋ถ์ ๋น์๋์ง ๋ง์ธ์
catch
์ ์์ ๋จ์ง ์๋ฌ๋ฅผ ๋ฐ๋ ๊ฒ๋ง์ผ๋ก๋ ํด๋น ์๋ฌ์ ๋์ํ ์ ์์ต๋๋ค. ๋ํ, ์ฝ์์ ์๋ฌ๋ฅผ ๊ธฐ๋กํ๋ ๊ฒ(console.log
)์ ์ฝ์์ ์ถ๋ ฅ๋ ๋ง์ ๊ฒ๋ค ์ฌ์ด์์ ๋ฐ๊ฒฌ๋์ง ๋ชปํ ์ ์๊ธฐ ๋๋ฌธ์ ๊ทธ๋ค์ง ์ข์ ์ ํ์ ์๋๋๋ค. ๋น์ ์ด ์ด๋ค ์ฝ๋๋ฅผ try/catch
๋ก ๊ฐ์๋ค๋ฉด, ๊ทธ ์ฝ๋์์ ์๋ฌ๊ฐ ์ผ์ด๋ ์ ์์ผ๋ฉฐ, ์ฆ ์๋ฌ๊ฐ ๋ฐ์ํ์ ๋์ ๋ํ ๊ณํ์ด๋ ์ฅ์น๊ฐ ์์ด์ผ ํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
Bad:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
// ์๋ ์์ ๋ ํจ์ฌ ๋์ฉ๋๋ค.
try {
functionThatMightThrow();
} catch (error) {
// ์๋ฌ๋ฅผ ๋ฌด์
}
Good:
import { logger } from './logging'
try {
functionThatMightThrow();
} catch (error) {
logger.log(error);
}
์์ฒญ์ด ๊ฑฐ๋ถ๋ ํ๋ก๋ฏธ์ค ๊ฐ์ฒด๋ฅผ ๋ฌด์ํ์ง ๋ง์ธ์
์์ ๊ฐ์ด try/catch
์ ์์ ๋ฐ์ ์๋ฌ ์ฒ๋ฆฌ ๋ถ๋ถ์ ๋น์๋๋ฉด ์๋ฉ๋๋ค.
Bad:
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
console.log(error);
});
Good:
import { logger } from './logging'
getUser()
.then((user: User) => {
return sendEmail(user.email, 'Welcome!');
})
.catch((error) => {
logger.log(error);
});
// ๋๋ async/await ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค:
try {
const user = await getUser();
await sendEmail(user.email, 'Welcome!');
} catch (error) {
logger.log(error);
}
์์
์์์ ์ฃผ๊ด์ ์ ๋๋ค. ์ฌ๊ธฐ์ ์๋ ๋ง์ ๊ท์น๋ค๊ณผ ๊ฐ์ด ๋น์ ์ด ๋ฐ๋ฅด๊ธฐ ์ด๋ ค์ด ๊ท์น์ ์์ต๋๋ค. ์ค์ํ ์ ์ ์์์ ๋ํด์ ๋ ผ์ํ์ง ์๋ ๊ฒ์ ๋๋ค. ์์์ ์๋ํํ๊ธฐ ์ํ ๋๊ตฌ๋ค์ด ๋งค์ฐ ๋ง์ต๋๋ค. ๊ทธ ์ค ํ๋๋ฅผ ์ฌ์ฉํ์ธ์! ์์์ ๋ํด ๋ ผ์ํ๋ ๊ฒ์ ์์ง๋์ด์๊ฒ ์๊ฐ๊ณผ ๋ ๋ญ๋น์ผ ๋ฟ์ ๋๋ค. ๋ฐ๋ผ์ผํ๋ ์ผ๋ฐ์ ์ธ ๊ท์น์ ์ผ๊ด์ ์ธ ์์ ๊ท์น์ ์ง์ผ์ผํ๋ ๊ฒ์ ๋๋ค.
TSLint๋ผ๊ณ ๋ถ๋ฆฌ๋ ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ํ ๊ฐ๋ ฅํ ๋๊ตฌ๊ฐ ์์ต๋๋ค. ์ด๊ฒ์ ์ฝ๋์ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๊ทน์ ์ผ๋ก ๊ฐ์ ์ํค๋๋ก ๋์์ฃผ๋ ์ ์ ๋ถ์ ๋๊ตฌ์ ๋๋ค. ํ๋ก์ ํธ์ ์ฐธ๊ณ ํ ์ ์๋ TSLint ์ค์ ์ ์ฌ์ฉํ ์ค๋น๊ฐ ๋์์ต๋๋ค:
-
TSLint Config Standard - ํ์ค ์คํ์ผ ๊ท์น
-
TSLint Config Airbnb - ์์ด๋น์๋น ์คํ์ผ ๊ฐ์ด๋
-
TSLint Clean Code - Clean Code: A Handbook of Agile Software Craftsmanship์ ์๊ฐ ๋ฐ์ TSLint ๊ท์น
-
TSLint react - React & JSX์ ๊ด๋ จ๋ lint ๊ท์น
-
TSLint + Prettier - Prettier ์ฝ๋ ํฌ๋งทํฐ๋ฅผ ์ํ lint ๊ท์น
-
ESLint rules for TSLint - ํ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ํ ESLint ๊ท์น
-
Immutable - ํ์ ์คํฌ๋ฆฝํธ์์ ๋ณ๊ฒฝ์ ํ๋ฝํ์ง ์๋ ๊ท์น
๋ํ, ํ๋ฅญํ ์๋ฃ์ธ ํ์ ์คํฌ๋ฆฝํธ ์คํ์ผ ๊ฐ์ด๋์ ์ฝ๋ฉ ์ปจ๋ฒค์ ์ ์ฐธ๊ณ ํด์ฃผ์ธ์.
์ญ์์ฃผ: TSLint๋ deprecated๋์์ต๋๋ค. Roadmap: TSLint -> ESLint ์ด์๋ฅผ ํ์ธํด์ฃผ์ธ์.
์ผ๊ด์ ์ผ๋ก ๋์๋ฌธ์๋ฅผ ์ฌ์ฉํ์ธ์
๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ฌ ์์ฑํ๋ ๊ฒ์ ๋น์ ์๊ฒ ๋ณ์, ํจ์ ๋ฑ์ ๋ํด์ ๋ง์ ๊ฒ์ ์๋ ค์ค๋๋ค. ์ด ๊ท์น์ ์ฃผ๊ด์ ์ด์ด์, ๋น์ ์ ํ์ด ์ํ๋ ๊ฒ์ ์ ํํด์ผ ํฉ๋๋ค. ์ค์ํ ์ ์ ์ด๋ค ๊ฑธ ์ ํํ์๋ ์ง ๊ฐ์ ์ผ๊ด์ ์ด์ด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค.
Bad:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restore_database() {}
type animal = { /* ... */ }
type Container = { /* ... */ }
Good:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];
function eraseDatabase() {}
function restoreDatabase() {}
type Animal = { /* ... */ }
type Container = { /* ... */ }
ํด๋์ค, ์ธํฐํ์ด์ค, ํ์
๊ทธ๋ฆฌ๊ณ ๋ค์์คํ์ด์ค ์ด๋ฆ์๋ PascalCase
๋ฅผ ์ฌ์ฉํ์ธ์.
๋ณ์, ํจ์ ๊ทธ๋ฆฌ๊ณ ํด๋์ค ๋ฉค๋ฒ ์ด๋ฆ์๋ camelCase
๋ฅผ ์ฌ์ฉํ์ธ์.
ํจ์ ํธ์ถ์์ ํผํธ์ถ์๋ฅผ ๊ฐ๊น๊ฒ ์์น์ํค์ธ์
ํจ์๊ฐ ๋ค๋ฅธ ํจ์๋ฅผ ํธ์ถํ ๋, ์ฝ๋์์ ์ด ํจ์๋ค์ ์์ง์ ์ผ๋ก ๊ฐ๊น๊ฒ ์ ์งํ๋๋ก ํ์ธ์. ์ด์์ ์ผ๋ก๋, ํธ์ถํ๋ ํจ์๋ฅผ ํธ์ถ์ ๋นํ๋ ํจ์ ๋ฐ๋ก ์์ ์์น์ํค๋๊ฒ ์ข์ต๋๋ค. ์ฐ๋ฆฌ๋ ์ ๋ฌธ์ฒ๋ผ ์ฝ๋๋ฅผ ์์์ ์๋๋ก ์ฝ๋ ๊ฒฝํฅ์ด ์๊ธฐ ๋๋ฌธ์, ์ฝ๋๋ฅผ ์์ฑํ ๋์๋ ์ด๋ฐ ๋ฐฉ์์ผ๋ก ์ฝ๋ ๊ฒ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
Bad:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getManagerReview() {
const manager = this.lookupManager();
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
Good:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private getManagerReview() {
const manager = this.lookupManager();
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.review();
import ๊ตฌ๋ฌธ์ ํน์ ์์๋๋ก ์ ๋ฆฌํ์ธ์
import
๊ตฌ๋ฌธ์ ์ฝ๊ธฐ ์ฝ๊ณ ๋ช
๋ฃํ๊ฒ ํ๋ฉด ๋น์ ์ ํ์ฌ ์ฝ๋์ ์์กด์ฑ์ ๋น ๋ฅด๊ฒ ํ์ธํ ์ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ import
๊ตฌ๋ฌธ ์ ๋ฆฌ๋ฅผ ์ํ ์ข์ ๋ฐฉ๋ฒ๋ค์ ์ ์ฉํด๋ณด์ธ์:
- import ๊ตฌ๋ฌธ์ ์ํ๋ฒณ ์์๋๋ก ๋ฐฐ์ดํ๊ณ ๊ทธ๋ฃนํํด์ผ ํฉ๋๋ค.
- ์ฌ์ฉํ์ง ์์ import ๊ตฌ๋ฌธ์ ์ ๊ฑฐ๋์ด์ผ ํฉ๋๋ค.
- ์ด๋ฆ์ด ์๋ import ๊ตฌ๋ฌธ์ ์ํ๋ฒณ ์์๋๋ก ๋ฐฐ์ดํด์ผ ํฉ๋๋ค. (์:
import {A, B, C} from 'foo';
) - import ํ๋ ์์ค์ฝ๋๋ ๊ทธ๋ฃน ๋ด์์ ์ํ๋ฒณ ์์๋๋ก ๋ฐฐ์ดํด์ผ ํฉ๋๋ค. (์:
import * as foo from 'a'; import * as bar from 'b';
) - import ๊ตฌ๋ฌธ์ ๊ทธ๋ฃน์ ๋น ์ค๋ก ๊ตฌ๋ถ๋์ด์ผ ํฉ๋๋ค.
- ๊ทธ๋ฃน์ ๋ค์ ์์๋ฅผ ์ค์ํด์ผ ํฉ๋๋ค:
- ํด๋ฆฌํ (์:
import 'reflect-metadata';
) - Node ๋ด์ฅ ๋ชจ๋ (์:
import fs from 'fs';
) - ์ธ๋ถ ๋ชจ๋ (์:
import { query } from 'itiriri';
) - ๋ด๋ถ ๋ชจ๋ (์:
import { UserService } from 'src/services/userService';
) - ์์ ๋๋ ํ ๋ฆฌ์์ ๋ถ๋ฌ์ค๋ ๋ชจ๋ (์:
import foo from '../foo'; import qux from '../../foo/qux';
) - ๋์ผํ ๊ณ์ธต์ ๋๋ ํ ๋ฆฌ์์ ๋ถ๋ฌ์ค๋ ๋ชจ๋ (์:
import bar from './bar'; import baz from './bar/baz';
)
- ํด๋ฆฌํ (์:
Bad:
import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
Good:
import 'reflect-metadata';
import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';
import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';
import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
ํ์ ์คํฌ๋ฆฝํธ ์จ๋ฆฌ์ด์ค๋ฅผ ์ฌ์ฉํ์ธ์
tsconfig.json
์ compilerOptions
์น์
์์์ paths
์ baseUrl
์์ฑ์ ์ ์ํด ๋ ๋ณด๊ธฐ ์ข์ import ๊ตฌ๋ฌธ์ ์์ฑํด์ฃผ์ธ์.
์ด ๋ฐฉ๋ฒ์ import ๊ตฌ๋ฌธ์ ์ฌ์ฉํ ๋ ๊ธด ์๋๊ฒฝ๋ก๋ฅผ ์์ฑํ๋ ๊ฒ์ ํผํ๊ฒ ๋์์ค ๊ฒ์ ๋๋ค.
Bad:
import { UserService } from '../../../services/UserService';
Good:
import { UserService } from '@services/UserService';
// tsconfig.json
...
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@services": ["services/*"]
}
...
}
...
์ฃผ์
์ฃผ์์ ์ฌ์ฉํ๋ ๊ฒ์ ์ฃผ์ ์์ด ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์คํจํ๋ค๋ ํ์์ ๋๋ค. ์ฝ๋๋ ๋จ์ผ ์ง์ค ๊ณต๊ธ์(Single source of truth)์ด์ด์ผ ํฉ๋๋ค.
๋์ ์ฝ๋์ ์ฃผ์๋ค ๋ฌ์ง ๋ง๋ผ. ์๋ก ์ง๋ผ.
โ Brian W. Kernighan and P. J. Plaugher
์ฃผ์ ๋์ ์ ์์ฒด์ ์ผ๋ก ์ค๋ช ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ์์ฑํ์ธ์
์ฃผ์์ ๋ณ๋ช ์ผ ๋ฟ, ํ์ํ์ง ์์ต๋๋ค. ์ข์ ์ฝ๋๋ ๋๋ถ๋ถ ๊ทธ ์กด์ฌ ์์ฒด๋ก ๋ฌธ์ํ๊ฐ ๋ฉ๋๋ค.
Bad:
// subscription์ด ํ์ฑํ ์ํ์ธ์ง ์ฒดํฌํฉ๋๋ค.
if (subscription.endDate > Date.now) { }
Good:
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }
๋น์ ์ ์ฝ๋๋ฅผ ์ฃผ์ ์ฒ๋ฆฌํ์ง ๋ง์ธ์
๋ฒ์ ๊ด๋ฆฌ ์์คํ ์ด ์กด์ฌํ๋ ์ด์ ์ ๋๋ค. ์ฌ์ฉํ์ง ์๋ ์ฝ๋๋ ๊ธฐ๋ก์ ๋จ๊ธฐ์ธ์.
Bad:
type User = {
name: string;
email: string;
// age: number;
// jobPosition: string;
}
Good:
type User = {
name: string;
email: string;
}
์ผ๊ธฐ ๊ฐ์ ์ฃผ์์ ๋ฌ์ง ๋ง์ธ์
๋ฒ์ ๊ด๋ฆฌ ์์คํ
์ ์ฌ์ฉํ์ธ์! ์ฃฝ์ ์ฝ๋, ์ฃผ์ ์ฒ๋ฆฌ๋ ์ฝ๋, ํนํ ์ผ๊ธฐ ๊ฐ์ ์ฃผ์์ ํ์ ์์ต๋๋ค. ๋์ ์ ๊ธฐ๋ก์ ๋ณด๊ธฐ ์ํด git log
๋ช
๋ น์ด๋ฅผ ์ฌ์ฉํ์ธ์!
Bad:
/**
* 2016-12-20: ์ดํดํ์ง ๋ชปํด์ ๋ชจ๋๋๋ฅผ ์ ๊ฑฐํจ (RM)
* 2016-10-01: ํน๋ณํ ๋ชจ๋๋๋ฅผ ์ฌ์ฉํด ๊ฐ์ ํจ (JP)
* 2016-02-03: ํ์
์ฒดํน ์ถ๊ฐํจ (LI)
* 2015-03-14: combine ํจ์๋ฅผ ๊ตฌํํจ (JR)
*/
function combine(a: number, b: number): number {
return a + b;
}
Good:
function combine(a: number, b: number): number {
return a + b;
}
์ฝ๋์ ์์น๋ฅผ ์ค๋ช ํ๋ ์ฃผ์์ ์ฌ์ฉํ์ง ๋ง์ธ์
์ด๊ฑด ๋ณดํต ์ฝ๋๋ฅผ ์ด์ง๋ฝํ๊ธฐ๋ง ํฉ๋๋ค. ํจ์์ ๋ณ์ ์ด๋ฆ์ ์ ์ ํ ๋ค์ฌ์ฐ๊ธฐ์ ์์์ผ๋ก ๋น์ ์ ์ฝ๋์ ์๊ฐ์ ์ธ ๊ตฌ์กฐ๊ฐ ๋ณด์ด๋๋ก ํ์ธ์.
๋๋ถ๋ถ์ IDE(ํตํฉ ๊ฐ๋ฐ ํ๊ฒฝ)์์๋ ์ฝ๋ ๋ธ๋ก์ ์ ๊ธฐ/ํผ์น๊ธฐ
ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ง์ํฉ๋๋ค. (Visual Studio Code์ folding regions๋ฅผ ํ์ธํด๋ณด์ธ์).
Bad:
////////////////////////////////////////////////////////////////////////////////
// Client ํด๋์ค
////////////////////////////////////////////////////////////////////////////////
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
////////////////////////////////////////////////////////////////////////////////
// public ๋ฉ์๋
////////////////////////////////////////////////////////////////////////////////
public describe(): string {
// ...
}
////////////////////////////////////////////////////////////////////////////////
// private ๋ฉ์๋
////////////////////////////////////////////////////////////////////////////////
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
Good:
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
public describe(): string {
// ...
}
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...
}
};
TODO ์ฃผ์
์ถํ์ ๊ฐ์ ์ ์ํด ์ฝ๋์ ๋ฉ๋ชจ๋ฅผ ๋จ๊ฒจ์ผํ ๋, // TODO
์ฃผ์์ ์ฌ์ฉํ์ธ์. ๋๋ถ๋ถ์ IDE๋ ์ด๋ฐ ์ข
๋ฅ์ ์ฃผ์์ ํน๋ณํ๊ฒ ์ง์ํ๊ธฐ ๋๋ฌธ์ ํด์ผํ ์ผ ๋ชฉ๋ก์ ๋น ๋ฅด๊ฒ ๊ฒํ ํ ์ ์์ต๋๋ค.
ํ์ง๋ง TODO ์ฃผ์์ด ๋์ ์ฝ๋๋ฅผ ์์ฑํ ์ด์ ๋ ์๋๋ผ๋ ๊ฒ์ ๋ช ์ฌํ์ธ์.
Bad:
function getActiveSubscriptions(): Promise<Subscription[]> {
// ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
Good:
function getActiveSubscriptions(): Promise<Subscription[]> {
// TODO: ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date() } });
}
๋ฒ์ญ
์ด ๊ธ์ ๋ค๋ฅธ ์ธ์ด๋ก๋ ์ฝ์ ์ ์์ต๋๋ค:
- Brazilian Portuguese: vitorfreitas/clean-code-typescript
- Chinese:
- Japanese: MSakamaki/clean-code-typescript
- Russian: Real001/clean-code-typescript
- Turkish: ozanhonamlioglu/clean-code-typescript
- Korean: 738/clean-code-typescript
๋ฒ์ญ์ด ์๋ฃ๋๋ฉด ์ฐธ๊ณ ๋ฌธํ์ ์ถ๊ฐ๋ฉ๋๋ค. ์์ธํ ๋ด์ฉ๊ณผ ์งํ์ํฉ์ ๋ณด๊ณ ์ถ๋ค๋ฉด ์ด ๋ ผ์๋ฅผ ํ์ธํ์ธ์. ๋น์ ์ ๋น์ ์ ์ธ์ด์ ์ด ๊ธ์ ๋ฒ์ญํจ์ผ๋ก์จ ํด๋ฆฐ ์ฝ๋ ์ปค๋ฎค๋ํฐ์ ์ค์ํ ๊ธฐ์ฌ๋ฅผ ํ ์ ์์ต๋๋ค.