Web Study/React

React 프로젝트 설정 (CRA + mobx + twin.macro)

지로원 2021. 3. 8. 19:16

새로운 프로젝트를 들어가기에 앞서 새로운 라이브러리를 적용해보기로 했다.

MobX는 Redux와 같이 상태관리를 목표로 하는 라이브러리인데 Redux 보다 훨씬 쉽고 코드도 간편하고 단점보다는 장점이 많다고 생각되어 최근에 작업하는 모든 프로젝트를 MobX로 작업하고 있다. 생각같아서는 순수 React Context를 사용하여 상태관리를 하고 싶은데 아직 관리할 수 있는 디테일(?)이 살짝 부족하다고 생각되고 귀찮기도 해서 미루는 중이다.

(MobX 와 Redux 의 차이점은 여기에 자세히 나와있다.)

twin.macro 라는 라이브러리인데 요즘 유행(?)하는 tailwind.css 와 emotion 혹은 styled-component를 합쳐놓은 라이브러리다.

tailwind.cssUtility-First 컨셉을 가진 CSS 프레임워크로 bootstrap이랑 어느정도 비슷하다고 생각하면 된다.

emotion 혹은 styled components 는 CSS in JS 라는 개념을 도입한 라이브러리다.

(더 자세한 설명은 직접 찾아보는게 빠르다) 

 

예전에는 리액트 개발을 위해서 css, sass, less 등의 css 파일을 분리해서 작성했고 그게 좋아보였지만

요즘에는 해당 파일들이 굳이 필요없다고 생각되고 괜히 프로젝트에 파일이 많아지면서 프로젝트 자체가 좀 지저분(?) 해보여서

tailwind와 styled-components 를 본격적으로 사용하게 된 것 같다.

styled-components는 사용한지 조금되었고 tailwind는 사용한지 얼마 되지 않았는데 각각의 장단점이 명확했고 

두가지를 따로 사용해본 경험은 많았지만 둘 다 동시에 사용해본 경험은 없던 와중에 각각이 필요한 부분이 미묘하게 다른데

둘 다 사용하면 훨씬 더 많은 케이스에 적용할 수 있을 것이라는 생각이 들었고 관련 케이스를 구글링하던 와중에 

twin.macro 라는 라이브러리를 발견했다. 처음은 항상 어렵지만 시간이 지나면 잘했다는 생각이 들 수 있게끔 잘 사용해보려고 한다.

그에 앞서 세팅을 정리해보려고 오랜만에 블로그 글을 쓴다!! 정말 오랜만에..... (작성하던 시리즈는 여러개인데 항상 중간에 관뒀었다...ㅠ)

 

자 이제 드가자~!

Chapter1

npx create-react-app react-twin-boilerplate

명령어를 통해 cra 기반 프로젝트를 생성한다.

프로젝트가 생성되면 우선 mobx 설정부터 해보자,

yarn add mobx mobx-react-lite

mobx-react 가 아닌 mobx-react-lite 을 설치한 이유는 어차피 훅 형태로 개발할거고 데코레이터 없이 사용하기 위해서이다. (Mobx 측에서도 데코레이터는 deprecated 되었다)

mobx를 성공적으로 추가했다면 store를 만들어보자.

간단하게 appStore를 만들고 title을 넣고 변경해보겠다.

Store를 만드는 방법은 여러가지가 있는데 3가지 방법으로 간단하게 만들어보겠다.

 

1. makeObservable을 이용한 class 형태의 store 

// stores/ClassAppStore.js
import { makeObservable, observable, action } from 'mobx'

class ClassAppStore {
  constructor() {
    makeObservable(this, {
      appInfo: observable,
      changeAppTitle: action
    })
  }
  appInfo = {
    title: 'Hello React'
  }

  changeAppTitle = (title) => {
    this.appInfo.title = title
  }
}

const classAppStore = new ClassAppStore()
export default classAppStore

2. makeAutoObservable을 이용한 class 형태의 store 

// stores/ClassAppStore2.js
import { makeAutoObservable } from 'mobx'

class ClassAppStore2 {
  constructor() {
    makeAutoObservable(this)
  }

  appInfo = {
    title: 'Hello React'
  }

  changeAppTitle = (title) => {
    this.appInfo.title = title
  }
}

const classAppStore2 = new ClassAppStore2()
export default classAppStore2

1번과 2번을 보면 2번이 훨씬 깔끔하다. 하지만 2번은 class에서 super 혹은 subclassed가 있을 경우 사용할 수 없다.

그러므로 두 방법 모두 알고 있는 것이 좋을 듯?

3. observable 을 이용한 function 형태의 store

// stores/FuncAppStore.js
import { observable } from 'mobx'

const funcAppStore = observable({
  appInfo: {
    title: 'Hello React'
  },
  changeAppTitle(title) {
    this.appInfo.title = title
  }
})

export default funcAppStore

 제일 간단하다. 하지만 이 방식은 내부적으로 클론을 하고 observable 상태로 만드는 것이기에 쓸데없는 과정이 하나 더 있다고 보면 된다.

또한 Proxy object 형태로 생성하기 때문에 콘솔을 찍어보면 굉장히 복잡한 형태로 나오는 것을 확인할 수 있다.

MobX 에서 가장 추천하는 방식은 2번이기 때문에 이 방법을 기본으로 사용하되 super 혹은 subclass 가 있을 경우에는 1번 방법을 사용하자! :)

 

store/index.js

// stores/index.js
import { createContext, useContext } from 'react'
import funcAppStore from 'stores/FuncAppStore'
import classAppStore from 'stores/ClassAppStore'
import classAppStore2 from 'stores/ClassAppStore2'

export const stores = {
  funcAppStore,
  classAppStore,
  classAppStore2
}

export const storesContext = createContext({
  ...stores
})

export const useStores = () => {
  const store = useContext(storesContext)
  if (!store) {
    throw new Error('useStore must be used within a StoreProvider')
  }
  return store
}

각기 다른 페이지 혹은 컴포넌트 들에서 각각에 필요한 store들을 각각 호출해서 사용할 수 있지만

stores라는 변수 한 곳에 모아놓고 그때그때 호출해서 사용할 수 있게 useStore을 만들어준다.

 

App.js

// src/App.js
import logo from './logo.svg'
import './App.css'
import { useStores } from 'stores'
import { observer } from 'mobx-react-lite'

const App = observer(() => {
  const { classAppStore, classAppStore2 } = useStores()
  const { appInfo: appInfo2, changeAppTitle: changeAppTitle2 } = classAppStore2
  const { appInfo, changeAppTitle } = classAppStore

  return (
    <div className='App'>
      <header className='App-header'>
        <img src={logo} className='App-logo' alt='logo' />
        <p>makeObservable class store</p>
        <div>Title: {appInfo.title}</div>
        <input onChange={(e) => changeAppTitle(e.currentTarget.value)} />
        <p>makeAutoObservable class store</p>
        <div>Title: {appInfo2.title}</div>
        <input onChange={(e) => changeAppTitle2(e.currentTarget.value)} />
      </header>
    </div>
  )
})

export default App

가볍게 App.js 를 바꾸고 테스트를 해보자 

초기

다음과 같은 화면이 나오고 input 값을 변경하게 되면, 

input 입력

위와 같은 화면으로 바뀐다. 물론 아래도 똑같이 동작한다.

그럼 이제 twin.macro를 세팅해보자!

 

Chapter2

yarn add twin.macro tailwindcss styled-components

tail.macro, tailwindcss, styled-components를 설치한다. 

그 다음 tailwindcss를 사용하기 위해서 twin.macro에서 GlobalStyles을 가져와서 적용해줘야 된다.

// src/GlobalStyles.js
import React from 'react'
import { GlobalStyles } from 'twin.macro'

export default function GlobalStylesComponent() {
  return <GlobalStyles />
}
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import GlobalStyles from './GlobalStyles'

ReactDOM.render(
  <React.StrictMode>
    <GlobalStyles />
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

굳이 이렇게 불편하게 한번 거쳐서 작업하는 이유는 styled-components 와 관련된 이슈때문이다.

그 후 babel macro 설정까지 해주면 twin.macro를 사용할 수 있다.

// package.json
"babelMacros": {
  "twin": {
    "preset": "styled-components",
    "styled": {
      "import": "default",
      "from": "styled-components"
    },
    "css": {
      "import": "css",
      "from": "styled-components"
    },
  }
},

원래는 @babel-core 와 babel-plugin-macros 라이브러리들을 설치해줘야 되지만 CRA에 기본적으로 포함되어있으니 추가로 설정만 해주면 된다. package.json 안에서 styled-components 에 대한 설정을 마무리하자.

 

마지막으로 tailwindcss 를 적용해보자!

// src/App.js
import logo from './logo.svg'
import './App.css'
import { useStores } from 'stores'
import { observer } from 'mobx-react-lite'
import tw, { styled } from 'twin.macro'

const Input = tw.input`border hover:border-black px-2 outline-none`
const PurpleInput = tw(Input)`border-purple-500 text-purple-700`
const StyledP = styled.p(({ isPurple }) => [`color: black;`, isPurple && tw`text-purple-700`])
const StyledDiv = styled.div`
  color: black;
  ${({ isPurple }) => isPurple && tw`text-purple-700`}
`

const App = observer(() => {
  const { classAppStore, classAppStore2 } = useStores()
  const { appInfo: appInfo2, changeAppTitle: changeAppTitle2 } = classAppStore2
  const { appInfo, changeAppTitle } = classAppStore

  return (
    <div className='App'>
      <header className='App-header'>
        <img src={logo} className='App-logo' alt='logo' />
        <p>makeObservable class store</p>
        <div tw={'my-2 font-bold'}>Title: {appInfo.title}</div>
        <input css={[tw`rounded-2xl hocus: (outline-none) text-black`]} onChange={(e) => changeAppTitle(e.currentTarget.value)} />
        <StyledP isPurple={false}>makeAutoObservable class store</StyledP>
        <StyledDiv isPurple>Title: {appInfo2.title}</StyledDiv>
        <PurpleInput onChange={(e) => changeAppTitle2(e.currentTarget.value)} />
      </header>
    </div>
  )
})

export default App

결과 화면

위와 같이 여러가지 방법을 사용해서 적용시킬 수 있다. 이밖에도 다양한 방법으로 자신한테 편하게 골라서 쓸 수 있다!!   

이로써 tailwindcss와 styled-components가 아주 잘 어우러지게 적용할 수 있었고 이로써 프로젝트에는 css파일이 필요없게 되었고 tailwindcss를 씀으로써 불필요하게 많은 코드도 사용할 필요가 없어졌다....!! 으메이징...😆

아 그리고 tailwind 설정파일을 정의하여 사전에 더욱 많은 스타일들을 정의할 수 있다. (프로젝트에서 빈번하게 쓰일수록 유용)

1. 처음부터 세팅 (tailwind doc페이지를 보면서 추가해나가면 됨.)

// {project Root}/tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {},
    },
  },
  plugins: [],
}

2. 풀세팅 파일 생성 (엄청난 설정파일이 하나 생성된다.)

npx tailwindcss-cli@latest init --full

그리고 아까 package.json 에 설정한 babelMacros 에 명시해주면 된다!

// 최종 package.json
{
  "name": "cra-twin-boilerplate",
  "version": "0.1.0",
  "private": true,
  "author": "zer01ne",
  "description": "cra-twin-boilerplate",
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "mobx": "^6.1.8",
    "mobx-react-lite": "^3.2.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.2",
    "styled-components": "^5.2.1",
    "tailwindcss": "^2.0.3",
    "twin.macro": "^2.3.0",
    "web-vitals": "^1.0.1"
  },
  "devDependencies": {
    "prettier": "^2.2.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "prettier": "prettier --write --config ./.prettierrc 'src/**/*.{js,jsx}'"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "babelMacros": {
    "twin": {
      "preset": "styled-components",
      "styled": {
        "import": "default",
        "from": "styled-components"
      },
      "css": {
        "import": "css",
        "from": "styled-components"
      }
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Fin.

 

 

 

아참, twin.macro가 좋은 이유는 정말 여러개가 있는데 이중에 하나는 잘못 썼으면 뭐가 틀렸는지, 어떤 옵션들이 있는지 보여준다. (그냥 tailwindcss만 쓰면 가끔 에러가 발생했을 때 찾기 어려울 수 있다) 

이 외에도 많은 기능이 더 있고 현재도 활발하게 개발중이니 땡큐! 하면서 잘 사용해보자! 

twin.macro 장점

 

정말 오랜만에 블로그 글을 쓰니까 뿌듯하긴한데 넘나 힘들다리...