Google의 TypeScript Style Guide > Source file structure

FeelsBotMan·2024년 11월 29일
0

GTS

목록 보기
3/8
post-thumbnail

3 Source file structure

소스 파일의 구성 순서에 대한 규칙은 다음과 같다.

  1. 저작권 정보 (필요한 경우)
  2. JSDoc with @fileoverview (필요한 경우)
    파일의 목적이나 설명을 제공하는 JSDoc 주석이다.
    보통 파일의 기능과 구조를 문서화할 때 사용된다.
  3. Imports (필요한 경우)
  4. 파일의 구현

각 섹션은 정확히 한 줄의 빈 줄로 구분되어야 한다.

// 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으로 추가하라.

3.2 @fileoverview JSDoc

@fileoverview JSDoc의 용도:
@fileoverview 태그는 파일의 목적과 내용, 그리고 이 파일이 어떻게 사용되는지를 문서화하는 데 사용된다.
코드의 시작 부분에 위치하며, 파일 전체에 대한 설명을 제공하여 다른 개발자가 파일의 내용을 이해하는 데 도움을 준다.

3.3 Imports

ES6와 TypeScript에서 사용하는 import 문에는 네 가지 방법이 있다.

  1. 모듈
    모듈 전체를 가져올 때 사용. 모듈에서 export된 모든 내용을 이름으로 접근할 수 있다.
import * as ng from '@angular/core';
  1. 네임드
    모듈에서 특정 항목(함수, 객체, 클래스 등)을 선택적으로 가져올 때 사용.
import { Foo } from './foo';
  1. 디폴트
    모듈이 export default로 내보내는 항목을 가져올 때 사용하며, 이때 가져오는 이름은 자유롭게 지정할 수 있다.
import Button from 'Button';
  1. 사이드이펙트
    모듈을 코드에 포함시키지만, 해당 모듈에서 export된 항목을 코드에서 직접 사용할 필요는 없다. 종종 라이브러리의 초기화설정을 위해 사용된다.
import 'jasmine';
import '@polymer/paper-button';
  • 이러한 코드는 jasmine이나 @polymer/paper-button 모듈의 부수 효과만을 가져오기 위해 사용되며, 모듈의 export된 기능을 코드에서 참조하지는 않음.

3.3.1 Import paths

상대 경로 사용을 권장함:

상대 경로 (Relative Path):
./ 또는 ../를 사용하여 현재 파일의 위치를 기준으로 다른 파일을 참조.

절대 경로 (Absolute Path):
프로젝트의 루트 디렉토리에서부터 시작하는 경로.

상대경로를 사용하면 프로젝트를 다른 위치로 옮기거나 디렉토리 구조를 변경해도 코드 수정 없이 그대로 사용할 수 있다.
다만, ../../../와 같이 너무 많은 단계의 상위 경로를 남용하지 말자.

import { Symbol3 } from './sibling';  // 같은 폴더 내의 파일을 참조
import { Symbol2 } from '../parent/file';  // 상위 폴더 내의 파일을 참조

TypeScript의 paths 설정
tsconfig.json 파일에서 compilerOptionspaths 옵션을 사용하여 모듈의 경로 별칭을 설정할 수 있다.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
import { Symbol1 } from '@components/button';
import { Symbol2 } from '@utils/helper';
  • 절대 경로 사용 시의 단점을 보완하고 가독성을 높이는 데 도움이 된다.

3.3.2 Namespace versus named imports

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) {
  // 구현 내용
}

3.3.3 Renaming imports

이름 변경이 도움이 되는 세 가지 경우

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]);

3.4 Exports

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';

3.4.1 Export visibility

타입스크립트의 가시성 제한 한계
타입스크립트는 자바나 C++과 같은 언어의 private, protected 접근 제어자를 모듈 레벨에서 완전히 지원하지 않기 때문에 모듈 외부로 익스포트되는 심볼(변수, 함수, 클래스 등)은 해당 모듈 외부에서 자유롭게 접근할 수 있다.

외부로 노출되는 API(공개 인터페이스)를 최소화하는 것이 좋다:
특정 심볼만 선택적으로 내보내는 것이 좋다. 예를 들어, 전체 모듈을 export하는 것보다 필요한 것만 export하는 것이 바람직하다.
모듈 내부에서만 사용되는 함수나 클래스는 export하지 않아야 한다. 이러한 것들은 모듈 외부에서 접근할 필요가 없으므로 내부 구현으로 남겨야 한다.

API 표면 최소화의 중요성:

  • 코드 유지보수 용이: 외부로 노출되는 심볼이 줄어들면 코드의 복잡도가 감소하고, 변경 시 외부 코드에 미치는 영향이 줄어들어 유지보수가 쉬워진다.
  • 명확한 의도: 외부 코드가 모듈의 내부 구현을 알고 필요하지 않은 부분까지 사용하는 것을 방지할 수 있다.
  • 캡슐화: 모듈 내부의 구현 세부 사항을 숨기고, 필요한 인터페이스만 제공함으로써 코드의 캡슐화가 향상된다.

3.4.2 Mutable exports

export let을 사용하여 가변 변수를 내보내는 것은 권장되지 않는다:

변경 가능한(Mutable) 익스포트의 문제점
TypeScript 및 ES6에서는 export let을 통해 변수를 내보낼 수 있지만, 이는 다음과 같은 문제를 일으킬 수 있다:

  • 가변성 문제: 모듈을 import하는 다른 파일에서 변수의 값이 언제 어떻게 변경될지 예측하기 어렵다. 코드가 복잡해지면 이러한 변수의 상태를 추적하기 어려워지고, 디버깅이 힘들어진다.
  • 모듈 간 재익스포트(Re-export) 시의 불일치: ES6 모듈 시스템과 TypeScript의 모듈 시스템에서 동작이 다를 수 있다. 한 모듈에서 다른 모듈로 재익스포트할 경우, 값 변경이 모든 import 지점에 일관되게 반영되지 않을 수 있다.

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가 이루어지도록 하여 코드의 일관성과 예측 가능성을 높여야 한다.

이 패턴의 중요한 원칙:

  • 조건부 로직을 통해 어떤 API나 값을 익스포트할지 결정
  • 모듈 본문 실행 후 모든 익스포트는 최종(final) 상태여야 한다.
  • 익스포트되는 값은 한 번 결정되면 변경되지 않음
function pickApi() {
  if (useOtherApi()) return OtherApi;
  return RegularApi;
}
export const SomeApi = pickApi();
  • pickApi 함수는 조건에 따라 OtherApi 또는 RegularApi를 선택하여 SomeApi에 할당하고, export됩니다.

3.4.3 Container classes

네임스페이싱을 위한 정적 메서드나 프로퍼티를 가진 컨테이너 클래스 사용을 피하자:

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);
  • 더 명확한 코드 구조: 각 함수와 상수가 독립적으로 존재. 불필요한 클래스 래퍼 제거하고 코드를 더 평평하고(flat) 직접적으로 만들기.
  • 더 나은 트리 셰이킹(Tree Shaking): 사용하지 않는 함수나 상수 제거가 쉬워짐. 번들 크기 최적화에 유리.
  • 가독성 향상: 코드가 더 직관적이고 단순해짐.

3.5 Import and export type

3.5.1 Import type

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을 사용하는가?

  • 코드가 더 명확해지고, 타입과 값의 구분이 쉬워진다.
  • 불필요한 런타임 모듈 로드를 방지하여 빌드 성능을 개선할 수 있다.
  • 프로덕션 코드가 경량화되어 실행 성능이 좋아질 수 있다.

3.5.2 Export type

export type은 명시적이고 안전한 타입 전달 메커니즘이다.

예를 들어, 다른 파일에서 정의된 타입을 다른 파일에서 다시 내보내려면 다음과 같이 작성할 수 있다:

// foo.ts
export interface AnInterface {
  name: string;
  age: number;
}

// bar.ts
export type { AnInterface } from './foo';

export type을 사용하는가?

  • TypeScript는 파일 단위로 변환(transpile)할 때 export type 구문을 사용하여 타입만 재-export하면, 런타임 코드가 생성되지 않는다. 이는 파일 단위 전환(isolatedModules) 시 타입 정보만 전달하고, 실제 코드 실행에는 영향을 미치지 않게 해준다.
  • export type을 사용하면 해당 기호가 타입임을 명확히 할 수 있다. 이렇게 하면 코드가 더 읽기 쉽고 유지보수가 쉬워진다.

3.5.3 Use modules not namespaces

namespace, <reference>, require() 구문은 지양하고 ES6 모듈 시스템을 사용하는 것이 권장된다:

  • 더 나은 코드 구조화: ES6 모듈은 파일 단위로 코드를 분리하고, 명확한 의도를 나타낼 수 있다.
  • 호환성: ES6 모듈은 현대 자바스크립트 환경과 호환되며, 브라우저와 Node.js에서 모두 지원된다.
  • 트리 쉐이킹: 불필요한 코드를 제거해 코드 크기를 줄일 수 있다.
  • 타입스크립트의 최신 기능 지원: 최신 타입스크립트 기능은 ES6 모듈을 기반으로 작동한다.

Before

import x = require('mydep');

After

export class Rocket {
  launch() { ... }
}

import { Rocket } from './rocket';

참고자료

Google TypeScript Style Guide

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

ts.dev - TypeScript style guide

profile
안드로이드 페페

0개의 댓글