[Next.js 13] Emotion์ผ๋ก app router RSC๋ฅผ ๊ตฌ์ฑํด๋ณด์
Next13์ app router ๋ฐฉ์์์ ํ์ฌ Emotion์ ๊ณต์์ ์ผ๋ก ์ง์ํด์ฃผ์ง ์๋๋ค.๐ฅฒ
๊ทธ๋ ์ง๋ง Project์์ Emotion lib๋ฅผ ์ฌ์ฉํ๊ณ , css์์ฑ์ ์ฌ์ฉํ๊ณ , app router์ RSC๋ฅผ ์ฌ์ฉํ๊ณ ์ถ์ ์ฌ๋๋ค์ด ์์๊น ๋ด(๋ณธ์ธ ํฌํจ!)
๋ฐฉ๋ฒ์ ์๋ ค์ฃผ๋ คํ๋ค.
๋ค๋ค ์๋ค์ํผ Emotion์ css ์์ฑ์ ์ฌ์ฉํ๋ ค๋ฉด tsconfig.json์
"jsxImportSource": "@emotion/react",
์์ ๊ฐ์ ์์ฑ์ ๋ฃ์ด์ผ ํ๋ค.
๊ทธ๋ฌ๋ฉด ๋ชจ๋ ํ๊ทธ์ css๋ฅผ ๋ฃ์ ์ ์์ด ์๋์ฒ๋ผ ์์ฑํ ์ ์๊ฒ ๋๋ค.
import { css } from '@emotion/react';
<div
ref={sectionImageRef}
css={css`
width: 240px;
`}
>
<Image
src={'/pngwing.png'}
width={800}
height={450}
alt="main"
/>
</div>
๊ทธ๋ ์ง๋ง tsconfig์
"jsxImportSource": "@emotion/react",
์ ์์ฑ์ ๊ฐ์ง ํ๊ทธ๋ ๋ชจ๋ client ์ปดํฌ๋ํธ์ด๋ค.
์ด์ ๋ ์๋์ ๊ฐ์ด ํ๊ทธ์ css๊ฐ optional๋ก ๋ค์ด๊ฐ ๋ฒ๋ฆฐ๋ค.
์ด๋ ๊ฒ ๋๋ฉด app router ๋ฐฉ์์์ ์๋ฒ์ฌ์ด๋๋ ๋๋ง์ ์ ํ ํ์ง ๋ชปํ๊ฒ ๋๋ค.
์ปดํ์ผํ ๋ ์ ์ด์ emotionํ๊ทธ๋ฅผ ์ฌ์ฉํด ๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์
// ex) app/auth/signIn/page.tsx
// โ Error => use client๋ฅผ ๋ถ์ด์ธ์! ๋ผ๋ ์๋ฌ๋ฅผ ๋ฑ์
const SignIn = () => {
return <div>1234</div>
};
export default SignIn;
// โ
OK but No RSC๐
"use client";
const SignIn = () => {
return <div>1234</div>
};
export default SignIn;
์์ ๊ฐ์ ๊ธฐ๋ณธ์ ์ธ ํ๊ทธ์๋ "use client"๋ฅผ ๋ถ์ฌ์ค์ผ ํ๋ค.
๊ทธ๋ฌ๋ฉด Emotion์ ์ฌ์ฉํ๋ ํ๋ก์ ํธ๋ app router ๋ฐฉ์์ RSC๋ฅผ ํ์ฉํ์ง ๋ชปํ ๊น?
์๋๋ค! ๋ฐฉ๋ฒ์ ์๋ค :)
ํด๊ฒฐ ๋ฐฉ์
next.config.js์์ webpack ์ปค์คํ ์ค์ ์ ํ๋ ๊ฒ์ด๋ค.
// next.config.js
/** @type {import('next').NextConfig} */
const enableEmotionRSC = require('./enableEmotionRSC');
const nextConfig = enableEmotionRSC({
reactStrictMode: true,
experimental: { appDir: true },
swcMinify: true,
output: 'standalone',
async rewrites() {
return [
{
source: '/api/:path*',
destination: `http://${process.env.REACT_APP_SERVER_DOMAIN_URL}/api/:path*`
}
];
}
});
module.exports = nextConfig;
์๋ ๋ด nextConfig ์ค์ ์ด๋ค.
๋ค๋ฅธ ๊ฑฐ๋ ๋ฌด์ํ๊ณ enableEmotionRSC๋ฅผ ์ดํด๋ณด์.
// enableEmotionRSC.js
const path = require('path');
const includedDirs = [path.resolve(__dirname, 'src')];
module.exports = function enableEmotionRSC(nextConfig) {
return {
...nextConfig,
webpack: (config, { isServer }) => {
config.module.rules = config.module.rules.filter(
(rule) =>
!(rule.test && rule.test.test && rule.test.test('.tsx'))
);
config.module.rules.push({
test: /\.tsx?$/,
include: includedDirs,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, 'tsconfig.json')
}
}
]
});
if (isServer) {
config.resolve.alias['@emotion/react'] = 'react';
}
return config;
}
};
};
nextConfig๋ฅผ ์ธ์๋ก ๋ฐ๊ณ , ์ถ๊ฐ์ ์ผ๋ก webpack ์ค์ ์ ํด์ค๋ค.
webpack์ options์์ dev, isServer ๋ฑ๋ฑ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์๋ค.
๋๋ isServer ๋ฐ์ดํฐ๋ง ์ผ๋จ ํ์ํด์ ํด๋น ๊ฐ๋ง ๊ฐ์ ธ์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ts-loader๋ฅผ ์ฌ์ฉํด tsconfig๋ฅผ reslove ํด์ฃผ๊ณ ,
๊ทธ ๋ค์
if (isServer) {
config.resolve.alias['@emotion/react'] = 'react';
}
์์์ isServer๋ฅผ ๋ถ๊ธฐ๋ก ๊ฐ์ ์ค์ ํด ์ค๋ค
์๋ฒ์ฌ์ด๋๋ ๋๋ง(SSR)์ ํ ๋๋ @emotion/react์ ๊ฐ์ react๋ก ๋์ฒดํด ์ค๋ค๋ ์๋ฏธ์ด๋ค.
๋ฌผ๋ก ๋น์ฐํ๊ฒ๋ emotion์ ์ฌ์ฉํ๋ tsconfig.json์
"jsxImportSource": "@emotion/react",
์์ ๊ฐ์ ๊ฐ์ด ๋ค์ด๊ฐ ์์ด์ผ ํ๋ค.
๊ทธ๋ฌ๋ฉด emotion์ ํ์ฉํ๋ฉด์, app router ๋ฐฉ์์ RSC ํ๊ฒฝ์ ๊ตฌ์ฑํ ์ ์๊ฒ ๋๋ค.
์ ์ฒด ์ฝ๋๋ ์๋์ ๊ฐ๋ค.
์ถ๊ฐ์ ์ธ eslint์ค์ ์ ๋ณธ์ธ ์ ๋ง์ ๋ง๊ฒ ๋ณ๊ฒฝํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค!
// root/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsxImportSource": "@emotion/react",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"cypress/**/*",
"**/*.cy.ts",
".next/types/**/*.ts",
"enableEmotionRSC.js"
],
"exclude": ["node_modules"]
}
// root/enableEmotionRSC.js
const path = require('path');
const includedDirs = [path.resolve(__dirname, 'src')];
module.exports = function enableEmotionRSC(nextConfig) {
return {
...nextConfig,
webpack: (config, { isServer }) => {
config.module.rules = config.module.rules.filter(
(rule) =>
!(rule.test && rule.test.test && rule.test.test('.tsx'))
);
config.module.rules.push({
test: /\.tsx?$/,
include: includedDirs,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: path.resolve(__dirname, 'tsconfig.json')
}
}
]
});
if (isServer) {
config.resolve.alias['@emotion/react'] = 'react';
}
return config;
}
};
};
// root/next.config.js
/** @type {import('next').NextConfig} */
const enableEmotionRSC = require('./enableEmotionRSC');
const nextConfig = enableEmotionRSC({
reactStrictMode: true,
experimental: { appDir: true },
swcMinify: true,
output: 'standalone',
async rewrites() {
return [
{
source: '/api/:path*',
destination: `http://${process.env.REACT_APP_SERVER_DOMAIN_URL}/api/:path*`
}
];
}
});
module.exports = nextConfig;