Communitygithub.com

Samueladewole/claude-skills

Expo 프로젝트에서 Tailwind CSS v4를 react-native-css 및 NativeWind v5와 함께 설정하여 iOS, Android, 웹에서 일관된 스타일링을 구현합니다.

claude-skills란 무엇인가요?

이 스킬은 Expo 환경에서 Tailwind CSS v4를 설정하는 방법을 상세히 안내합니다. react-native-css와 NativeWind v5를 활용하여 iOS, Android, 웹 플랫폼에서 일관된 스타일링을 구현할 수 있도록 도와줍니다. 기존의 Babel 설정 없이 CSS 기반 설정으로 전환되었으며, `@tailwindcss/postcss` 플러그인과 PostCSS를 사용합니다. 주요 구성 요소는 Metro 설정, PostCSS 설정, 글로벌 CSS 파일, 그리고 CSS로 감싸진 컴포넌트 래퍼들입니다. 이 스킬은 Tailwind v3에서 v4로의 주요 변경 사항(예: Babel 설정 제거, CSS import 방식 변경, 테마 설정 방식 변경)을 명확히 설명하며, Apple 시스템 컬러를 CSS 변수로 통합하는 방법과 플랫폼별 스타일링을 위한 미디어 쿼리 사용법도 제공합니다. 사용자는 `useCssElement` 훅을 통해 className을 지원하는 컴포넌트를 손쉽게 만들 수 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Samueladewole/claude-skills/tree/HEAD/expo-tailwind-setup

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

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

문서

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

이 가이드는 Expo에서 Tailwind CSS v4를 react-native-css와 NativeWind v5를 사용하여 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
export 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)";

이미지 컴포넌트 (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}
    />
  );
}

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>
  );
}

사용자 정의 테마 변수

@theme을 사용하여 global.css에 사용자 정의 테마 변수를 추가하세요:

@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">기본 텍스트</Text>
<Text className="text-sf-text-2">보조 텍스트</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 import - @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 5 individual skills — each has its own dedicated page.

Samueladewole/claude-skills

Create API handoff documentation for frontend developers. Use when backend work is complete and needs to be documented for frontend integration, or user says 'create handoff', 'document API', 'frontend handoff', or 'API documentation'.

Samueladewole/claude-skills

Build accessible, responsive, and performant frontend components with design system best practices, modern CSS, and framework-agnostic patterns.

Samueladewole/claude-skills

Document frontend data needs for backend developers. Use when frontend needs to communicate API requirements to backend, or user says 'backend requirements', 'what data do I need', 'API requirements', or is describing data needs for a UI.

Samueladewole/claude-skills

Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.

Samueladewole/claude-skills

Analyze a short-form video (TikTok, Instagram Reel, YouTube Short, or any MP4) to extract its full transcript, understand why it performed, and generate concrete repurposing ideas. Use this skill whenever the user asks to: analyze a video, understand why something went viral, reverse-engineer a creator's content, extract a transcript from a video, figure out what makes a video perform, or get repurposing ideas from a video. Also trigger when the user says things like "why did this perform", "what's the hook here", "can I repurpose this", "analyze this TikTok/Reel", or drops a video file path or URL.

관련 스킬