소스 파일의 구성 순서에 대한 규칙은 다음과 같다.
@fileoverview
(필요한 경우)각 섹션은 정확히 한 줄의 빈 줄로 구분되어야 한다.
// Copyright (c) 2024 Your Name. All rights reserved.
/**
* @fileoverview This file implements a simple calculator.
*/
// Import statements
import { add, subtract } from './mathUtils';
// Implementation code
function calculateSum(a, b) {
return add(a, b);
}
function calculateDifference(a, b) {
return subtract(a, b);
}
파일에 라이선스나 저작권 정보가 필요하다면 파일 맨 위에 JSDoc으로 추가하라.
@fileoverview
JSDoc@fileoverview
JSDoc의 용도:
@fileoverview
태그는 파일의 목적과 내용, 그리고 이 파일이 어떻게 사용되는지를 문서화하는 데 사용된다.
코드의 시작 부분에 위치하며, 파일 전체에 대한 설명을 제공하여 다른 개발자가 파일의 내용을 이해하는 데 도움을 준다.
ES6와 TypeScript에서 사용하는 import 문에는 네 가지 방법이 있다.
import * as ng from '@angular/core';
import { Foo } from './foo';
import Button from 'Button';
import 'jasmine';
import '@polymer/paper-button';
jasmine
이나 @polymer/paper-button
모듈의 부수 효과만을 가져오기 위해 사용되며, 모듈의 export된 기능을 코드에서 참조하지는 않음.상대 경로 사용을 권장함:
상대 경로 (Relative Path):
./
또는 ../
를 사용하여 현재 파일의 위치를 기준으로 다른 파일을 참조.
절대 경로 (Absolute Path):
프로젝트의 루트 디렉토리에서부터 시작하는 경로.
상대경로를 사용하면 프로젝트를 다른 위치로 옮기거나 디렉토리 구조를 변경해도 코드 수정 없이 그대로 사용할 수 있다.
다만, ../../../
와 같이 너무 많은 단계의 상위 경로를 남용하지 말자.
import { Symbol3 } from './sibling'; // 같은 폴더 내의 파일을 참조
import { Symbol2 } from '../parent/file'; // 상위 폴더 내의 파일을 참조
TypeScript의
paths
설정
tsconfig.json
파일에서compilerOptions
의paths
옵션을 사용하여 모듈의 경로 별칭을 설정할 수 있다.{ "compilerOptions": { "baseUrl": ".", "paths": { "@components/*": ["src/components/*"], "@utils/*": ["src/utils/*"] } } }
import { Symbol1 } from '@components/button';
import { Symbol2 } from '@utils/helper';
Namespace:
정의: 모듈 전체를 하나의 객체처럼 가져와, 해당 객체를 통해 모든 export된 심볼에 접근하는 방식
대규모 API에서 많은 심볼을 사용하는 경우에 적합. 특히, 같은 이름의 충돌을 대비해 각각의 별칭을 선언하여 구분할 필요가 없어져서 좋다.
Before
// Bad: 너무 긴 import 문
import {Item as TableviewItem, Header as TableviewHeader, Row as TableviewRow,
Model as TableviewModel, Renderer as TableviewRenderer} from './tableview';
let item: TableviewItem | undefined;
After
// GOOD: 모듈 자체를 namespace로 사용
import * as tableview from './tableview';
let item: tableview.Item | undefined;
named imports:
정의: 모듈에서 필요한 특정 심볼만 선택하여 import하는 방식
as
키워드를 사용해 각각의 심볼에 별칭을 강조할 때 사용.
별칭은 명확하고 구체적인 이름으로 서로 겹치지 않게 사용.
Before
import * as testing from './testing';
// BAD: 모듈 이름은 가독성을 향상시키지 않음
testing.describe('foo', () => {
testing.it('bar', () => {
testing.expect(null).toBeNull();
testing.expect(undefined).toBeUndefined();
});
});
After
import {describe, it, expect} from './testing';
// GOOD: 특정 함수들을 별도로 import하여 더 읽기 좋게 만들기
describe('foo', () => {
it('bar', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
});
});
특수 사례: Apps JSPB Protos:
JSPB 프로토타입 파일을 사용할 때는 빌드 최적화를 위해(빌드 성능 향상 및 dead code elimination(불필요 코드 제거)) named imports를 사용해야 한다.
.proto
파일은 여러 메시지를 포함할 수 있지만, 특정 메시지만 필요할 때 해당 메시지만 import하는 것이 효율적이다.
// Good: 필요한 심볼만 가져오기
import {Foo, Bar} from './foo.proto';
function copyFooBar(foo: Foo, bar: Bar) {
// 구현 내용
}
이름 변경이 도움이 되는 세 가지 경우
1. 중복 심볼 충돌 방지:
동일한 이름의 심볼이 여러 모듈에서 가져올 때 발생하는 상황.
import {Button as HeaderButton} from './header';
import {Button as SidebarButton} from './sidebar';
let headerBtn: HeaderButton;
let sidebarBtn: SidebarButton;
2. 동적으로 생성된 심볼 처리 (Handling Generated Symbol Names):
코드 생성기나 자동화 도구에서 생성된 심볼의 이름이 복잡하거나 읽기 어려운 경우.
예를 들어, 그래픽 라이브러리에서 자동 생성된 인터페이스나 타입의 이름을 더 의미 있게 만들 때 사용
import {
ComplexlyGeneratedInterfaceWithAVeryLongName as GraphicElement
} from 'graphic-library';
function renderElement(element: GraphicElement) {
// 코드가 더 읽기 쉬워진다.
}
3. 모호한 심볼 이름을 명확하게 하기:
심볼의 이름이 코드의 의도를 명확하게 전달하지 못할 때, 코드의 가독성을 높이기 위해 별칭을 붙여 사용하는 방법.
import { from as createObservableFrom } from 'rxjs';
const numbers$ = createObservableFrom([1, 2, 3, 4, 5]);
default exports
대신에 named exports
를 권장한다:
default exports
는 모듈당 하나의 기본 값을 내보내는 방식으로 다음의 단점이 존재한다.
default exports
는 가져올 때 자유롭게 이름을 지정할 수 있어 코드의 일관성이 깨질 수 있다. 같은 모듈을 Foo
, Bar
등 다른 이름으로 가져올 수 있기 때문에 코드를 읽을 때 어떤 클래스나 함수가 실제로 가져와졌는지 파악하기 어려울 수 있다.// foo.ts (디폴트 익스포트)
export default class User { ... }
// 다음 모든 임포트가 legal하고 완전히 다른 이름 사용 가능
import User from './foo';
import Person from './foo';
import RandomClass from './foo';
// 반면 네임드 익스포트는 더 엄격하고 명확하다
import { User } from './foo';
default exports
를 사용할 경우, 가져온 값의 타입과 이름을 정확히 알기 어렵기 때문에 코드 오류가 발생할 경우 디버깅이 어려울 수 있다. // foo.ts (문제적 디폴트 익스포트)
const foo = 'hello';
export default foo;
// bar.ts
import fizz from './foo'; // fizz === 'hello' (의도치 않은 결과!)
// 반면 네임드 익스포트는 명확한 오류 발생
import { foo } from './foo'; // 명확한 컴파일 에러
default exports
는 모든 것을 하나의 클래스에 넣으려는 유혹을 만든다.// 안티 패턴: 디폴트 익스포트
export default class User {
static ROLE_ADMIN = 'admin';
static createUser() { ... }
name: string;
// 클래스 로직...
}
// 권장되는 패턴: 네임드 익스포트
export const ROLE_ADMIN = 'admin';
export function createUser() { ... }
export class User {
name: string;
// 클래스 로직에 집중
}
named exports
는 파일 스코프를 네임스페이스로 활용할 수 있게 해준다.// users.ts
export const DEFAULT_ROLE = 'user';
export interface UserData { ... }
export class User { ... } // 불필요한 클래스
export function validateUser() { ... }
// 다른 파일에서 명확하고 선택적인 임포트 가능
import { User, validateUser, DEFAULT_ROLE } from './users';
타입스크립트의 가시성 제한 한계
타입스크립트는 자바나 C++과 같은 언어의 private
, protected
접근 제어자를 모듈 레벨에서 완전히 지원하지 않기 때문에 모듈 외부로 익스포트되는 심볼(변수, 함수, 클래스 등)은 해당 모듈 외부에서 자유롭게 접근할 수 있다.
외부로 노출되는 API(공개 인터페이스)를 최소화하는 것이 좋다:
특정 심볼만 선택적으로 내보내는 것이 좋다. 예를 들어, 전체 모듈을 export하는 것보다 필요한 것만 export하는 것이 바람직하다.
모듈 내부에서만 사용되는 함수나 클래스는 export하지 않아야 한다. 이러한 것들은 모듈 외부에서 접근할 필요가 없으므로 내부 구현으로 남겨야 한다.
API 표면 최소화의 중요성:
export let
을 사용하여 가변 변수를 내보내는 것은 권장되지 않는다:
변경 가능한(Mutable) 익스포트의 문제점
TypeScript 및 ES6에서는 export let
을 통해 변수를 내보낼 수 있지만, 이는 다음과 같은 문제를 일으킬 수 있다:
before
export let foo = 3;
window.setTimeout(() => {
foo = 4; // 값이 변경됨
}, 1000);
After
let foo = 3;
window.setTimeout(() => {
foo = 4; // 값이 변경됨
}, 1000);
// 외부 코드에서 foo의 값을 가져오는 getter 함수 사용
export function getFoo() { return foo; }
조건부 익스포트 패턴:
조건부 내보내기를 할 때는 조건이 먼저 평가되고 그 후에 export가 이루어지도록 하여 코드의 일관성과 예측 가능성을 높여야 한다.
이 패턴의 중요한 원칙:
function pickApi() {
if (useOtherApi()) return OtherApi;
return RegularApi;
}
export const SomeApi = pickApi();
pickApi
함수는 조건에 따라 OtherApi
또는 RegularApi
를 선택하여 SomeApi
에 할당하고, export됩니다. 네임스페이싱을 위한 정적 메서드나 프로퍼티를 가진 컨테이너 클래스 사용을 피하자:
before
export class MathUtils {
static PI = 3.14159;
static calculateCircleArea(radius: number) {
return this.PI * radius * radius;
}
static calculateCircumference(radius: number) {
return 2 * this.PI * radius;
}
}
// 사용 예
import { MathUtils } from './math-utils';
const area = MathUtils.calculateCircleArea(5);
After
export const PI = 3.14159;
export function calculateCircleArea(radius: number) {
return PI * radius * radius;
}
export function calculateCircumference(radius: number) {
return 2 * PI * radius;
}
// 사용 예
import { calculateCircleArea, PI } from './math-utils';
const area = calculateCircleArea(5);
TypeScript 프로젝트에서는 타입과 값을 명확히 구분하여 임포트하는 것이 좋다:
타입 임포트의 세 가지 방식
// 방식 1: 타입만 임포트, 런타임 코드 로드 없이 타입만 임포트
import type {Foo} from './foo';
// 방식 2: 값 임포트, 런타임 코드 로드와 함께 값 임포트
import {Bar} from './foo';
// 방식 3: 혼합 임포트, `Foo`는 타입으로만, `Bar`는 값으로 임포트
import {type Foo, Bar} from './foo';
TypeScript의 컴파일 모드
import type
의 정확한 사용 확인.왜 import type
을 사용하는가?
export type
은 명시적이고 안전한 타입 전달 메커니즘이다.
예를 들어, 다른 파일에서 정의된 타입을 다른 파일에서 다시 내보내려면 다음과 같이 작성할 수 있다:
// foo.ts
export interface AnInterface {
name: string;
age: number;
}
// bar.ts
export type { AnInterface } from './foo';
왜 export type
을 사용하는가?
export type
구문을 사용하여 타입만 재-export하면, 런타임 코드가 생성되지 않는다. 이는 파일 단위 전환(isolatedModules) 시 타입 정보만 전달하고, 실제 코드 실행에는 영향을 미치지 않게 해준다.export type
을 사용하면 해당 기호가 타입임을 명확히 할 수 있다. 이렇게 하면 코드가 더 읽기 쉽고 유지보수가 쉬워진다.namespace
, <reference>
, require()
구문은 지양하고 ES6 모듈 시스템을 사용하는 것이 권장된다:
Before
import x = require('mydep');
After
export class Rocket {
launch() { ... }
}
import { Rocket } from './rocket';
GitHub - google/gts: ☂️ TypeScript style guide, formatter, and linter.
Typescript Google Code Style Part 1
Typescript Google Code Style Part 2
Typescript Google Code Style Part 3