eslint import 순서 정렬하기

이나리·2022년 9월 12일
19

프로젝트를 진행하면서 모듈을 그냥 가져오기만 하고 순서에 대한 정렬을 하지 않으니, 다음과 같은 코드를 마주쳤습니다.

import { useAppSelector, useAppDispatch } from '@store/hooks';
import * as React from 'react';
import { GoodsData } from '@typings/db';
import { styles } from './styles';
import { RiHeartAddLine } from 'react-icons/ri';
import {
  removeWishItem,
  selectIsWishItem,
  addWishItem,
} from '@store/slices/wishSlice';

// ...코드 생략

어떤가요? ...판단은 자유입니다.

import를 어떤 식으로 했는지가 코드가 돌아가는 데 중요한 부분은 아니지만, 순서없이 뒤죽박죽 되어 있으니 나중에 import 관련 코드만 봤을 때 그 흐름이 눈에 잘 들어오지 않았습니다.
이런 식으로 import를 계속 하다가는 다른 사람과 협업을 했을 때도 문제가 되기 때문에 개선을 시도해봤습니다.

문제는 프로젝트를 꽤 진행하던 도중에 발견한 사실이기 때문에 이 파일 하나만 그런 것이 아니고, 꽤 많은 파일이 저런 문제를 겪고 있기 때문에 많은 파일이 코드 외 흐름에서 변경점을 가져야 했는데요.

그래도 이번 기회에, 모듈 import 순서를 정렬하는 시간을 가져보면서 다음 프로젝트를 할 때는 이런 문제를 겪지 않아보려고 합니다.

eslint-plugin-import

가장 먼저 설치해야 할 패키지가 있습니다. eslint-plugin-import 입니다.
이 패키지를 설치하게 되면, 기본적으로 어떤 모듈인지에 따라 import 순서를 정할 수 있습니다.

eslint 에 기본적으로 내장된 sort-imports 를 활용할 수도 있지만, 저 같은 경우에는 제가 정해놓은 규칙이 있었기 때문에 그렇습니다.

규칙에 대해 간단히 설명하자면,

나만의 import 규칙

  1. path, fs 같은 내장 모듈은 항상 최상위에 존재해야 한다.
  2. 리액트 프로젝트를 하므로, react, react-dom, react-router-dom 과 같은 라이브러리는 다른 라이브러리보다 위에 존재해야 한다.
  3. 직접 작성한 ts, tsx 모듈은 외부 라이브러리 아래에 존재해야 하며, tsconfig path 옵션을 사용한 매핑 경로는 상대경로 모듈보다는 위에 위치해야 한다.

위의 규칙은 제가 사용할 규칙으로서, 꼭 위와 같이 정하지 않아도 됩니다. 코드를 작성하다 보면 내가 어떤 규칙으로 import를 할 것인지에 대한 선호도가 자연스럽게 생길 것입니다.

아래 내용은 위에 정해놓은 규칙을 기반으로, import/order 문서를 참고하여 설정했습니다.

groups

groups 옵션만 지정하더라도 최소한 절대경로 모듈과 상대경로 모듈이 뒤섞이는 경우는 방지할 수 있습니다. 이 옵션에 따로 지정하지 않은 모듈은 모두 동일한 순서를 가집니다.

"groups": ["builtin", "external", "internal"]

위에는 제가 설정한 groups 옵션입니다. 앞서 정해놓은 규칙 기억하시나요?

builtin 모듈은 내장 모듈을 뜻하므로 맨 앞에, 외부 라이브러리 모듈은 external 에 해당하므로 2번째입니다. tsconfig paths 옵션을 통해 매핑한 절대 경로는 internal 모듈로 해석되기 때문에, 3번째로 지정했습니다.

이렇게 하면, 이 매핑 경로에 해당하는 경로는 항상 외부 라이브러리 모듈보다는 아래 있고, 그 외 상대경로보다는 위에 위치할 수 있습니다.

그외 나머지 모듈을 지정하지 않은 것은 사실 대부분의 경로를 paths 매핑하여 사용하고, ./ 와 같이 현재 경로에서 불러오는 sibling 모듈 외에는 사용할 필요가 없었기 때문입니다.

pathGroups

이 옵션은 react, react-dom 과 같은 라이브러리 모듈이 external 모듈 중에 가장 상위에 올 수 있도록 하기 위해 지정한 옵션입니다.

리액트 컴포넌트를 사용할 때, 개인적으로 이 모듈들은 다른 라이브러리 모듈보다 상위에 있는 것이 코드를 읽는 흐름 측면에서 더 좋은 것 같아 이 방법을 선택했습니다.

"pathGroups": [
  { "pattern": "react", "group": "builtin", "position": "after" },
  { "pattern": "react-dom", "group": "builtin", "position": "after" }
]

이렇게 지정하게 되면, react, react-dom 모듈이 builtin 모듈 뒤에 오도록 함으로써, 다른 어떤 external 모듈보다 위에 위치하도록 할 수 있습니다.

{ "pattern": "react", "group": "external", "position": "before" },

첫번째 설정과 약간 다르게, 다른 external 모듈 앞에 위치하도록 해도 위와 동일한 효과를 볼 수 있습니다. 문법적으로 약간의 차이는 있지만 다른 모듈을 그 위의 위치로 변경하지 않는다면 크게 상관은 없습니다.

이때 pathGroupsExcludedImportTypes 옵션에 해당 모듈들도 추가해야 하는데요. 그렇지 않으면, pathGroups 에서 지정한 위치가 적용되지 않기 때문입니다.

pathGroupsExcludedImportTypes

이 옵션에 대해서는 사실 정확하게 어떻게 작동하는지 알지 못합니다. 공식문서 설명과 이것저것 실험해본 결과, 결과적으로는 pathGroups 에 정의한 경로가 적용될 수 있도록 해준다고 할 수 있을 것 같습니다.

문서에는 pathGroups 에 구성되지 않은 import types을 정의한다고 되어 있으며, 기본값은 ["builtin", "external", "object"] 입니다. 이 기본값 때문에, 그 중에서도 external 값 때문에 위에서 설정한 모듈의 위치가 적용되지 않는 것인데요.

짐작해보면, 이 옵션에 포함된 모듈은 pathGroups 옵션을 통해 그 순서를 변경했더라도 모듈이 갖고 있는 기본 순서가 변경되지 않는다고 볼 수 있습니다. 그래서 이 옵션에 들어있는 값과 상충된다면, pathGroups 에서 설정한 모듈의 위치는 의미가 없어집니다.

이 추론을 더 증명해보고자, 옵션의 기본값 중 하나인 builtin 모듈에 해당하는 path 모듈을 pathGroups 옵션에서 external after 로 위치를 설정해주고, 옵션이 기본값인 경우와 옵션에서 builtin 모듈을 제외하고 테스트 해봤습니다.

결과는 어땠을까요?

  1. 옵션이 기본값인 경우 - pathGroups 옵션 적용 안됨
  2. 옵션에서 builtin 모듈을 제외한 경우 - pathGroups 옵션 적용됨

저의 추론이 맞는 걸까요? (이 옵션에 대해 명확하게 설명해주실 분이 계셨으면 좋겠습니다.)

그럼 이제 이 옵션을 어떻게 적용해야 할까요?
공식 문서에는 해당 모듈을 추가하는 방법을 제시했습니다. 아래처럼 변경하면 이제 pathGroups 옵션은 잘 적용됩니다.

"pathGroupsExcludedImportTypes": ["react", "react-dom"]

방법은 이 외에도 여러가지가 있는 것 같습니다. 기본값에서 external 만 제외해도 되고, 아니면 빈 배열을 넣어줘도 pathGroups 옵션이 잘 실행됩니다. 여러 방법을 시도해보면서, 모듈이 원하는대로 위치하는지 테스트를 한번 해보시기 바랍니다.

그리고, 이때 같은 위치로 지정된 이들 모듈끼리의 순서는 어떻게 될까요? 먼저 지정된 것이 더 위에 위치합니다. 여기에서는 react가 react-dom 보다 위에 위치해야 하겠네요.

알파벳 순서 정렬

groups 에서 정한 모듈 순서대로 모듈을 import 해오더라도, 동일 위치에 있는 모듈 간에도 알파벳 순서대로 정렬이 필요합니다.

"alphabetize": {
  // 알파벳 순서 정렬 방식 (기본값: "ignore", "asc": 오름차순, "desc": 내림차순)
  "order": "asc", 
  
  // 알파벳 대소문자 구분하지 않음 (true: 대소문자 구분 무시, false(기본값): 대소문자 구분)
  "caseInsensitive": true 
}

간단한 예제 코드를 한번 볼까요?
axiosreact-icons/ri 는 모두 external 모듈입니다.

// order: asc
import axios from 'axios';
import { RiHeartAddLine } from 'react-icons/ri';

// order: desc
import { RiHeartAddLine } from 'react-icons/ri';
import axios from 'axios';

위의 코드는 대소문자를 구분하지 않았습니다. 구분하게 되면, A,B,C,...a,b,c.. 이런 식으로 대문자가 소문자보다 항상 먼저 와야 합니다.

결과

import * as React from 'react';
import { RiHeartAddLine } from 'react-icons/ri';
import { useAppDispatch, useAppSelector } from '@store/hooks';
import {
  addWishItem,
  removeWishItem,
  selectIsWishItem,
} from '@store/slices/wishSlice';
import { GoodsData } from '@typings/db';
import { styles } from './styles';

위의 설정들을 토대로 하면 원하는 대로 결과가 만들어지면서, 에러메세지도 발생하지 않습니다.
그러나 코드를 조금만 바꿔보면, 완벽하게 import 순서를 잡아주지 않았다는 걸 알 수 있습니다.

named export 로 가져온 모듈 멤버의 순서를 바꿀 경우, 에러메세지가 뜨지 않습니다. 이때 eslint에 내장된 sort-imports 플러그인을 error로 변경하여 처리해줘야 합니다.

sort-imports

아래 내용은 sort-imports 문서를 참고하여 작성했습니다.

"sort-imports": "error"

여기서 끝이 나면 좋겠지만, sort-imports 에도 기본 설정이 존재하기 때문에, 그 설정에 맞지 않으면 또 에러가 발생합니다. 기본 옵션 참고

이번에는 발생한 에러 메세지를 직접 보면서 처리해보겠습니다.

import * as React from 'react';
import { RiHeartAddLine } from 'react-icons/ri';
import { useAppDispatch, useAppSelector } from '@store/hooks';
// 1. Expected 'multiple' syntax before 'single' syntax.

import {
  addWishItem,
  removeWishItem,
  selectIsWishItem,
} from '@store/slices/wishSlice';
// 2. Imports should be sorted alphabetically.

import { GoodsData } from '@typings/db';
import { styles } from './styles';

1. Expected 'multiple' syntax before 'single' syntax.

sort-imports 기본 설정에서 memberSyntaxSortOrder 는 import 하려는 모듈에서도 member가 몇개인지에 따라 이 모듈의 위치를 정합니다. 기본값은 none, all, multiple, single 순입니다.

그런데 2번째 줄의 모듈의 멤버가 1개인데, 3번째 줄의 2개보다 앞서있게 되죠.
2번째 줄의 모듈은 external 모듈이고, 3번째 줄의 모듈은 internal 모듈입니다.
이때 이걸 무작정 바꾸게 되면, 앞서 정해놓은 import/order 규칙에 걸려서, internal 모듈이 external 모듈보다 앞에 있을 수 없다는 에러가 발생합니다.

바꿀 때마다 계속 각각의 에러에 걸리기 때문에, 단순히 모듈의 위치를 바꾸는 것으로는 해결할 수 없습니다.
멤버의 우선순위를 변경하는 방법 역시 불가합니다. 이건 현재 import/order 규칙이 적용된 상태이기 때문에 변경하기가 어렵습니다. 다른 데서 글자만 바꿔서 같은 에러 메세지를 발생시키기 때문입니다.

결국 import/ordersort-imports 의 규칙이 서로 충돌하는 상태라, 한쪽의 규칙을 우선하고, 다른 한쪽의 규칙은 무시해야 합니다.

이때는 sort-importsignoreDeclarationSort 옵션을 true 로 지정합니다.
import 하려는 모듈의 선언 순서 정렬을 무시할지 말지를 결정하는 옵션인데요. 기본값은 false로, 가져오려는 모듈 이름의 알파벳 순으로 정렬됩니다.

// ignoreDeclarationSort: false (기본값) - 아래 코드는 에러 발생 X
// ignoreDeclarationSort: true - 아래 코드는 에러 발생 X
import a from 'foo';
import b from 'bar';

// ignoreDeclarationSort: false (기본값) - 아래 코드는 에러 발생
// ignoreDeclarationSort: true - 아래 코드는 에러 발생 X
import b from 'foo';
import a from 'bar';

기본값인 false를 설정했을 땐, 알파벳 순서로 정렬된 첫번째 코드는 에러가 발생하지 않았지만, 그렇지 않은 두번째 코드는 에러가 발생했습니다.

반대로 옵션값을 true 로 변경하면 이제 두 코드는 모두 에러가 발생하지 않습니다.

이렇게 모듈의 선언 순서를 무시하게 되면, 위에 선언한 모듈의 이름이 앞서있다 하더라도 의미가 없어집니다. 예제를 보시면, a가 먼저 오든, b가 먼저 오든 에러가 발생하지 않죠? 이는 가져오는 멤버의 우선순위에도 구애받지 않게 해줍니다.

불러오려는 모듈의 기본적인 선언 순서(알파벳 순)를 무시함으로써, 앞서 import/ordergroups 옵션에 적용한 순서가 불러오려는 모듈의 가장 큰 우선순위를 갖고, 이후 알파벳 정렬 설정 방식에 따라 모듈이 정렬되도록 합니다.

이제 interal 모듈에서 가져오려는 멤버의 개수가 더 위에 위치해야 하는 exteranl 모듈에서 가져오려는 멤버의 개수보다 많아도 더이상 에러가 발생하지 않습니다.

이 옵션은 다음에 나올 2번째 에러메세지도 해결합니다.

2. Imports should be sorted alphabetically.

해결 방법은 찾았지만, 왜 에러가 발생했는지 그 원인을 찾아보겠습니다.

sort-imports 의 기본 설정은 사실 별도 설정하지 않아도 가져오려는 모듈의 이름을 모두 알파벳순으로 정렬하도록 지정되어 있습니다.

이 설정은 import/order 규칙에서 해결하지 못했던, 모듈 멤버 간의 정렬을 해주기도 하지만, 각 모듈 간에도 정렬될 것을 요구합니다.

import * as React from 'react';
import { RiHeartAddLine } from 'react-icons/ri';
import { useAppDispatch, useAppSelector } from '@store/hooks';
import {
  addWishItem,
  removeWishItem,
  selectIsWishItem,
} from '@store/slices/wishSlice';

위의 코드에서 addWishItem이 알파벳 순으로 더 위인데, useAppDispatch가 더 위에 위치하고 있는 것은 아무런 문제가 되지 않아야 합니다.

앞선 1번의 상황처럼, external 모듈이 internal 모듈이 항상 위에 있어야 하는 것처럼 같은 특성의 모듈, 즉 internal 모듈 간에도 순서(알파벳 순)가 존재하기 때문에 이 규칙이 적용된 이후 같은 모듈의 멤버들끼리 다시 정해진 알파벳 순서에 따라 정렬되어야 합니다.

그런데 기본적으로 불러오려는 모듈의 선언 순서가 알파벳 순서로 정렬될 것을 요구하는 sort-imports 는 이 규칙을 알지 못하기에 충돌이 발생합니다.

결국 한쪽의 규칙을 무시해야 합니다. 우선권은 모듈의 멤버가 아닌 불러오는 모듈의 특성 (builtin 모듈인지, external 모듈인지 등...)에 있으므로 sort-imports 의 기본 규칙을 무시하는 설정을 추가합니다.

추가적으로 ignoreCase 옵션을 true 로 설정하여 가져온 모듈의 이름의 대소문자 구분도 무시하도록 해주는 것도 좋습니다. A,B,C,...a,b,c... 보다는 A,a,B,b,C,c.... 순서로 가져오길 원하니까요.

// ignoreCase: false (기본값) - 아래 코드 에러 발생
// ignoreCase: true - 아래 코드 에러 발생 X
import a from 'foo';
import B from 'bar';

// ignoreCase: false (기본값) - 아래 코드 에러 발생 X
// ignoreCase: true - 아래 코드 에러 발생
import B from 'foo';
import a from 'bar';

마치며

import 규칙이란 것은 정해진 규격이 따로 없기에, 개발자의 설정 방식에 따라 많이 달라질 수도 있겠지만, 그래도 어느 정도의 패턴은 정해져 있다고 생각됩니다. 이를 테면, 절대경로는 상대경로 보다는 위에 위치해야 한다는 것?

개발자라면 자신이 선호하는 import 규칙이 존재할 겁니다. 설정을 하실 때 가능하면 먼저 규칙을 정해놓고, 문서를 참조하시는 것을 추천드립니다.

규칙을 정해놓으면, 문서를 참조하면서 어떤 식으로 설정을 해야 할지 머릿속으로 그림이 그려지기 때문에 도움이 많이 됩니다.

0개의 댓글