네트워크엔지니어 주제에 이것저것 해보고 싶은게 많다.
백엔드는 NestJS를 사용을 하고있고, 앞으로도 그럴 것 같다. 프론트엔트는 React 경험이 있지만, 개인프로 젝트 목적상 SEO 가 가능해야하여 Next를 사용해 보기로 했다.
하여,
Typescript+Next+Emotion+Tailwind+Twin.macro+apollo
로 개발환경을 구축하려 한다.
Nextjs Github에는 Nextjs와의 다양한 조합의 예제들이 있다. 그 중 아래의 Boilerplate 사용한다.
https://github.com/vercel/next.js/tree/master/examples/with-typescript-eslint-jest
yarn create next-app --example with-typescript-eslint-jest test-next
쉬운 관리, 기존의 react와 유사한 디렉토리 구조로 변경하기 위해 pages폴더를 src폴더 바로 아래로 이동한다.
mkdir src && mv ./pages ./src/pages
import 시 ../../../
와 같은 복잡한 형태를 피하기 위해 절대경로를 사용한다.
yarn add -D babel-plugin-module-resolver
{
"presets": ["next/babel"],
"plugins": [
"@emotion/babel-plugin",
"babel-plugin-macros",
[
"module-resolver",
{
"root": ["."],
"alias": {
"~": "./src"
}
}
]
]
}
json
, js
, cjs
, mjs
확장자로 사용 가능하위 디렉토리에 override 할 별도의 설정이 필요하지 않으므로 .babelrc.json
로 설정
mv .babelrc .babelrc.json
import 시 src/
를 ~/
로 표현 가능
"compilerOptions": {
... 생략 ...
"baseUrl": ".",
"paths": { "~/*": ["src/*"] }
},
.
├── .babelrc.json
├── .eslintignore
├── .eslintrc.json
├── .git
├── .gitignore
├── .prettierignore
├── .prettierrc
├── README.md
├── jest.config.js
├── next-env.d.ts
├── node_modules
├── package.json
├── public
├── src
├── test
├── tsconfig.json
└── yarn.lock
기존에 Styled-components
는 써본터라 다른 CSS 프레임워크를 써보려는중, Tailwind CSS
에 관심이 갔다. Tailwind의 경우 CSS를 깔끔하고 직관적으로 작성 가능한 장점이 있다. 하지만 이 역시도 코드안에서 className을 작성하다보면 지저분해지는 느낌을 받게 되었다.
그래서 Tailwind를 사용하면서 CSS-in-JS 와 같이 컴포넌트 별로 CSS를 작성해 분할 수 없을까 찾아보던 중 Twin.macro
를 알게 되었다.
Twin.macro를 통해 Tailwind CSS와 emotion을 통합하여 사용한다.
yarn add @emotion/react @emotion/styled @emotion/css @emotion/server
yarn add -D twin.macro tailwindcss postcss@latest autoprefixer@latest @emotion/babel-plugin babel-plugin-macros
npx tailwindcss-cli@latest init -p
src/pages/_app.tsx
import { FC } from 'react'
import Head from 'next/head'
import { GlobalStyles } from 'twin.macro'
import { AppProps } from 'next/app'
const App: FC<AppProps>= ({ Component, pageProps }: AppProps) => (
<>
<Head>
<title>
Nextjs App with TypeScript, ESlint, Jest, Emotion, Tailwind and Twin
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GlobalStyles />
<Component {...pageProps} />
</>
)
export default App
JSX.Element의 Props의 형태가 명시되어 있지 않으면 오류를 내도록 ESlint 규칙을 변경
.esllintrc.json
"rules": {
... 생략 ...
~~"react/prop-types": 0,~~ // 삭제
"@typescript-eslint/explicit-module-boundary-types": "off",
... 생략 ...
src/pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from 'next/document'
import { extractCritical } from '@emotion/server'
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
const page = await ctx.renderPage()
const styles = extractCritical(page.html)
return {
...initialProps,
...page,
styles: (
<>
{initialProps.styles}
<style
data-emotion-css={styles.ids.join(' ')}
dangerouslySetInnerHTML={{ __html: styles.css }}
/>
</>
),
}
}
render() {
return (
<Html lang="ko-kr">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
package.json
"lint-staged": {
... 생략 ...
},
"babelMacros": {
"twin": {
"preset": "emotion"
}
},
"dependencies": {
... 생략 ...
.babelrc.json
{
"presets": [
[
"next/babel",
{
"preset-react": {
"runtime": "automatic",
"importSource": "@emotion/react"
}
}
]
],
"plugins": [
... 생략 ...
]
}
twin.d.ts
import 'twin.macro'
import styledImport from '@emotion/styled'
import { css as cssImport } from '@emotion/react'
declare module 'twin.macro' {
// The styled and css imports
const styled: typeof styledImport
const css: typeof cssImport
}
declare module 'react' {
// The css prop
interface HTMLAttributes<T> extends DOMAttributes<T> {
css?: CSSProp
}
// The inline svg css prop
interface SVGProps<T> extends SVGProps<SVGSVGElement> {
css?: CSSProp
}
}
// The 'as' prop on styled components
declare global {
namespace JSX {
interface IntrinsicAttributes<T> extends DOMAttributes<T> {
as?: string
}
}
}
tsconfig.json
{
"compilerOptions": {
...
},
"files": ["twin.d.ts"], // 추가
"exclude": ["node_modules", ".next", "out"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]
}
yarn add graphql @apollo/client
apollo client 사용을 위한 apollo 설정
src/apollo.ts
import {
ApolloClient,
InMemoryCache,
createHttpLink,
from,
} from '@apollo/client'
import { onError } from '@apollo/client/link/error'
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql',
})
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${JSON.stringify(
locations
)}, Path: ${path}`
)
)
if (networkError) console.log(`[Network error]: ${networkError}`)
})
export const client = new ApolloClient({
link: from([errorLink, httpLink]),
cache: new InMemoryCache(),
})
apollo client 사용을 위한 ApolloProvider 지정
src/pages/_app.tsx
import { FC } from 'react'
import { AppProps } from 'next/app'
import Head from 'next/head'
import { ApolloProvider } from '@apollo/client' // 추가
import { GlobalStyles, css } from 'twin.macro'
import { Global } from '@emotion/react'
import { client } from '~/apollo' // 추가
const App: FC<AppProps> = ({ Component, pageProps }: AppProps) => (
<>
<ApolloProvider client={client}> // 추가
<Head>
... 생략 ...
<Component {...pageProps} />
</ApolloProvider> // 추가
</>
)
...생략...
개인프로젝트를 진행할 프론트엔트 개발환경은 이렇게 진행할 예정🎉
참고
https://github.com/vercel/next.js/tree/canary/examples/with-typescript-eslint-jest
https://github.com/Dylan-Oleary/next-apollo-tailwind-typescript
https://github.com/ben-rogerson/twin.examples/tree/master/next-emotion