본문 바로가기

vscode-with-tistory

vscode extension 개발일기9: 코드 최적화

코드 최적화

작성된 코드를 정리할 필요성이 느껴저서 아는대로 코드를 정리해보았다. 수행된 내용은 다음과 같다.

1. nodejs fs겉어내기

vscode에서는 nodejs fs 사용을 권장하지 않는다. 개인이 직접 파일을 읽고 쓰는 것에 대해 처리하는 과정이 어렵기 때문이라고 조심스럽게 예상해본다. nodejs에 종속되어 있는 기능을 사용하기 보다 vscode에 종속된 기능을 사용하는 것이 vscode가 파일 읽기를 보장해준다고 생각하여 vscode fs로 변경하기로 하였다.

https://code.visualstudio.com/updates/v1_37#_vscodeworkspacefs

해당 내용을 읽어보면 vscode.workspace.fs는 nodejs의 fs를 대체하는 기능을 제공해준다. vscode.workspace.fs는 SSH, WSL에 있는 remote file systems, 즉 외부 컴퓨터에 있는 파일을 읽을 수 있도록 지원해준다.

nodejs의 fs에 비해 vscode의 fs는 좀 더 간결하게 코드를 짜거나 nodejs fs에서 제공하지 않은 기능들을 제공해준다 예를들어 파일을 삭제할 때 nodejs fs모듈에는 파일을 휴지통으로 삭제하는 기능은 없지만 vscode의 fs에는 파일을 휴지통으로 삭제할 수 있는 기능을 옵션으로 제공한다.

추후 기능의 확장을 위해 nodejs의 fs대신 vscode의 fs를 사용하는 것이 유지보수측면에서 편리할 것이라고 생각하여 nodejs의 fs대신 vscode fs로 사용하기로 결정하였다.

2. 인터페이스 통합

게시글을 수정로직을 작성하다가 인터페이스 부분을 최적화하고 싶었다. 타입스크립트를 사용함에도 불구하고 인터페이스의 장점을 100% 이용하지 않는 것은 매우 불-편하기 때문이다.

기존의 인터페이스는 다음과 같다.

export interface BlogInfo {
    name: string;
    url: string;
    secondaryUrl: string;
    nickname: string;
    title: string;
    description: string;
    default: "Y" | "N";
    blogIconUrl: string;
    faviconUrl: string;
    profileThumbnailImageUrl: string;
    profileImageUrl: string;
    role: string;
    blogId: string;
    statistics: {
        post: string;
        comment: string;
        trackback: string;
        guestbook: string;
        invitation: string;
    };
}
export interface PostInfo {
    access_token: string;
    output: "json" | "xml";
    blogName: string;
    title: string;
    content?: string;
    visibility?: string;
    category?: string;
    published?: string;
    slogan?: string;
    tag?: string;
    acceptComment?: string;
    password?: string;
    postId?: string;
}

export interface PushPostInfo {
    url: string;
    secondaryUrl: string;
    title: string;
    content: string;
    categoryId: string;
    postUrl: string;
    visibility: string;
    acceptComment: string;
    acceptTrackback: string;
    tags: {
        tag: Array<string>;
    };
    comments: string;
    trackbacks: string;
    date: string;
}

export interface TistoryFormat {
    status: string;
    error_message?: string;
    items?: { categories: Array<CategoryInfo> } | BlogInfo;
    url?: string;
}

export interface CategoryInfo {
    id: string;
    name: string;
    parent: string;
    label: string;
    entries: string;
}

위처럼 저렇게 개판이 된 이유가 처음에 짤 때 인터페이스를 어떻게 합칠 지 몰랐기 때문이다. 그래서 검색해보니 인터페이스는 다음과 같은 방식으로 합칠 수 있었다.

1. 상속

타입스크립트는 상속을 통해 interface를 합칠 수 있다.

export interface Tistory {
    tistory: {
        status: string;
        error_message?: string;
    };
}

export interface ResponsePost extends Tistory {
    status: string;
    url: string;
    postId: string;
}

해당 방식으로 구현하여 부모 클래스를 재작성할 수 있다. 만약 상속을 수행하여 작성할 경우 부모 인터페이스의 필수 속성에 대한 정의가 필요하다. 예를들어 위의 인터페이스 코드에서 ResponsePost에서 status속성을 제거하면 타입스크립트가 에러를 반환한다. 이유는 부모 인터페이스인 Tistory에서 status라는 속성을 필수 속성으로 정의했기 때문이다.

2. type alias

type alias를 통해 2개의 타입을 1개로 합칠 수 있다.

export interface Tistory {
    tistory: {
        status: string;
        error_message?: string;
    };
}

interface FileUploadResult {
    tistory: {
        url: string;
        replacer: string;
    };
}
type TMP = Tistory & FileUploadResult;
const test: TMP = {
    tistory: {
        status: "200",
        url: "asdf",
        replacer: "asdf",
    },
};

이런 방식은 typescript내에 같은 객체가 있으면 이를 합쳐서 반환해준다. 인터페이스를 합쳐주는 것이므로 상속처럼 필수 속성이 없어서 자유롭게 합칠 수 있다.

1번과 2번중에 필자는 1번을 선택하기로 하였다. 이유는 다음과 같다.

  1. tistory객체는 네트워크 처리 객체에서 처리를 수행하고 필요한 아이템값만 뽑아서 api에서 사용하면 코드가 더 깔끔해질 것이라고 생각했기 때문이다.
  2. Tistory를 부모속성으로 작성하므로서 해당 인터페이스는 티스토리와 관련된 인터페이스임을 명시함

3. Network모듈 통일

티스토리 api를 요청해서 수행하는 코드를 1개의 네트워크 모듈로 통일하기로 하였다. axios모듈은 200~300내의 상태코드만 정상적으로 처리하고 나머진 에러로 던져준다. 이 과정은 에러임을 정확하게 파악할 수 있으나 티스토리측에서 제공해주는 에러 메세지를 읽을 수 없다. 그래서 모든 네트워크 통신은 200이상 500미만의 코드를 정상적으로 통과하고 axios의 결과 상태코드를 통해 직접 에러 여부를 판별하여야 티스토리 에러를 볼 수 있다. 이를 위해 공통적인 부분만 구현하고 메소드, 파라미터, URL은 입력 인자로 받아들여서 통신을 수행하도록 작성하였다.

export const requestTistory = async (
    axiosRequestConfig: AxiosRequestConfig
) => {
    axiosRequestConfig.validateStatus = (status: number) =>
        status >= 200 && status < 500;
    const axiosResponse: AxiosResponse<Tistory> = await axios(
        axiosRequestConfig
    );
    if (axiosResponse.status !== 200) {
        const {
            data: {
                tistory: { status, error_message },
            },
        } = axiosResponse;
        throw new Error(
            `${ERROR_MESSAGES.TistoryAPIError}\n status: ${status} | ${error_message}`
        );
    } else {
        return axiosResponse.data.tistory;
    }
};

Tistory인터페이스로 반환하고 반환된 값은 명시적 타입 형변환을 통해 사용한다. 명시적 타입형변환으로 원하는 타입으로 변경하여 사용할 때 Tistory타입의 필수요소를 만족하지 않으면 사용할 수 없다.

    const tistory: ResponsePost = await requestTistory({
        method: "post",
        url: API_URI.PUSH_POST,
        data: postedData,
    }) as ResponsePost;
    tistory as ResponsePost;
    return tistory;

그러나 기존의 tistory포멧에 맞지 않는 access_token을 발급하는 과정의 경우 해당 함수를 사용할 수 없다. 그래서 로그인을 수행하는 과정은 별도의 axios모듈로 네트워크 작업을 따로 작성해서 사용한다.

4. enum변수명 변경

API_URI의 enum변수명을 변경였다. PUSH_POST를 CREATE_POST로 바꾸었다. 초반에는 깃에 새 글을 PUSH한다는 생각으로 PUSH_POST라고 작성하였으나 뒤로갈 수록 의미가 햇갈려서 CRUD의 CREATE로 바꾸었다.

5. 포스트 업로드 분리

api.ts파일에 블로그 포스팅 함수만 존재하면 파일명의 역할이 무색하다고 생각하여 extension.ts에서 로그인을 수행하는 기능을 일부 삽입하였다.

6. 블로그 주소 대신 블로그이름으로

블로그 주소를 사용하니 문제점이 있다. 주소가 반드시 /로 끝나야 한다는 문제점이다. 이유는 기존의 로직이 블로그 주소를 보고 판별하는 방법을 사용하기 때문이다.

티스토리 api를 통해 등록된 블로그 리스트를 반환받으면 url값이 / 문자로 끝나있다. 그래서 / 문자로 끝는 url이어야만 판별할 수 있다.

개발 당시에는 중복된 이름이 존재할 수 있다고 생각하여 넘어갔으나 생각해보니 같은 유저가 같은 블로그이름을 여러개를 사용한다는 것부터 불가능하다는 것을 알아서 귀찮은 블로그 주소대신 블로그 이름으로 바꾸었다.