JK.dev

레거시 프로젝트, 유지보수 기반 다지기

레거시 프로젝트, 유지보수 기반 다지기

들어가며

이직한 뒤 맡게 된 프로젝트는 한동안 여러 사람의 손을 거치며 운영되어 온 레거시 프로젝트였습니다. 회사 차원에서도 프로젝트를 일회성으로 유지하는 수준이 아니라, 앞으로 지속적으로 운영해 나가기로 한 상태였습니다.

가장 먼저 고민한 것은 새로운 기능을 추가하는 일보다, 지금의 상태로 안정적으로 운영 가능한 기반이 갖춰져 있는가 하는 점이었습니다.

지속적인 운영과 유지보수를 위해서는 환경 분리, 코드 스타일 통일 등등 선행 작업이 필요했고, 이번 글에서는 그 과정에서 진행한 작업들을 정리해보려 합니다.

1. 환경 분리

기존 환경-백엔드 의존성-기능 영역 관계도는 대략 다음과 같았습니다.

기존 환경-백엔드 의존성 관계도

각 기능이 Firebase, Server를 혼합해 의존하고, 의존 관계가 dev·stg·prd 환경과 교차되면서 구조가 복잡해졌습니다. 그리고 검색엔진으로 사용하는 Algolia는 Firebase, Server 둘다에 의존하고 있습니다.

이 방식의 가장 큰 문제는 변경 영향 범위를 예측하기 어렵다는 점입니다.

하나의 기능을 수정해도 어떤 의존 대상을 통해 어느 환경까지 영향을 주는지 한눈에 파악하기 어렵고, 운영 환경에만 연결된 설정이나 데이터가 섞여 있으면 테스트와 검증도 제한됩니다.

1.1 Firebase 환경 분리

환경 분리 작업에서는 Firebase가 연결되는 환경부터 먼저 분리했습니다.

dev·stg·prd 기준으로 Firebase 환경을 선제적으로 나눠두면, 이후 기능을 Server로 이전하는 과정도 각 환경에서 독립적으로 검증할 수 있어 전체 작업의 안정성을 높일 수 있습니다.

즉, Firebase 환경 분리는 단순한 설정 분리가 아니라, 이후 의존성을 Server 중심으로 전환하기 위한 안전한 검증 기반을 마련하는 선행 작업이었습니다.

Firebase 환경 분리 후 관계도

1.2 Server 통일

이후 백엔드 의존성을 Firebase에서 자체 Server로 통일하는 작업을 진행했습니다. 백엔드 개발자 또한 데이터 흐름과 책임 경계가 분산된 구조적 한계를 인지하고 있었기 때문에, 프론트와 백엔드가 함께 방향을 맞춰 기능 단위로 마이그레이션 범위를 나누고 순차적으로 이전을 진행했습니다.

그 결과 기능별 처리 경로가 서버 중심으로 정리되었고, 백엔드는 로직과 데이터 관리를 더 일관되게 가져갈 수 있게 되었으며 프론트엔드도 연동 방식이 단순해져 개발과 유지보수의 복잡도를 줄일 수 있었습니다.

Server 통일 후 관계도

2. 코드 포맷, 정적 분석 규칙 정비

이 작업을 진행한 가장 큰 배경은, 프로젝트 안에 통일된 코드 작성 규칙이 없었다는 점입니다. 실제로는 프로젝트 차원의 일관된 기준보다 각 개발자의 IDE 설정이나 개인 습관에 따라 포맷과 스타일이 다르게 적용되고 있었고, 그 결과 같은 역할의 코드도 파일마다 표현 방식이 조금씩 달라지는 상태였습니다.

2.1 Prettier 적용

코드 포맷은 보편적으로 쓰이는 Prettier를 사용해 정리했습니다.

Prettier는 코드 스타일을 자동으로 정리해주는 코드 포맷터입니다.

들여쓰기, 줄바꿈 등 뿐만 아니라 가독성을 위해 import 순서도 함께 적용했습니다.

@trivago/prettier-plugin-sort-imports 적용 예시

Input

import React, { FC, useEffect, useRef, ChangeEvent, KeyboardEvent } from 'react';
import { logger } from '@core/logger';
import { reduce, debounce } from 'lodash-es';
import { Message } from '../Message';
import { createServer } from '@server/node';
import { Alert } from '@ui/Alert';
import { repeat, filter, add } from '../utils';
import { initializeApp } from '@core/app';
import { Popup } from '@ui/Popup';
import { createConnection } from '@server/database';

Output

import { debounce, reduce } from 'lodash-es';
import React, { ChangeEvent, FC, KeyboardEvent, useEffect, useRef } from 'react';

import { createConnection } from '@server/database';
import { createServer } from '@server/node';

import { initializeApp } from '@core/app';
import { logger } from '@core/logger';

import { Alert } from '@ui/Alert';
import { Popup } from '@ui/Popup';

import { Message } from '../Message';
import { add, filter, repeat } from '../utils';
.prettierrc
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80,
  "arrowParens": "always",
  "endOfLine": "lf",
  "bracketSpacing": true,
  "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
  "importOrder": [
    "^react$",
    "^next",
    "<THIRD_PARTY_MODULES>",
    "^@components/(.*)$",
    "^@apis/(.*)$",
    "^@config/(.*)$",
    "^@hooks/(.*)$",
    "^@store(.*)$",
    "^@constants/(.*)$",
    "^@utils/(.*)$",
    "^@assets/(.*)$",
    "^@styles/(.*)$",
    "^[./]",
    "^.+\\.(css|scss)$"
  ],
  "importOrderSeparation": true,
  "importOrderSortSpecifiers": true
}

(*글을 작성하는 시점에 찾아보니 prettier-plugin-packagejson, prettier-plugin-sort-json 플러그인으로 package.json, json 파일 또한 포맷을 적용해도 좋을 것 같습니다.)

2.2 ESLint

이 작업들은 당장 코드 품질을 높이는 게 아닌 최소한의 규칙을 설정하고 적용하는 것이 목표이기 때문에 빌드 에러가 발생하지 않도록 심각도는 "warn"으로 적용했습니다.

.eslintrc.json
{
  "root": true,
  "extends": ["next", "next/core-web-vitals", "prettier"],
  "rules": {
    "unused-imports/no-unused-imports": "warn",
    "react/display-name": "off",
    "react-hooks/exhaustive-deps": "warn",
    "@next/next/no-sync-scripts": "off",
    "no-console": "warn",
    "prefer-const": "warn",
    "no-var": "warn",
    "prefer-arrow-callback": "warn",
    "no-param-reassign": ["warn", { "props": false }],
    "object-shorthand": "warn",
    "prefer-template": "warn",
    "no-duplicate-imports": "warn",
    "react/jsx-no-target-blank": "warn",
    "react/self-closing-comp": "warn",
    "consistent-return": "warn",
    "no-fallthrough": "warn",
    "eqeqeq": ["warn", "smart"]
  },
  "plugins": ["unused-imports"]
}
ESLint 적용 후 경고 결과

3. 미사용 코드, 파일, 주석, 패키지 정리

기능 변경과 구조 개편이 반복되며 더 이상 사용되지 않는 코드, 파일, 주석, 패키지가 쉽게 누적됩니다. 이러한 요소들은 실제 동작과 무관하더라도 코드 탐색 범위를 넓히고, 현재 기준과 과거 흔적을 구분하기 어렵게 만들어 협업과 유지보수 비용을 높입니다.

따라서 미사용 자산 정리는 프로젝트에 실제로 필요한 요소만 남겨 구조를 명확하게 하고, 이후 작업의 복잡도를 낮추기 위한 선행 작업이었습니다.

더 이상 접근이 불가하거나 지원하지 않는 페이지, 유저 흐름 상 도달하지 않는 페이지 10여 개가 있었고 이 외에도 수많은 미사용 코드가 존재했습니다.

3.1 Knip

Knip은 JS, TS 프로젝트에서 미사용 패키지, 타입, export, 코드, 파일을 정리하는 패키지입니다.

Knip 홈페이지

유령 의존성(코드에서 참조하지만 package.json에 직접 선언되지 않은 경우)에 대해서도 탐색이 되어 설치하는 작업도 진행하였습니다.

Knip 유령 의존성 탐색 결과
knip.json
{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "entry": ["src/pages/**/*.{js,jsx,ts,tsx}", "next-sitemap.config.js", "scripts/**/*.{js,ts}"],
  "project": ["src/**/*.{js,jsx,ts,tsx}", "scripts/**/*.{js,ts}", ".storybook/**/*.{js,ts}"],
  "ignoreDependencies": ["eslint-plugin-prettier", "eslint-plugin-storybook"]
}

직접 참조되지 않는 패키지는 Knip 설정 파일을 통해 예외처리했습니다.

Knip 정리 결과

3.2 주석 제거

주석 또한 프로젝트 코드에 많이 남아있었기 때문에 jscodeshift 기반 스크립트로 일괄 정리했습니다. 라이선스, ESLint/TypeScript 지시자 등 보존이 필요한 주석은 패턴 매칭으로 제외했습니다.

const PRESERVE_PATTERNS = [
  /@license/i,
  /^\s*!/,
  /eslint-(disable|enable|env|global)/i,
  /@ts-(ignore|expect-error|nocheck|check)/i,
  /@prettier-ignore/i,
  /webpackChunkName\s*:/,
  /^\s*global\s+/i,
];

const COMMENT_KEYS = ['comments', 'leadingComments', 'trailingComments', 'innerComments'];

function shouldPreserveComment(comment) {
  if (!comment || typeof comment.value !== 'string') {
    return false;
  }

  return PRESERVE_PATTERNS.some((pattern) => pattern.test(comment.value));
}

function filterCommentFields(node) {
  let changed = false;

  COMMENT_KEYS.forEach((key) => {
    const value = node[key];

    if (!Array.isArray(value) || value.length === 0) {
      return;
    }

    const nextValue = value.filter(shouldPreserveComment);

    if (nextValue.length !== value.length) {
      changed = true;
    }

    if (nextValue.length === 0) {
      delete node[key];
      return;
    }

    node[key] = nextValue;
  });

  return changed;
}

function walk(value) {
  let changed = false;

  if (Array.isArray(value)) {
    value.forEach((entry) => {
      if (walk(entry)) {
        changed = true;
      }
    });

    return changed;
  }

  if (!value || typeof value !== 'object') {
    return false;
  }

  if (typeof value.type === 'string') {
    if (filterCommentFields(value)) {
      changed = true;
    }
  }

  Object.keys(value).forEach((key) => {
    if (key === 'loc' || key === 'start' || key === 'end') {
      return;
    }

    if (walk(value[key])) {
      changed = true;
    }
  });

  return changed;
}

module.exports = function transform(fileInfo, api) {
  const j = api.jscodeshift;
  const root = j(fileInfo.source);
  const ast = root.get().value;

  const changed = walk(ast);

  if (!changed) {
    return null;
  }

  return root.toSource({
    quote: 'double',
    trailingComma: true,
    reuseWhitespace: true,
  });
};

4. Dependabot

정리하며

레거시 프로젝트를 인수받아 가장 먼저 한 일은 새로운 기능을 추가하는 것이 아니라, 지속 가능한 운영 기반을 마련하는 것이었습니다.

  • 환경 분리: Firebase 환경을 dev·stg·prd로 분리하고, 백엔드 의존성을 Server 중심으로 통일하여 변경 영향 범위를 명확하게 했습니다.
  • 코드 스타일 정비: Prettier와 ESLint를 도입해 일관된 코드 작성 규칙을 확립했습니다.
  • 미사용 자산 정리: Knip으로 미사용 패키지·코드·파일을 탐색하고, 주석 제거 스크립트로 불필요한 흔적을 정리했습니다.
  • 의존성 관리: Dependabot을 통해 의존성 업데이트를 자동화하여 보안과 최신성을 유지할 수 있게 했습니다.

이러한 작업은 당장 눈에 보이는 기능 개선은 아니지만, 이후 기능 개발과 유지보수의 복잡도를 줄이고 팀 전체의 생산성을 높이는 토대가 됩니다. 레거시를 단순히 “오래된 코드”로 보기보다, 안정적으로 운영할 수 있는 상태로 만드는 것이 첫 번째 과제임을 다시 한번 느꼈습니다.

공유:
Last updated on