리액트로 프로젝트를 시작하기 앞서 항상 서버 사이드 렌더링을 고려해 넥스트(Next.js
) 환경을 선택할 지, 아니면 그냥 클라이언트 사이드 렌더링 방식의 환경을 선택할 지 고민에 빠지곤 한다. 가급적이면 두 가지 방식 모두 혼용가능한 넥스트 프레임워크를 선택하는것이 확장성이 더 좋다라고 판단될 수 있겠지만, 개인적으로 굳이 서버 사이드 렌더링을 앞으로도 사용하지 않을 거라고 생각되는 상황에서 넥스트 프레임워크의 선택은 괜히 무거운 개발환경을 안고 가는 것이 아닐까란 생각도 종종 들곤한다.
때문에 이번엔 Flutter
를 사용해 구동하는 앱 환경에서 사용할 화면을 웹뷰(WebView
)를 통해 만들어야 하는 상황이었기에, CRA(Create-React-App)
CLI를 이용해서 개발환경을 설정하기로 선택했다.
사실 CRA
방식에서 내장된 기본 웹팩만 하더라도 넥스트 프레임워크의 CNA(Create-Next-App)
보다 용량이 더 큰 것은 함정이지만, 계속 가벼운 나만의 리액트 보일러 플레이트를 만들어야지! 라는 내면의 외침을 애써 무시한 나의 잘못이라 치고 넘어가도록 하자. (만들어두면 요긴하게 쓰겠지만 웹팩 공식문서를 읽어가며 필요한 기능을 하나씩 빼오기가 너무 귀찮두렵다) 언젠간 나만의 보일러 플레이트를 제작하는 포스팅을 꼭 올려야겠다.
다시 본론으로 돌아와서 본 프로젝트 환경은 다음의 명령어를 통해 리액트를 한다면 누구나 알고 있는 CRA
환경으로 구축했다. 추가로 타입스크립트를 사용하기 위해서 템플릿으로 타입스크립트를 지정해주었다.
npx create-react-app flutter-webview --template typescript
프로젝트에서 디렉토리를 페이지/레이아웃/컴포넌트 단위로 나누고 다시 세부 항목을 묶어서 정리하다보면 굉장히 뎁스가 깊어지는 경우가 많다. 이 경우에 다른 경로에서 만들어둔 컴포넌트나 훅스를 가지고 오는 경우 아래와 같이 import
문이 굉장히 지저분해지게 된다.
import SomeComponentName from '../../src/components/Navbar/NavTab;
사실 Auto-import
를 사용하는 입장에서 일일이 경로를 직접 치고 있지 않으니 그냥 무시하고 넘어가도 될 문제이다. 하지만 본인의 이상한 결벽증에 의해 길어지는 경로를 계속 쳐다보는 것이 너무나도 괴로웠다(?). 그래서 위와 같이 적는 상대경로를 조금 더 단축된 형태의 절대경로로 사용할 수 있는 방법이 없나 찾아본 결과 특정 디렉토리의 별칭을 지정해 훨씬 더 짧게 모듈이나 컴포넌트를 import
하고 export
하는 방법을 알게 되었다.
타입 스크립트를 템플릿으로 지정해 CRA
환경으로 리액트 프로젝트를 구성하면 타입 스크립트의 설정을 담아두고 있는 tsconfig.json
형식의 파일이 하나 생긴다. 해당 파일에서 타입 스크립트 관련 설정들을 입맛대로 만질 수 있다. 자세한 옵션 설정값은 공식문서에서 확인할 수 있다. 웬만한 옵션은 CRA
환경에서 알아서 포함하고 있기 때문에 자신이 평소 쓰는 기능이 누락되어 있다면 추가해주도록 하자. 나는 가급적 건들이지 않고 순정 그대로 쓰는 편이다.
해당 옵션들 중에서는 paths
라는 옵션이 있는데, 이는 baseUrl
을 기준으로 관련된 위치에 모듈 이름의 경로 매핑 목록을 지정할 수 있다. 여기서 우리는 경로의 시작지점을 설정해 길어지는 상대경로를 단축할 수 있음과 동시에 별도의 별칭을 지정해줄 수 있다. 예를 들면 다음과 같다.
{
"compilerOptions": {
"baseUrl" : "./src",
"paths" : {
"@components/*" : ["./components/*"],
"@hooks/*" : ["./hooks/*"],
},
...// 기타 타입스크립트 설정
}
}
위 옵션을 설명하자면,
baseUrl
: 경로의 시작점이 되는 메인 디렉토리이다. 위 옵션에서는 src
폴더로 지정되어있다. 따라서 paths
에서 시작하는 모든 경로는 src/...
의 형태를 내부적으로 가진다.
paths
: baseUrl
내부에 위치한 세부 디렉토리를 지정할 수 있다. 이때 @
를 사용하여 별칭(alias
)의 형태로 각 경로를 import/export
할 수 있다. 별칭에 해당하는 진짜 주소는 배열 자료형으로 전달하는데, 각 원소는 문자열이다. 배열이기 때문에 2개 이상의 주소를 매핑할 수 있다. 본 설정에서는 ./components/*
로 @components
가 매핑되어 있는데, 이는 src/components
아래에 있는 모든 파일들을 가리키는 지점이 된다.
이해하기 크게 어렵지 않다. baseUrl
을 기점으로 위치한 다른 디렉토리의 경로와 별칭을 매핑시켜주기만 하면 된다. 이처럼 설정을 마치면 처음에 상대경로로 모듈이나 컴포넌트를 들여올때와 달리 아래와 같이 간단하게 선언할 수 있다.
import SomeComponentName from '@components/Navbar/NavTab;
즉 tsconfig.json
파일에 baseUrl
이 기점으로 작용하기 때문에 현재 파일의 뎁스가 어디이건 상관없이 별칭(@...
)으로 접근할 수 있는 것이다. ../../
이 없어진 것만으로도 상당한 안구의 평화가 찾아옴을 느끼려하는 찰나...
CRA
환경의 리액트 프로젝트에서는 계속해서 다음과 같은 오류가 발생했다.
The following changes are being made to your tsconfig.json file:
- compilerOptions.paths must not be set (aliased imports are not supported)
짧은 영어지식으로 감히 해석을 해보건데, "너가 설정한 경로 관련 옵션이 있구나. 근데 안 해줄거니 돌아가"
라는 뉘앙스인 것 같다 (지극히 개인적인 주관이 많이 반영되어 있음). 문제는 해당 오류 문구가 npm start
명령어를 통해 웹펙의 devServer
에서 구동시킬 때 정말이지 반짝하고 순식간에 사라져 캐치하기가 힘들었다는 점이다. 그래서 넋 놓고 있다 마주하는 에러메시지는 항상 해당 경로로 부터 제대로 된 파일을 가져올 수 없다는 빨간 글씨 뿐이었다.
분명 이상한건 넥스트 프레임워크에서 타입스크립트를 사용할 때는 위의 설정만으로도 의도한 동작이 문제없이 잘 수행되었다는 점이다. 이미 넥스트에서 해 본 적이 있다는 알량한 자신감과 순식간에 사라지는 에러메시지를 간파하지 못해 괜한 시간을 컴퓨터 모니터와 눈씨름하며 보내다가, 결국 구글의 집단지성을 빌려보기로 했다.
아니나 다를까, 나 이외에도 유사한 문제를 겪은 사람들이 세계 방방곡곡에 널리 퍼져있었다. 스택오버플로우만 해도 유사한 질문이 많았고, 관련 문제를 이미 한국어로 친절하게 다룬 포스트도 많이 있었다.
결론부터 말하자면, CRA
환경에서 설정되는 웹팩의 설정 중에는 tsconfig.json
에 특정 조건이 있을 경우 구동 시점에 이를 초기화하는 이슈가 있었다. 하필이면 위에서 지정한 paths
속성은 CRA
의 웹팩이 이를 받아들이지 못해 초기 생성시 만들어지는 tsconfig.json
파일로 돌아가게 되는 것이다. 때문에 우리가 정성들여 만든 경로 파일은 실행시점에서 깡그리 무시되고, 그 여파로 정상적인 파일 import/export
가 불가능했던 것이다.
이 문제를 근본적으로 다루기 위해서는 사실 초기 세팅되는 웹팩을 뜯어서 고쳐야 한다. 웹팩에서 해당 옵션에 대해 리미트가 걸려있기 때문에 직접 이 부분에 대한 설정을 변경해주면 된다. 그렇지만 CRA
환경으로 구축된 리액트는 기본 웹팩 설정을 건드리는 것이 상당히 까다롭다. 왜냐하면 CRA
에서 만들어지는 웹팩은 매우 수줍음이 많기 때문에 우리 프로젝트 구조에서 보이지가 않기 때문이다!
package.json
에 보면 react-script
를 통해 우리의 리액트 프로젝트를 실행/빌드하는 관련 명령어들이 기입되어 있는데 이 부분에는 eject
라는 명령어도 있다. 해당 명령어가 바로 수줍음쟁이 웹팩을 우리 프로젝트단으로 끌어오는 명령어다. 하지만 eject
를 하면 더 이상 돌이킬 수 없다는 문제점이 있다. 즉 해당 명령어를 실행하는 시점부터는 우리 프로젝트와 관련된 모든 의존성을 직접 관리해야 하는 문제가 생긴다. 뭐 사실 이 부분이 문제라고 할 것 까지는 없다만, 웹팩을 비롯한 ESLint/Babel
등의 온갖 설정 파일이 우후죽순 쏟아져 나오게 될 것이고 만약 이러한 설정에 익숙하지 않은 개발자라면 오히려 커스텀하는데 상당한 시간이 소요될 것이다. 따라서 eject
는 해당 시점의 설정파일을 내가 제대로 관리할 수 있고, 이로 인해 CRA
환경에서보다 상당한 이점을 얻을 수 있다는 확신이 있을때 진행하는 것이 좋다.
나의 상황은 단순히 tsconfig.json
에 paths
속성만 적용시키면 되는 문제이고, 또한 방대한 양의 웹팩 설정을 일일이 뒤져가며 해당 작업을 수행하는 것은 과도한 소요 시간이 걸릴 것이라고 판단했다. 때문에 eject
를 사용하지 않고도 CRA
환경에서의 웹팩 설정을 커스텀할 수 있는 방안을 찾아보았다.
사실 CRA
는 리액트 생태계에서 널리 쓰이는 CLI 도구이고 때문에 관련 문제가 어느정도는 오래전부터 화두에 올랐기 때문에 이를 해결하고자 하는 시도는 여러 라이브러리 형태로 등장한 바 있다.
그 중에서도 내가 사용한 방법은 craco
라고 불리는 라이브러리이다. 이는 Create React App Configuration Override
의 앞 글자만 따와 구성한 이름으로, 이름부터가 우리가 처한 문제를 해결할 수 있음을 직접적으로 전달하고 있다. 그 외에도 react-app-rewired
라는 라이브러리도 있으니 관심이 있다면 검색해볼 것을 추천한다. 다만 2021년을 기준으로 최근까지 지속적인 관리가 이루어지고 있는 것은 craco
이다.
1. 의존성 설치
먼저 craco
라이브러리를 우리 프로젝트에 설치해주자. 해당 라이브러리를 정상적으로 사용하기 위해서는 라이브러리와 이를 위한 플러그인, 총 2개의 의존성 설치가 필요하다.
npm install @craco/craco // 라이브러리
npm install -D craco-alias // 플러그인
2. tsconfig.paths.json
그리고 위에서 tsconfig.json
파일에 직접 지정해주었던 경로 관련 설정을 또 하나의 외부파일로 분리해줄 것이다. 해당 파일을 craco
에게 처리하도록 넘겨주고, craco
는 리액트 실행 시 웹팩 기본 설정에게 이 옵션을 제대로 인식할 수 있도록 전달하는 역할을 수행하게 된다. 따라서 경로 옵션만 담고있는 tsconfig.paths.json
파일을 tsconfig.json
파일과 동일 레벨에서 하나 만들어주도록 하자. 보통 설정파일은 루트폴더 밑 최상위에 위치하는것이 일반적이다. 별도 파일로 분리만 되었을 뿐 내용은 동일하다.
// tsconfig.paths.json
{
"compilerOptions": {
"baseUrl" : "./src",
"paths" : {
"@components/*" : ["./components/*"],
"@hooks/*" : ["./hooks/*"],
}
}
}
경로 옵션 설정이 외부로 분리되었기 때문에 이를 다시 메인 tsconfig.json
에서 인식할 수 있도록 지정해주어야 한다. 이는 extends
키워드를 통해 다른 파일의 설정을 상속받는 것으로 해결한다.
// tsconfig.json
{
"extends": "./tsconfig.paths.json",
"compilerOptions": {
"target": "es5",
...// webpack 기본 tsconfig.json 설정
}
}
타입 스크립트 설정은 이것으로 마무리할 수 있다.
3. craco.config.js
이제는 앞서 언급했던 바와 같이, 해당 타입 스크립트 설정을 craco
가 다룰 수 있도록 만들어주어야 한다. 이는 craco.config.js
라는 해당 라이브러리 설정 파일을 통해 작업할 수 있다.
const CracoAlias = require("craco-alias");
module.exports = {
plugins: [
{
plugin: CracoAlias,
options: {
source: 'tsconfig',
baseUrl: "./src",
tsConfigPath: "tsconfig.paths.json",
},
}
]
};
craco
라이브러리 설정에서 웹팩을 위한 자동 alias
생성 플러그인 craco-alias
를 지정해주고 있다. 우리가 필요한 기능은 해당 플러그인이 전부 지원하기 때문에 별다른 설정을 건들지 않고 있다. 이 외에도 craco
에서는 웹팩을 비롯한 다양한 설정파일을 건들 수 있다. 자세한 점은 npm 문서를 참고하자.
플러그인에서 지정할 수 있는 options
에는 다음의 값들이 있다. 본인은 3개의 프로퍼티만 지정해주었지만, 필요하다면 다른 프로퍼티 역시 사용할 수 있다. 각자의 판단하에 필요한 기능을 사용하도록 하자.
source
: options | jsconfig | tsconfig
중 하나, 디폴트는 options
이지만 타입스크립트를 사용한다면 tsconfig
를 선택
baseUrl
: 타입 스크립트 경로에서 기본 출발점이 되는 경로와 동일
aliases
: source: 'options'
일때만 필요한 프로퍼티로, 별칭 이름과 경로를 가진 객체를 지정
tsConfigPath
: source: 'tsconfig'
일때 지정하는 프로퍼티로, 여기선 우리가 만든 경로 관련 tsconfig.paths.json
파일을 지정
filter
: 필터링 함수를 넘겨주며, 별칭 중 특정 기준으로 사용할 것과 사용하지 않을 것을 필터링하고자 할 때 사용
unsafeAllowModulesOutsideOfSrc
: ./src
디렉토리 외부에 있는 모듈 역시 import/export
를 가능하게 하는 설정으로, 이는 기본 웹팩 설정의 ModuleScopePlugin
과 상충되는 옵션이기에 사용 시 판단근거 마련이 필요
debug
: 디버그 관련 정보를 로그로 출력
4. pakage.json
마지막으로 pakage.json
의 scripts
부분을 수정해주어야 한다. 지금까지 한 작업은 모두 craco
라이브러리가 커스텀 설정을 제대로 인식하게 하기 위한 사전 작업이었다. 따라서 실제로 리액트 애플리케이션을 실행/빌드 시에 해당 설정 정보를 가지고 craco
를 통해 실행 또는 빌드가 되도록 명령어를 바꾸어주어야 한다. 이때 eject
는 그대로 react-scripts
에 의해 처리되도록 놔둔다.
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
위 과정을 모두 마치고 npm start
를 실행한다면 기존과 달리 정상적으로 import/export
가 별칭을 통해 이루어지는 것을 확인할 수 있다.
만약
npm start
를 통해 개발서버가 돌아가고 있는 상황에서tsconfig.paths.json
을 수정하는 경우엔 웹팩에서 제공하는hot-reloading/live-reloading
가craco
가 관리하는 영역에는 적용되지 않기 때문에 즉각 반영되지 않는다. 때문에 개발서버를 한 번 종료시키고 재부팅 시켜야 한다.
CRA
환경 역시 계속 발전을 거듭해오면서 개발에 필요한 기능은 대부분 갖추고 있고, 프로덕션 배포에 있어서도 상당 부분을 지원하고 있다고 생각한다. 그렇지만 세세한 설정 및 커스텀을 위해서는 CRA
를 뜯어보거나 설정을 다시 만져야 하는 경우가 생길 수 있다. 해당 작업이 위 사례처럼 간단한 경우엔 외부 라이브러리의 도움을 받을 수 있을 것이고, 만약 대대적인 수술이 필요하다면 eject
역시 고려해볼만 하다.
또 다른 해결 방안으로는 CRA
환경이 아닌 손수 제작한 보일러 플레이트를 만드는 것이다. 즉 맨땅에서부터 하나씩 쌓아올라가는 것인데, 웹팩을 비롯한 설정파일을 다루는 것에 익숙하다면 아마 개인마다 적합한 버전의 보일러 플레이트를 하나쯤은 가지고 있지 않을까 싶다. 어떤 선택이 되었든간에 처음 시작하는 입장이라면 설정에만 상당한 작업 소요가 있지 않을까 생각한다. (때문에 나는 아직 시도를 못하고 있다)
문득 문제를 해결하고 돌이켜보니, 해당 프로젝트에서는 단 3개의 컴포넌트만 사용한다는 것을 깨달았다. 이 경우엔 깊어지는 뎁스를 고민할 필요가 사실 없다. 지나친 과욕은 언제나 화를 부르는 법! 무작정 코딩을 시작하는 것이 아니라 항상 생각해보고 효율적인 코딩을 하는 것이 정말 중요한 것 같다.
어떤 문제를 해결하기 위해 코딩 이외의 솔루션이 항상 존재할 수 있다는 것을 염두해두면 좋을 것 같다. 가급적이면 코딩을 안 하거나 적게하는게 최선의 솔루션이 될 수 있다고 말하는 개발자 분들을 접할 수 있는데, 어떤 상황과 느낌을 말하는 건지 점점 가까이 다가가는 기분이 든다. 물론 공부 목적에서는 이러한 삽질이 여전히 많은 도움이 되고 있으니 허투루 낭비한 시간은 아니라고 생각한다.