사이드로 진행하는 프로젝트에서, react-native를 사용한 앱을 만드는 토이 프로젝트를 진행했습니다. 이번에도 역시 주저없이 react와 typescript를 이용하여 앱을 만들고자 했고, create-react-native-app을 이용해서 App base를 만들고자 했습니다.

추가적으로 create-react-native-app에는 expo라는 sdk를 내장하고 있습니다. expo는 react-native에 필요한 모듈들을 기본적으로 내장하고 있으며, 디버깅 또는 watch file들을 통해서 앱을 좀 더 쉽게 만들 수 있도록 도와줍니다. (실례로 font 설정이나 asset 설정이 간단했습니다. - 제가 만들고자 하는 앱은 정해진 font를 쓰고자 했습니다.)

그러나, expo자체 모듈에 대한 용량도 있기 때문에, 이후 expokit을 ejecting하게 되면, 용량 자체가 커집니다. 이는 ejecting시에 선택적으로 expokit을 설치할 수 있기 때문에, 만약 사용하지 않으시려면 expo관련 설정들은 사용하지 않으시는 것이 좋습니다.

또한.. expo 자체에 에러들도 많았습니다.. 이는 상당히 큰 문제였는데, firebase 관련 모듈에 대한 충돌도 있었고, iOS에서 빌드되던 앱이 Android에서는 빌드되지 않는 문제도 상당했습니다. (그러나, 이 부분들은 expo 30버전에서 수정되었습니다. 기본적으로 expo 30버전을 사용하시는 것을 권장합니다.)

create-react-native-app 설치

이전에는 create-react-native-app을 npm으로 설치하였으나, 현재 create-react-native-app 버전은 expo-cli와 통합된 상태입니다. 따라서, expo-cli를 npm으로 전역설치 하시면 됩니다.

sudo npm install -g expo-cli

프로젝트 생성

expo-cli를 전역으로 설치하셨기 때문에, expo init 커맨드를 이용하여 실행하면 됩니다.

cd path/to/project/root
expo init MyProject

typescript, definetlyTypes 설치

expo init으로 설치한 프로젝트 내에는 현재 javascript로만 설정이 되어 있습니다. typescript를 적용하기 위해, 프로젝트 폴더 내에서 typescript 및 definetlyTypes를 같이 설치해 줍니다.

yarn add typescript
yarn add @types/react @types/react-native @types/expo --dev
# or
npm i typescript --save
npm i @types/react @types/react-native @types/expo --save-dev

tsconfig.json / tslint.json 설정

typescript compile을 위한 tsconfig 및 linting을 위한 설정을 할 차례입니다. 상황에 맞게 바꿔서 쓰시면 됩니다. 개인적으로 저는 이렇게 설정했습니다. tsconfig.json 및 tslint.json은 프로젝트 root 경로에 설정해줍니다.

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es2015",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "esnext",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    "jsx": "react-native",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "outFile": "./main.js",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist",                        /* Redirect output structure to the directory. */
    "rootDir": ".",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
    "forceConsistentCasingInFileNames": true,
    /* Strict Type-Checking Options */
    "strict": false,                           /* Enable all strict type-checking options. */
    "noImplicitAny": false,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": false,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
    "suppressImplicitAnyIndexErrors": true,

    /* Additional Checks */
    "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
    "skipLibCheck": true,

    /* Module Resolution Options */
    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": ".",                       /* Base directory to resolve non-absolute module names. */
    "paths": {
      "components/*": ["./src/components/*"],
      "constants/*": ["./src/constants/*"],
      "screen/*": ["./src/screen/*"],
      "assets/*": ["./assets/*"],
      "stores/*": ["./src/stores/*"],
      "utils/*": ["./src/utils/*"],
      "models/*": ["./src/models/*"]
    },                          /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "./",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "./",                       /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
    
    /* Experimental Options */
    "experimentalDecorators": true        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  },
  "include": [
    "./src/**/*",
  ],
  "exclude": [
    "App.js",
    "App.test.js",
    "dist",
    "node_modules"
  ],
  "compileOnSave": false
}

한가지 특이사항이 있다면, 저는 import시에 path를 alias 형태로 관리하는 것을 선호하기 때문에, paths 설정에서는 주로 컴포넌트를 다루는 components 폴더와, 페이지를 담당하는 screens로 나누어서 관리하고자 합니다. 나머지 설정은 편하신대로 설정해주시면 될것 같습니다.

tslint는 linting을 위해서 설정하나, 설정하지 않으셔도 무관합니다. 저는 따로 linting을 설정하는데, 주의하실 부분은 typescript를 사용할때는 세미콜론(;)을 사용하지 않도록 설정을 해놓았습니다…..

{
  "rules": {
    "member-access": false,
    "member-ordering": [
      true,
      "public-before-private",
      "static-before-instance",
      "variables-before-functions"
    ],
    "no-any": false,
    "no-inferrable-types": [false],
    "no-internal-module": true,
    "no-var-requires": false,
    "typedef": [false],
    "typedef-whitespace": [
      true, {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }, {
        "call-signature": "space",
        "index-signature": "space",
        "parameter": "space",
        "property-declaration": "space",
        "variable-declaration": "space"
      }
    ],
    "ban": false,
    "curly": false,
    "forin": true,
    "label-position": true,
    "no-arg": true,
    "no-bitwise": true,
    "no-conditional-assignment": true,
    "no-console": [
      true,
      "debug",
      "info",
      "time",
      "timeEnd",
      "trace"
    ],
    "no-construct": true,
    "no-debugger": true,
    "no-duplicate-variable": true,
    "no-empty": true,
    "no-eval": true,
    "no-null-keyword": false,
    "no-shadowed-variable": true,
    "no-string-literal": true,
    "no-switch-case-fall-through": true,
    "no-unused-expression": true,
    "no-use-before-declare": true,
    "no-var-keyword": true,
    "radix": true,
    "switch-default": true,
    "triple-equals": [
      true,
      "allow-undefined-check"
    ],
    "eofline": false,
    "indent": [
      true,
      "spaces"
    ],
    "max-line-length": [
      true,
      150
    ],
    "no-require-imports": false,
    "no-trailing-whitespace": false,
    "object-literal-sort-keys": false,
    "trailing-comma": [
      true, {
        "multiline": "always",
        "singleline": "never"
      }
    ],
    "align": [true],
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "interface-name": [false],
    "jsdoc-format": true,
    "no-consecutive-blank-lines": [true],
    "no-parameter-properties": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-finally",
      "check-whitespace"
    ],
    "quotemark": [
      true,
      "single",
      "avoid-escape"
    ],
    "semicolon": [
      true,
      "never"
    ],
    "variable-name": [
      true,
      // "check-format",
      "allow-leading-underscore",
      "ban-keywords"
    ],
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ]
  }
}

만약 세미콜론을 필수로 넣고 싶으시면, semicolon설정을 false로 바꿔주시면 됩니다 :) react-native + typescript의 기본적인 설정만 알아보았습니다. 이제 스캐폴딩 후에 react app에 대한 개발을 진행해볼까 합니다 :)