Communitygithub.com

Geruyang/agent-skills-library

Expo에서 react-native-css와 NativeWind v5를 사용하여 Tailwind CSS v4를 설정하여 iOS, Android, 웹에서 일관된 스타일링을 구현합니다.

agent-skills-library란 무엇인가요?

이 스킬은 Expo 프로젝트에서 Tailwind CSS v4, react-native-css, NativeWind v5를 사용하여 iOS, Android, 웹 플랫폼에서 일관된 유니버설 스타일링을 설정하는 방법을 제공합니다. Tailwind v3/v4와 NativeWind v5의 주요 차이점을 반영하여 Babel 설정 없이 CSS 우선 방식으로 구성하며, PostCSS(@tailwindcss/postcss)를 통해 처리합니다. 또한 react-native-css의 `useCssElement` 훅을 사용하여 View, Text, ScrollView, Pressable 등 기본 React Native 컴포넌트를 래핑하여 className 속성을 지원하는 재사용 가능한 컴포넌트를 만드는 방법을 포함합니다. CSS 변수를 활용한 커스텀 테마, 플랫폼별 스타일링(iOS/Android), Apple 시스템 컬러 적용 등을 지원하며, Metro 설정(withNativewind)과 PostCSS 설정 방법을 구체적으로 안내합니다. Tailwind v4의 `@import tailwindcss/…` 구문과 `@theme`을 사용한 CSS 기반 테마 설정, JavaScript에서 CSS 변수를 사용하는 `useCSSVariable` 훅 등 실무에 필요한 핵심 팁을 제공합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Geruyang/agent-skills-library/tree/HEAD/original-skills/x-openai-expo-tailwind-setup-db649d8

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Expo에서 react-native-css를 사용한 Tailwind CSS 설정

이 가이드는 Expo에서 react-native-css와 NativeWind v5를 사용하여 Tailwind CSS v4를 설정하여 iOS, Android, 웹에서 일관된 스타일링을 구현하는 방법을 다룹니다.

개요

이 설정은 다음을 사용합니다:

  • Tailwind CSS v4 - 최신 CSS 우선 구성
  • react-native-css - React Native용 CSS 런타임
  • NativeWind v5 - React Native에서 Tailwind를 위한 Metro 변환기
  • @tailwindcss/postcss - Tailwind v4용 PostCSS 플러그인

설치

# 의존성 설치
npx expo install tailwindcss@^4 [email protected] [email protected] @tailwindcss/postcss tailwind-merge clsx

lightningcss 호환성을 위해 resolutions 추가:

// package.json
{
  "resolutions": {
    "lightningcss": "1.30.1"
  }
}
  • Expo에서는 lightningcss를 사용하므로 autoprefixer가 필요하지 않습니다.
  • postcss는 Expo에 기본 포함되어 있습니다.

설정 파일

Metro 설정

metro.config.js를 생성하거나 업데이트:

// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativewind } = require("nativewind/metro");

/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);

module.exports = withNativewind(config, {
  // 인라인 변수는 CSS 변수의 PlatformColor를 깨뜨림
  inlineVariables: false,
  // className 지원을 수동으로 추가
  globalClassNamePolyfill: false,
});

PostCSS 설정

postcss.config.mjs 생성:

// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

글로벌 CSS

src/global.css 생성:

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";

/* 플랫폼별 폰트 패밀리 */
@media android {
  :root {
    --font-mono: monospace;
    --font-rounded: normal;
    --font-serif: serif;
    --font-sans: normal;
  }
}

@media ios {
  :root {
    --font-mono: ui-monospace;
    --font-serif: ui-serif;
    --font-sans: system-ui;
    --font-rounded: ui-rounded;
  }
}

중요: Babel 설정 불필요

Tailwind v4 및 NativeWind v5에서는 Tailwind를 위한 babel.config.js가 필요하지 않습니다. NativeWind Babel 프리셋이 있다면 제거하세요:

// NativeWind 설정만 포함된 babel.config.js는 삭제
// 다음은 더 이상 필요하지 않습니다:
// module.exports = function (api) {
//   api.cache(true);
//   return {
//     presets: [
//       ["babel-preset-expo", { jsxImportSource: "nativewind" }],
//       "nativewind/babel",
//     ],
//   };
// };

CSS 컴포넌트 래퍼

react-native-css는 명시적인 CSS 요소 래핑이 필요하므로 재사용 가능한 컴포넌트를 생성합니다:

주요 컴포넌트 (src/tw/index.tsx)

import {
  useCssElement,
  useNativeVariable as useFunctionalVariable,
} from "react-native-css";

import { Link as RouterLink } from "expo-router";
import Animated from "react-native-reanimated";
import React from "react";
import {
  View as RNView,
  Text as RNText,
  Pressable as RNPressable,
  ScrollView as RNScrollView,
  TouchableHighlight as RNTouchableHighlight,
  TextInput as RNTextInput,
  StyleSheet,
} from "react-native";

// CSS 지원 Link
const Link = (
  props: React.ComponentProps<typeof RouterLink> & { className?: string }
) => {
  return useCssElement(RouterLink, props, { className: "style" });
};

Link.Trigger = RouterLink.Trigger;
Link.Menu = RouterLink.Menu;
Link.MenuAction = RouterLink.MenuAction;
Link.Preview = RouterLink.Preview;

// CSS 변수 훅
export const useCSSVariable =
  process.env.EXPO_OS !== "web"
    ? useFunctionalVariable
    : (variable: string) => `var(${variable})`;

// View
export type ViewProps = React.ComponentProps<typeof RNView> & {
  className?: string;
};

export const View = (props: ViewProps) => {
  return useCssElement(RNView, props, { className: "style" });
};
View.displayName = "CSS(View)";

// Text
export const Text = (
  props: React.ComponentProps<typeof RNText> & { className?: string }
) => {
  return useCssElement(RNText, props, { className: "style" });
};
Text.displayName = "CSS(Text)";

// ScrollView
export const ScrollView = (
  props: React.ComponentProps<typeof RNScrollView> & {
    className?: string;
    contentContainerClassName?: string;
  }
) => {
  return useCssElement(RNScrollView, props, {
    className: "style",
    contentContainerClassName: "contentContainerStyle",
  });
};
ScrollView.displayName = "CSS(ScrollView)";

// Pressable
export const Pressable = (
  props: React.ComponentProps<typeof RNPressable> & { className?: string }
) => {
  return useCssElement(RNPressable, props, { className: "style" });
};
Pressable.displayName = "CSS(Pressable)";

// TextInput
export const TextInput = (
  props: React.ComponentProps<typeof RNTextInput> & { className?: string }
) => {
  return useCssElement(RNTextInput, props, { className: "style" });
};
TextInput.displayName = "CSS(TextInput)";

// AnimatedScrollView
export const AnimatedScrollView = (
  props: React.ComponentProps<typeof Animated.ScrollView> & {
    className?: string;
    contentClassName?: string;
    contentContainerClassName?: string;
  }
) => {
  return useCssElement(Animated.ScrollView, props, {
    className: "style",
    contentClassName: "contentContainerStyle",
    contentContainerClassName: "contentContainerStyle",
  });
};

// underlayColor 추출이 포함된 TouchableHighlight
function XXTouchableHighlight(
  props: React.ComponentProps<typeof RNTouchableHighlight>
) {
  const { underlayColor, ...style } = StyleSheet.flatten(props.style) || {};
  return (
    <RNTouchableHighlight
      underlayColor={underlayColor}
      {...props}
      style={style}
    />
  );
}

export const TouchableHighlight = (
  props: React.ComponentProps<typeof RNTouchableHighlight>
) => {
  return useCssElement(XXTouchableHighlight, props, { className: "style" });
};
TouchableHighlight.displayName = "CSS(TouchableHighlight)";

Image 컴포넌트 (src/tw/image.tsx)

import { useCssElement } from "react-native-css";
import React from "react";
import { StyleSheet } from "react-native";
import Animated from "react-native-reanimated";
import { Image as RNImage } from "expo-image";

const AnimatedExpoImage = Animated.createAnimatedComponent(RNImage);

export type ImageProps = React.ComponentProps<typeof Image>;

function CSSImage(props: React.ComponentProps<typeof AnimatedExpoImage>) {
  // @ts-expect-error: objectFit 스타일을 contentFit 속성으로 다시 매핑
  const { objectFit, objectPosition, ...style } =
    StyleSheet.flatten(props.style) || {};

  return (
    <AnimatedExpoImage
      contentFit={objectFit}
      contentPosition={objectPosition}
      {...props}
      source={
        typeof props.source === "string" ? { uri: props.source } : props.source
      }
      // @ts-expect-error: style이 위에서 다시 매핑됨
      style={style}
    />
  );
}

export const Image = (
  props: React.ComponentProps<typeof CSSImage> & { className?: string }
) => {
  return useCssElement(CSSImage, props, { className: "style" });
};

Image.displayName = "CSS(Image)";

애니메이션 컴포넌트 (src/tw/animated.tsx)

import * as TW from "./index";
import RNAnimated from "react-native-reanimated";

export const Animated = {
  ...RNAnimated,
  View: RNAnimated.createAnimatedComponent(TW.View),
};

사용법

tw 디렉토리에서 CSS로 래핑된 컴포넌트를 가져옵니다:

import { View, Text, ScrollView, Image } from "@/tw";

export default function MyScreen() {
  return (
    <ScrollView className="flex-1 bg-white">
      <View className="p-4 gap-4">
        <Text className="text-xl font-bold text-gray-900">Hello Tailwind!</Text>
        <Image
          className="w-full h-48 rounded-lg object-cover"
          source={{ uri: "https://example.com/image.jpg" }}
        />
      </View>
    </ScrollView>
  );
}

커스텀 테마 변수

global.css에서 @theme을 사용하여 커스텀 테마 변수를 추가합니다:

@layer theme {
  @theme {
    /* 커스텀 폰트 */
    --font-rounded: "SF Pro Rounded", sans-serif;

    /* 커스텀 줄 높이 */
    --text-xs--line-height: calc(1em / 0.75);
    --text-sm--line-height: calc(1.25em / 0.875);
    --text-base--line-height: calc(1.5em / 1);

    /* 커스텀 leading 배율 */
    --leading-tight: 1.25em;
    --leading-snug: 1.375em;
    --leading-normal: 1.5em;
  }
}

플랫폼별 스타일

플랫폼별 스타일링을 위해 플랫폼 미디어 쿼리를 사용합니다:

@media ios {
  :root {
    --font-sans: system-ui;
    --font-rounded: ui-rounded;
  }
}

@media android {
  :root {
    --font-sans: normal;
    --font-rounded: normal;
  }
}

CSS 변수를 사용한 Apple 시스템 색상

Apple 시맨틱 색상을 위한 CSS 파일 생성:

/* src/css/sf.css */
@layer base {
  html {
    color-scheme: light;
  }
}

:root {
  /* 라이트/다크 모드가 있는 강조 색상 */
  --sf-blue: light-dark(rgb(0 122 255), rgb(10 132 255));
  --sf-green: light-dark(rgb(52 199 89), rgb(48 209 89));
  --sf-red: light-dark(rgb(255 59 48), rgb(255 69 58));

  /* 회색조 */
  --sf-gray: light-dark(rgb(142 142 147), rgb(142 142 147));
  --sf-gray-2: light-dark(rgb(174 174 178), rgb(99 99 102));

  /* 텍스트 색상 */
  --sf-text: light-dark(rgb(0 0 0), rgb(255 255 255));
  --sf-text-2: light-dark(rgb(60 60 67 / 0.6), rgb(235 235 245 / 0.6));

  /* 배경 색상 */
  --sf-bg: light-dark(rgb(255 255 255), rgb(0 0 0));
  --sf-bg-2: light-dark(rgb(242 242 247), rgb(28 28 30));
}

/* platformColor를 통한 iOS 네이티브 색상 */
@media ios {
  :root {
    --sf-blue: platformColor(systemBlue);
    --sf-green: platformColor(systemGreen);
    --sf-red: platformColor(systemRed);
    --sf-gray: platformColor(systemGray);
    --sf-text: platformColor(label);
    --sf-text-2: platformColor(secondaryLabel);
    --sf-bg: platformColor(systemBackground);
    --sf-bg-2: platformColor(secondarySystemBackground);
  }
}

/* Tailwind 테마 색상으로 등록 */
@layer theme {
  @theme {
    --color-sf-blue: var(--sf-blue);
    --color-sf-green: var(--sf-green);
    --color-sf-red: var(--sf-red);
    --color-sf-gray: var(--sf-gray);
    --color-sf-text: var(--sf-text);
    --color-sf-text-2: var(--sf-text-2);
    --color-sf-bg: var(--sf-bg);
    --color-sf-bg-2: var(--sf-bg-2);
  }
}

그런 다음 컴포넌트에서 사용:

<Text className="text-sf-text">Primary text</Text>
<Text className="text-sf-text-2">Secondary text</Text>
<View className="bg-sf-bg">...</View>

JavaScript에서 CSS 변수 사용

useCSSVariable 훅 사용:

import { useCSSVariable } from "@/tw";

function MyComponent() {
  const blue = useCSSVariable("--sf-blue");

  return <View style={{ borderColor: blue }} />;
}

NativeWind v4 / Tailwind v3와의 주요 차이점

  1. babel.config.js 불필요 - 설정이 이제 CSS 우선입니다.
  2. PostCSS 플러그인 - tailwindcss 대신 @tailwindcss/postcss를 사용합니다.
  3. CSS 가져오기 - @tailwind 지시문 대신 @import "tailwindcss/..."를 사용합니다.
  4. 테마 설정 - tailwind.config.js 대신 CSS에서 @theme을 사용합니다.
  5. 컴포넌트 래퍼 - className 지원을 위해 컴포넌트를 useCssElement로 래핑해야 합니다.
  6. Metro 설정 - 다른 옵션(inlineVariables: false)으로 withNativewind를 사용합니다.

문제 해결

스타일이 적용되지 않음

  1. CSS 파일이 앱 진입점에 가져와졌는지 확인합니다.
  2. 컴포넌트가 useCssElement로 래핑되었는지 확인합니다.
  3. Metro 설정에 withNativewind가 적용되었는지 확인합니다.

플랫폼 색상이 작동하지 않음

  1. @media ios 블록에서 platformColor()를 사용합니다.
  2. 웹/Android의 경우 light-dark()로 대체합니다.

TypeScript 오류

컴포넌트 props에 className을 추가합니다:

type Props = React.ComponentProps<typeof RNView> & { className?: string };

Individual skills in this repo

This repo contains 12 individual skills — each has its own dedicated page.

Geruyang/agent-skills-library

Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.

Geruyang/agent-skills-library

Use for new frontend applications, dashboards, games, creative websites, hero sections, and visually driven UI from scratch, or when the user explicitly asks for a redesign/restyle/modernization. Builds from clean, airy, high-taste, readable image-generated concept design with section-specific references, faithful implementation, and browser testing.

Geruyang/agent-skills-library

Use when testing, debugging, or making targeted improvements to rendered frontend apps through the Build Web Apps or web dev plugin: local dev servers, UI regressions, interaction bugs, console errors, responsive layout, and visual QA. Check whether the Browser plugin is available and use it first when it is; otherwise use regular Playwright with the recorded reason.

Geruyang/agent-skills-library

Design UI surfaces for browser games. Use when the user asks for HUDs, menus, overlays, responsive layouts, or visual direction that must protect the playfield.

Geruyang/agent-skills-library

GSAP animation reference for HyperFrames. Covers gsap.to(), from(), fromTo(), easing, stagger, defaults, timelines (gsap.timeline(), position parameter, labels, nesting, playback), and performance (transforms, will-change, quickTo). Use when writing GSAP animations in HyperFrames compositions.

Geruyang/agent-skills-library

Guide for building, configuring, and deploying microfrontends on Vercel. Use this skill when the user mentions microfrontends, multi-zones, splitting an app across teams, independent deployments, cross-app routing, incremental migration, composing multiple frontends under one domain, microfrontends.json, @vercel/microfrontends, the microfrontends local proxy, or path-based routing between Vercel projects. Also use when the user asks about shared layouts across projects, navigation between microfrontends, fallback environments, asset prefixes, or feature flag controlled routing.

Geruyang/agent-skills-library

Next.js App Router expert guidance. Use when building, debugging, or architecting Next.js applications — routing, Server Components, Server Actions, Cache Components, layouts, middleware/proxy, data fetching, rendering strategies, and deployment on Vercel.

Geruyang/agent-skills-library

Use when sizing Public Equity Investing positions, finding equity hedges, or building an integrated position-and-hedge risk plan from a listed-equity thesis. Do not use for thesis construction, standalone event underwriting, trade execution, personal investment advice, or credit-instrument risk.

Geruyang/agent-skills-library

Integrate data visualizations into React and Next.js applications. Use when the user needs chart components, UML-like or architecture diagram components, React integration patterns, Next.js client or server boundaries, hydration-safe rendering, lazy loading, framework-aware performance, scroll-driven visual stories, or export guidance.

Geruyang/agent-skills-library

shadcn/ui expert guidance — CLI, component installation, composition patterns, custom registries, theming, Tailwind CSS integration, and high-quality interface design. Use when initializing shadcn, adding components, composing product UI, building custom registries, configuring themes, or troubleshooting component issues.

Geruyang/agent-skills-library

v0 by Vercel expert guidance. Use when discussing AI code generation, generating UI components from prompts, v0 CLI usage, v0 SDK/API integration, or integrating v0 into development workflows with GitHub and Vercel deployment.

Geruyang/agent-skills-library

Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".

관련 스킬