반응형

 

Flutter로 만든 macOS 앱의 기본 아이콘은 Flutter 로고입니다.

이를 커스텀 아이콘으로 바꾸기 위해 가장 확실하게 적용된 방식만 정리합니다.

 

.icns 방식 없이, PNG 파일과 Contents.json 수정만으로 완전히 반영된 방법입니다.

 


 

아이콘 이미지 준비

 

아이콘 이미지는 다음과 같이 정사각형 PNG 파일로 준비합니다.

크기는 Apple에서 요구하는 해상도에 맞춰 리사이징합니다.

파일 이름크기(px)

icon_16x16.png 16×16
icon_16x16@2x.png 32×32
icon_32x32.png 32×32
icon_32x32@2x.png 64×64
icon_128x128.png 128×128
icon_128x128@2x.png 256×256
icon_256x256.png 256×256
icon_256x256@2x.png 512×512
icon_512x512.png 512×512
icon_512x512@2x.png 1024×1024

 

 


 

파일 위치

 

생성한 PNG 파일들을 모두 아래 경로로 복사합니다:

macos/Runner/Assets.xcassets/AppIcon.appiconset/

기존에 있던 Flutter 아이콘 파일들은 덮어쓰거나 삭제해도 됩니다.

 


 

Contents.json 수정

 

같은 폴더에 있는 Contents.json 파일을 아래와 같이 수정합니다:

{
  "images": [
    { "idiom": "mac", "size": "16x16", "scale": "1x", "filename": "icon_16x16.png" },
    { "idiom": "mac", "size": "16x16", "scale": "2x", "filename": "icon_16x16@2x.png" },
    { "idiom": "mac", "size": "32x32", "scale": "1x", "filename": "icon_32x32.png" },
    { "idiom": "mac", "size": "32x32", "scale": "2x", "filename": "icon_32x32@2x.png" },
    { "idiom": "mac", "size": "128x128", "scale": "1x", "filename": "icon_128x128.png" },
    { "idiom": "mac", "size": "128x128", "scale": "2x", "filename": "icon_128x128@2x.png" },
    { "idiom": "mac", "size": "256x256", "scale": "1x", "filename": "icon_256x256.png" },
    { "idiom": "mac", "size": "256x256", "scale": "2x", "filename": "icon_256x256@2x.png" },
    { "idiom": "mac", "size": "512x512", "scale": "1x", "filename": "icon_512x512.png" },
    { "idiom": "mac", "size": "512x512", "scale": "2x", "filename": "icon_512x512@2x.png" }
  ],
  "info": {
    "version": 1,
    "author": "xcode"
  }
}

 

 


 

빌드 및 확인

 

설정이 완료되면 앱을 다시 빌드합니다:

flutter clean
flutter build macos

빌드가 완료된 .app 파일을 실행하거나 Finder, Dock, Spotlight에서 확인했을 때

아이콘이 제대로 보이면 성공입니다.

 


 

마무리

 

Flutter macOS 앱에 아이콘을 적용하는 가장 간단하고 확실한 방법은

.icns 없이 PNG와 Contents.json을 정확히 구성하는 것입니다.

 

이 방식은 Xcode 프로젝트를 수정할 필요도 없고, Flutter 기본 구조 내에서 완전히 동작합니다.

아이콘 적용이 막히셨다면 이 방식으로 시도해보시길 추천드립니다.

반응형
반응형

Flutter로 만든 macOS 앱을 .dmg 형식으로 만들어 사용자가 쉽게 설치할 수 있도록 배포하려면,

create-dmg라는 유틸리티를 사용하면 깔끔한 드래그 앤 드롭 설치 UI를 만들 수 있습니다.

이 글은 그 전체 과정을 정리한 실습 가이드입니다.

 


 

앱 빌드 완료 후 준비

 

먼저 Flutter 앱을 macOS 용으로 빌드합니다:

flutter build macos

빌드가 끝나면 .app 파일이 아래 경로에 생깁니다:

build/macos/Build/Products/Release/YourAppName.app

이제 이 .app.dmg로 포장해 보겠습니다.

 


 

1. 설치에 필요한 폴더 구성

 

.dmg 안에는 앱 외에 /Applications로 연결되는 링크가 필요합니다.

mkdir -p dist
cp -R build/macos/Build/Products/Release/YourAppName.app dist/
ln -s /Applications dist/Applications
dist/ 폴더 안에는 두 개가 있어야 합니다:

 

  • YourAppName.app
  • Applications (심볼릭 링크)

 


 

2. create-dmg 설치 (처음 한 번만)

brew install create-dmg

설치가 완료되면, 어디서든 create-dmg 명령을 사용할 수 있습니다.

 


 

3.  .dmg 파일 생성

create-dmg \
  --volname "YourAppName" \
  --window-size 500 300 \
  --icon-size 100 \
  --icon "YourAppName.app" 125 150 \
  --hide-extension "YourAppName.app" \
  --app-drop-link 375 150 \
  YourAppName.dmg \
  dist/
✅ 가장 중요한 포인트:

 

  • 출력 파일 (YourAppName.dmg) 은 마지막에서 두 번째
  • 소스 폴더 (dist/) 는 가장 마지막 인자

 

예시 결과:

SimpleFitLog.dmg
├── YourAppName.app
├── Applications (→ /Applications)

.dmg를 열면 macOS에서 친숙한 드래그 앤 드롭 설치 UI가 뜹니다.

 


 

4. 자주 발생하는 오류 및 해결

🔸 cd: --: invalid option

 

--output 옵션을 쓰면 안 됩니다.

.dmg 파일 이름은 항상 마지막 두 번째 인자로 넣어야 합니다.

 

잘못된 예:

create-dmg ... --output YourAppName.dmg dist/

올바른 예:

create-dmg ... YourAppName.dmg dist/

 

 


 

5. 추가 팁 (선택 사항)

배경 이미지 넣기

--background "background.png"

 

  • PNG만 지원
  • 이미지 크기는 창 크기보다 크거나 같아야 함

 

아이콘 위치 조정

 

아이콘 좌표는 픽셀 단위로 조정 가능:

--icon "YourAppName.app" 125 150
--app-drop-link 375 150

 

 


 

마무리

 

이제 완성된 .dmg 파일은 다른 사용자에게 배포하거나 웹사이트에 업로드해 사용할 수 있습니다.

앱 실행 시 Gatekeeper 경고가 뜰 경우, 터미널에서 아래 명령어로 실행 제한을 해제할 수 있습니다:

sudo xattr -rd com.apple.quarantine /Applications/YourAppName.app

이 과정을 통해 보다 전문적이고 신뢰감 있는 macOS 앱 배포가 가능해집니다.

 

반응형
반응형

앱에서 고유 ID(Unique ID)를 생성해야 할 때, 가장 널리 쓰이는 방식이 바로 UUID (Universally Unique Identifier)입니다.
Flutter에서는 uuid 패키지를 통해 간편하게 다양한 방식의 UUID를 생성할 수 있습니다.

이 글에서는:

  • uuid 패키지 설치 방법
  • UUID 버전(v1, v4, v5)의 차이점
  • 실전 예제와 사용 팁

까지 하나씩 정리해보겠습니다.


📦 1. uuid 패키지 설치하기

flutter pub add uuid

혹은 pubspec.yaml에 직접 추가:

dependencies:
  uuid: ^4.2.1  # 최신 버전 확인

그리고 import:

import 'package:uuid/uuid.dart';

🧬 2. UUID 버전별 차이점 정리

버전 방식 설명 특징

v1 Time-based 시간 + 기기(MAC 주소) 기반 순차적 생성 가능, MAC 유출 위험
v2 DCE Security v1 + 사용자/그룹 ID 거의 사용되지 않음
v3 Name-based (MD5) 문자열 + 네임스페이스 → MD5 해시 입력이 같으면 UUID도 같음
v4 Random 무작위(random) 값 기반 일반적으로 가장 많이 사용됨
v5 Name-based (SHA-1) v3와 동일하나 SHA-1 해시 사용 더 강력한 보안 해시

⚙️ 3. UUID 생성 예제 (Dart 코드)

final uuid = Uuid();

// ✅ v1 – 시간 + MAC 주소 기반
String idV1 = uuid.v1(); // 예: 'f64c8b26-3e46-11ec-8d3d-0242ac130003'

// ✅ v4 – 랜덤 기반 (가장 많이 사용)
String idV4 = uuid.v4(); // 예: 'c3c3bc94-b070-4787-9f7d-b2592b4d51ef'

// ✅ v5 – 고정된 문자열로 UUID 생성 (SHA-1)
String idV5 = uuid.v5(Uuid.NAMESPACE_URL, 'https://example.com');

📌 v3도 동일 방식으로 가능:

String idV3 = uuid.v3(Uuid.NAMESPACE_URL, 'https://example.com'); // MD5 기반

✅ 실전에서는 언제 어떤 버전?

상황 추천 버전 이유

단순한 고유 ID가 필요할 때 v4 간단하고 충돌 확률 매우 낮음
시간 순서대로 정렬이 필요할 때 v1 생성 시간 정보 포함됨
입력값이 항상 같을 때 동일한 ID가 필요할 때 v5 (또는 v3) 해시 기반 고정 UUID 생성

❗️주의할 점 (v1의 MAC 주소 노출)

  • v1 UUID는 내부적으로 기기의 MAC 주소를 사용합니다.
  • 이 정보는 유출되면 기기 식별 가능성이 있으므로 보안이 중요한 앱에서는 피하는 것이 좋습니다.
  • 대부분의 경우 v4를 사용하는 것이 안전하고 충분합니다.

✨ 실전 사용 예: 모델 ID 생성

class Task {
  final String id;
  final String title;

  Task({required this.title}) : id = const Uuid().v4();
}

→ 매 Task 인스턴스를 생성할 때 자동으로 고유한 UUID를 부여합니다.


🧩 요약

  • uuid 패키지를 설치하면 다양한 방식으로 고유 식별자를 생성할 수 있음
  • 대부분의 경우 v4 (랜덤 기반)가 안정적이고 보편적
  • v5는 동일 입력 → 동일 UUID가 필요할 때 매우 유용
반응형
반응형

📝 요약 설명 

Flutter로 macOS 앱을 개발할 때 기본 창 크기를 설정하고, 앱을 "항상 위에 고정(Always on Top)"으로 띄우는 방법을 소개합니다. 에디터와 앱을 나란히 띄우기 위한 실사용 팁!


🖥️ 문제 상황

Flutter로 macOS 앱을 개발할 때, 앱을 항상 에디터 옆에 띄워놓고 싶었어요.
하지만 앱을 실행하면 매번 이상한 위치에 뜨거나 다른 창 뒤로 가버리는 게 너무 불편했습니다.

그래서 macOS 앱을 항상 위에 고정시키고, 기본 창 크기도 지정하는 방법을 정리해봤습니다.


🔧 해결 방법 요약

  • MainFlutterWindow.swift에서 macOS 앱의 창 속성을 직접 설정할 수 있습니다.
  • 여기서 창 크기, 창 위치, 항상 위에 고정(floating)을 커스터마이징하면 됩니다.

📁 수정할 파일

macos/Runner/MainFlutterWindow.swift


✅ 최종 코드 예시

import Cocoa
import FlutterMacOS

class MainFlutterWindow: NSWindow {
  override func awakeFromNib() {
    let flutterViewController = FlutterViewController()

    // 원하는 창 크기
    let windowWidth: CGFloat = 800
    let windowHeight: CGFloat = 600

    // 화면 중앙에 창 위치 설정
    if let screenFrame = NSScreen.main?.frame {
      let originX = (screenFrame.width - windowWidth) / 2
      let originY = (screenFrame.height - windowHeight) / 2
      let newFrame = NSRect(x: originX, y: originY, width: windowWidth, height: windowHeight)
      self.setFrame(newFrame, display: true)
    }

    // 항상 위에 설정
    self.level = .floating

    self.contentViewController = flutterViewController

    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }
}

📌 추가 팁

  • self.level = .floating → 이걸 설정하면 앱 창이 항상 다른 창 위에 떠 있어요.
    특히 코드 에디터 + 앱 창 나란히 테스트할 때 엄청 편합니다.
  • self.setFrame(...) → 앱의 기본 위치와 크기를 지정할 수 있습니다.
반응형

✨ 마무리

에디터와 앱을 동시에 띄워놓고 작업할 때, 이 설정 하나로 스트레스가 확 줄었어요.
macOS 앱에서 UI 디버깅 자주 하시는 분들께 꼭 추천드립니다.


필요하면 이미지나 gif 첨부용 예시도 만들어줄 수 있어.
티스토리에 바로 붙여넣을 HTML 포맷도 원해?

반응형
반응형

🔍 문제 상황

React에서 컴포넌트 리스트를 .map()으로 렌더링할 때, 다음과 같은 에러가 자주 발생합니다:

Warning: Each child in a list should have a unique 'key' prop.

그래서 보통은 아래처럼 key를 추가하죠.

items.map((item) => (
  <MyComponent key={item.id} item={item} />
))

하지만! 정상적으로 key를 넣었음에도 불구하고 위와 같은 경고가 계속 발생하는 경우가 있습니다.


❓ 대체 왜?

이 문제의 진짜 원인은… 바로 Fragment (<>...</>) 때문입니다.


💥 문제 코드 예시

items.map((item) => (
  <>
    <MyComponent key={item.id} item={item} />
  </>
))

위 코드는 MyComponent에 key를 넣었지만,
사실상 무시되고 있습니다.

왜냐하면, React는 .map()으로 직접 리턴하는 최상위 노드에 key가 있는지를 보기 때문입니다.

즉, 위의 코드는:

  • Fragment가 최상위인데 key 없음 → ❌
  • 내부 MyComponent에 있는 key → 무시됨 → ❌ 경고 발생

✅ 해결 방법

방법 1: Fragment 없애기 (권장)

items.map((item) => (
  <MyComponent key={item.id} item={item} />
))

방법 2: Fragment에 key 넣기 (가능하지만 비권장)

items.map((item) => (
  <React.Fragment key={item.id}>
    <MyComponent item={item} />
  </React.Fragment>
))

💡 실전 예: React Native ScrollView 안에서 발생한 사례

<ScrollView>
  {items.map((item) => (
    <>
      <TimelineItem key={item.id} item={item} />
    </>
  ))}
</ScrollView>

이 구조에서 Fragment가 key를 무시하면서 경고가 발생했습니다.
→ Fragment를 제거하거나, key를 Fragment로 올리면 해결됩니다!


🧠 정리

구조 key 인식 여부 추천 여부

<Component key={...} /> ✅ 강력 추천
<><Component key={...} /></> ❌ 경고 발생
<Fragment key={...}><Component /></Fragment> ⚠️ 가능하지만 가독성 저하 우려

✅ 마무리

React에서 key 경고가 사라지지 않을 때는
"내가 넣은 key가 최상위 엘리먼트에 있는가?" 를 꼭 확인하세요!

특히, 아무 생각 없이 쓴 <> ... </> 가 문제의 원인일 수 있습니다 😉

 

 

반응형
반응형

⚙️ 시작: uuid 설치 및 사용

React Native 앱에서 일정 추가 시 고유 ID가 필요해서
보통 많이 사용하는 uuid 패키지를 설치하고 다음처럼 사용했습니다:

pnpm add uuid
import { v4 as uuidv4 } from 'uuid';

const id = uuidv4();

그리고 이 값을 일정 객체에 사용했죠:

addSchedule({
  id: uuidv4(), // ❌ 여기서 에러 발생
  title: '주사 맞기',
  ...
});

그런데 앱을 실행하자 다음과 같은 에러가 발생했습니다:

Error: crypto.getRandomValues() not supported.

👀 이 에러는 웹 브라우저에서 제공하는 crypto.getRandomValues()를
React Native에서 사용할 수 없기 때문에 발생한 것입니다.


🔍 원인 요약

  • uuid 패키지는 웹 브라우저 환경을 기준으로 만들어졌고
  • 내부적으로 crypto.getRandomValues()를 사용함
  • 하지만 React Native는 웹이 아니므로 해당 API가 없음

⚠️ 원인: uuid 패키지의 내부 동작

많은 개발자들이 사용하는 uuid 패키지는 브라우저 환경에서 잘 작동합니다.
하지만 이 패키지의 v4 구현은 내부적으로 crypto.getRandomValues()를 사용하기 때문에 React Native에서는 동작하지 않습니다.


💡 해결 방법: React Native에 맞는 UUID 패키지 사용

✅ 방법 1: react-native-uuid 사용 (권장)

  1. 설치
pnpm add react-native-uuid
  1. 사용법
import uuid from 'react-native-uuid';

const id = uuid.v4() as string;

uuid.v4()는 string | number[] 형태를 반환하므로, 타입스크립트에서는 as string으로 명시해주는 것이 좋습니다.


✅ 방법 2: expo-crypto 사용 (Expo 사용자)

  1. 설치
pnpm add expo-crypto
  1. 사용법
import * as Crypto from 'expo-crypto';

const id = await Crypto.getRandomUUID();

이 방식은 async/await이 필요하기 때문에 함수 구조를 비동기적으로 변경해야 합니다.


🧠 정리

패키지 RN 지원 여부 특징

uuid ❌ (웹 전용) crypto.getRandomValues() 사용 → 에러 발생
react-native-uuid 동기 방식, 간단하게 사용 가능
expo-crypto ✅ (Expo 전용) 공식 지원, 비동기 방식 필요

✨ 마무리

React Native에서는 브라우저의 crypto API를 사용할 수 없습니다.
따라서 UUID가 필요할 경우, RN에 맞는 패키지를 선택해서 사용하는 것이 중요합니다.

이번 경험을 통해 웹과 네이티브의 차이를 잘 이해하게 되었고, 앞으로는 환경에 맞는 라이브러리 선택이 얼마나 중요한지 다시 한 번 느꼈습니다. 💪

 

반응형
반응형

 

🔍 에러 상황

React Native 프로젝트에서 아래와 같은 코드를 작성했습니다:

import { colors } from '@repo/constants';

const styles = StyleSheet.create({
  container: {
    backgroundColor: colors.green[900],
  },
});

그런데 앱을 실행했더니 다음과 같은 에러가 발생했습니다:

❌ Cannot read property 'green' of undefined

🤔 왜 이런 에러가 발생할까?

에러의 원인은 default export를 named import로 불러왔기 때문입니다.

📁 colors.ts

const colors = {
  green: {
    900: '#064E3B'
  },
  ...
};

export default colors;

📁 index.ts

export * from './colors';

colors.ts는 default export지만,
index.ts에서는 named export로 넘기지 않았기 때문에
import { colors } from '@repo/constants'는 undefined가 됩니다.


✅ 해결 방법 3가지


✅ 방법 1. default → named export로 바꾸기 (가장 추천)

// colors.ts
export const colors = {
  ...
};
// index.ts
export * from './colors';
// 사용처
import { colors } from '@repo/constants';

✅ 가장 직관적이고 안전한 방식입니다.


✅ 방법 2. default export 유지하면서 export 이름 지정

// colors.ts
const colors = { ... };
export default colors;
// index.ts
export { default as colors } from './colors';
// 사용처
import { colors } from '@repo/constants';

✅ default export를 유지하면서도 named import처럼 쓸 수 있습니다.


⚠️ 방법 3. default import로 직접 경로 import (비추천)

import colors from '@repo/constants/colors';

⚠️ 경로를 직접 노출하게 되어 리팩토링이 어려워질 수 있습니다.


💡 결론

목적 추천 방식

import { colors } 하고 싶다 export const colors = ... 사용
default export 꼭 유지하고 싶다 export { default as colors } 추가
하위 경로로 import 가능은 하지만 비추천

 

반응형
반응형

React Native로 앱을 만들다 보면 버튼이나 카드처럼 누를 수 있는 영역을 만들 때 TouchableOpacity 또는 Pressable 중 어떤 걸 써야 할지 고민될 때가 있다.

두 컴포넌트 모두 유저의 터치 입력을 감지하는 역할을 하지만, 사용 방식과 유연성에서 차이가 있다.
이 글에서는 두 컴포넌트의 차이점과 사용 시점을 정리해 본다.


🔸 TouchableOpacity

TouchableOpacity는 React Native 초창기부터 존재한 컴포넌트로,
사용자가 터치하면 **불투명도(opacity)**가 낮아지는 시각적 피드백을 준다.

import { TouchableOpacity, Text } from 'react-native';

<TouchableOpacity onPress={() => alert('Pressed')}>
  <Text>Click me</Text>
</TouchableOpacity>

✅ 장점

  • 사용법이 매우 간단하다
  • 기본적인 눌림 효과(투명도 변화)가 내장돼 있어 빠르게 UI 구성 가능

❌ 단점

  • 스타일 커스터마이징이 제한적이다
  • 눌림 효과는 opacity 하나뿐

🔸 Pressable

Pressable은 React Native 0.63부터 도입된 최신 터치 컴포넌트로,
눌림 상태에 따라 스타일을 완전히 커스터마이징할 수 있는 것이 가장 큰 특징이다.

import { Pressable, Text } from 'react-native';

<Pressable
  onPress={() => alert('Pressed')}
  style={({ pressed }) => ({
    backgroundColor: pressed ? '#eee' : '#fff',
    padding: 10,
  })}
>
  <Text>Click me</Text>
</Pressable>

✅ 장점

  • pressed 상태를 이용해 배경색, 그림자, 테두리 등 다양한 스타일 변경 가능
  • 눌렀을 때 외에도 onPressIn, onPressOut 등 다양한 콜백 제공

❌ 단점

  • 코드가 상대적으로 길고 복잡할 수 있다
  • 간단한 버튼엔 오히려 과할 수 있음

🆚 한눈에 비교

항목 TouchableOpacity Pressable
도입 시기 오래됨 최신 (RN 0.63+)
눌림 효과 opacity 자동 적용 자유롭게 스타일 커스터마이징
스타일 제어 제한적 pressed로 조건부 스타일링
콜백 이벤트 기본적 onPressIn, onPressOut 등 다양
추천 사용처 간단한 터치 UI 다양한 효과가 필요한 경우

💡 언제 어떤 걸 써야 할까?

  • 단순한 버튼, 리스트 아이템 → TouchableOpacity
  • 눌렀을 때 배경색, 테두리 등 커스텀 UI 효과 → Pressable
  • ✅ 최신 React Native 기준의 코드 스타일을 따르고 싶다면 → Pressable 선호

✍️ 마무리

React Native에서 두 컴포넌트는 결국 터치 이벤트 처리를 위한 도구지만,
개발자의 의도에 따라 적절히 선택하는 것이 중요하다.

  • 빠르게 만들고 싶다면 TouchableOpacity
  • 정교한 UI 피드백이 필요하면 Pressable

 

반응형
반응형

 

🎨 Tailwind 색상 팔레트를 React Native로!

TailwindCSS의 색상 시스템은 직관적인 이름(blue_500, gray_900)과 단계별 명확한 색감으로 많은 개발자에게 사랑받고 있습니다.
React Native에서도 동일한 색상 시스템을 구성하면 일관된 디자인 시스템 구축, 다크모드 대응, alpha 값 처리 등 많은 이점을 누릴 수 있습니다.


🌈 전체 색상표 (100 ~ 900)

아래는 TailwindCSS 기준으로 React Native에서 사용할 수 있도록 구성한 색상 팔레트입니다.

글 하단에 요약된 코드를 넣어 두었습니다.

✅ 각 색상은 colors.색상이름[단계] 형태로 접근합니다.
예: colors.blue[500], colors.red[900]


🔵 blue

단계 HEX 코드

100 #DBEAFE
200 #BFDBFE
300 #93C5FD
400 #60A5FA
500 #3B82F6
600 #2563EB
700 #1D4ED8
800 #1E40AF
900 #1E3A8A

⚫ gray

단계 HEX 코드

100 #F3F4F6
200 #E5E7EB
300 #D1D5DB
400 #9CA3AF
500 #6B7280
600 #4B5563
700 #374151
800 #1F2937
900 #111827

🔴 red

단계 HEX 코드

100 #FEE2E2
200 #FECACA
300 #FCA5A5
400 #F87171
500 #EF4444
600 #DC2626
700 #B91C1C
800 #991B1B
900 #7F1D1D

🟢 green

단계 HEX 코드

100 #D1FAE5
200 #A7F3D0
300 #6EE7B7
400 #34D399
500 #10B981
600 #059669
700 #047857
800 #065F46
900 #064E3B

🟡 yellow

단계 HEX 코드

100 #FEF9C3
200 #FEF08A
300 #FDE047
400 #FACC15
500 #EAB308
600 #CA8A04
700 #A16207
800 #854D0E
900 #713F12

🟣 purple

단계 HEX 코드

100 #F3E8FF
200 #E9D5FF
300 #D8B4FE
400 #C084FC
500 #A855F7
600 #9333EA
700 #7E22CE
800 #6B21A8
900 #581C87

🌸 pink

단계 HEX 코드

100 #FCE7F3
200 #FBCFE8
300 #F9A8D4
400 #F472B6
500 #EC4899
600 #DB2777
700 #BE185D
800 #9D174D
900 #831843

🧊 cyan

단계 HEX 코드

100 #CFFAFE
200 #A5F3FC
300 #67E8F9
400 #22D3EE
500 #06B6D4
600 #0891B2
700 #0E7490
800 #155E75
900 #164E63

 


반응형

 

const colors = {
  gray: {
    100: '#F3F4F6',
    200: '#E5E7EB',
    300: '#D1D5DB',
    400: '#9CA3AF',
    500: '#6B7280',
    600: '#4B5563',
    700: '#374151',
    800: '#1F2937',
    900: '#111827',
  },
  blue: {
    100: '#DBEAFE',
    200: '#BFDBFE',
    300: '#93C5FD',
    400: '#60A5FA',
    500: '#3B82F6',
    600: '#2563EB',
    700: '#1D4ED8',
    800: '#1E40AF',
    900: '#1E3A8A',
  },
  green: {
    100: '#D1FAE5',
    200: '#A7F3D0',
    300: '#6EE7B7',
    400: '#34D399',
    500: '#10B981',
    600: '#059669',
    700: '#047857',
    800: '#065F46',
    900: '#064E3B',
  },
  red: {
    100: '#FEE2E2',
    200: '#FECACA',
    300: '#FCA5A5',
    400: '#F87171',
    500: '#EF4444',
    600: '#DC2626',
    700: '#B91C1C',
    800: '#991B1B',
    900: '#7F1D1D',
  },
  yellow: {
    100: '#FEF9C3',
    200: '#FEF08A',
    300: '#FDE047',
    400: '#FACC15',
    500: '#EAB308',
    600: '#CA8A04',
    700: '#A16207',
    800: '#854D0E',
    900: '#713F12',
  },
  indigo: {
    100: '#E0E7FF',
    200: '#C7D2FE',
    300: '#A5B4FC',
    400: '#818CF8',
    500: '#6366F1',
    600: '#4F46E5',
    700: '#4338CA',
    800: '#3730A3',
    900: '#312E81',
  },
  purple: {
    100: '#F3E8FF',
    200: '#E9D5FF',
    300: '#D8B4FE',
    400: '#C084FC',
    500: '#A855F7',
    600: '#9333EA',
    700: '#7E22CE',
    800: '#6B21A8',
    900: '#581C87',
  },
  pink: {
    100: '#FCE7F3',
    200: '#FBCFE8',
    300: '#F9A8D4',
    400: '#F472B6',
    500: '#EC4899',
    600: '#DB2777',
    700: '#BE185D',
    800: '#9D174D',
    900: '#831843',
  },
  teal: {
    100: '#CCFBF1',
    200: '#99F6E4',
    300: '#5EEAD4',
    400: '#2DD4BF',
    500: '#14B8A6',
    600: '#0D9488',
    700: '#0F766E',
    800: '#115E59',
    900: '#134E4A',
  },
  orange: {
    100: '#FFEDD5',
    200: '#FED7AA',
    300: '#FDBA74',
    400: '#FB923C',
    500: '#F97316',
    600: '#EA580C',
    700: '#C2410C',
    800: '#9A3412',
    900: '#7C2D12',
  },
  lime: {
    100: '#ECFCCB',
    200: '#D9F99D',
    300: '#BEF264',
    400: '#A3E635',
    500: '#84CC16',
    600: '#65A30D',
    700: '#4D7C0F',
    800: '#3F6212',
    900: '#365314',
  },
  cyan: {
    100: '#CFFAFE',
    200: '#A5F3FC',
    300: '#67E8F9',
    400: '#22D3EE',
    500: '#06B6D4',
    600: '#0891B2',
    700: '#0E7490',
    800: '#155E75',
    900: '#164E63',
  },
  zinc: {
    100: '#F4F4F5',
    200: '#E4E4E7',
    300: '#D4D4D8',
    400: '#A1A1AA',
    500: '#71717A',
    600: '#52525B',
    700: '#3F3F46',
    800: '#27272A',
    900: '#18181B',
  },
};

export default colors;

 

🏁 마무리

Tailwind의 색상 시스템은 미적으로 뛰어나고 확장도 쉽습니다.
React Native에서도 이 구조를 도입하면 다크모드 대응은 물론, 협업 시에도 큰 도움이 됩니다.

반응형
반응형

🔥 소개

TailwindCSS 스타일의 색상 시스템은 직관적이고 확장성이 뛰어나 React Native에도 잘 어울립니다.
이번 글에서는 blue_100, gray_900처럼 단계별 색상 정의, 다크모드 대응, alpha 값 추가, 그리고 자동완성 가능한 타입 정의까지 한 번에 구성하는 방법을 소개합니다.


📁 폴더 구조

먼저, 색상 관련 파일을 모아둘 theme 디렉토리를 만들어 아래처럼 구성합니다.

src/
└── theme/
    ├── colors.ts              # Tailwind 색상 팔레트
    ├── darkColors.ts          # 다크모드 색상
    ├── alphaColor.ts          # alpha 유틸
    ├── theme.ts               # 라이트/다크 테마 설정
    ├── useThemeColor.ts       # 현재 테마 기준 색상 반환
    ├── tailwindColors.d.ts    # 타입 정의 (자동완성 지원)
    └── index.ts               # export 모음

🎨 Tailwind 색상 정의

colors.ts에서 Tailwind 스타일로 색상을 정의합니다. 아래는 일부 예시이며, 필요에 따라 추가할 수 있습니다.

const colors = {
  blue: {
    100: '#DBEAFE',
    500: '#3B82F6',
    900: '#1E3A8A',
  },
  gray: {
    100: '#F3F4F6',
    500: '#6B7280',
    900: '#111827',
  },
  red: {
    100: '#FEE2E2',
    500: '#EF4444',
    900: '#7F1D1D',
  },
  // 추가 색상: green, yellow, indigo, etc...
}

export default colors

🌙 다크모드 색상 정의

다크모드에서는 gray 계열을 반전시켜 사용하면 좋습니다.

const darkColors = {
  ...colors,
  gray: {
    100: '#18181B',
    500: '#71717A',
    900: '#F4F4F5',
  },
  background: '#0f172a',
  foreground: '#f8fafc',
}

export default darkColors

💧 Alpha 값 유틸 함수

투명도를 추가하고 싶다면 아래처럼 alpha를 붙여주는 유틸을 만듭니다.

export function withAlpha(hex: string, alpha: number): string {
  const bounded = Math.max(0, Math.min(1, alpha))
  const alphaHex = Math.round(bounded * 255).toString(16).padStart(2, '0')
  return hex + alphaHex
}

🧠 현재 테마 기반 색상 가져오기

React Native의 useColorScheme() 훅을 활용해 현재 테마에 맞는 색상을 가져옵니다.

export function useThemeColor(color: 'blue' | 'gray', shade: 100 | 500 | 900) {
  const theme = useColorScheme() === 'dark' ? 'dark' : 'light'
  return themes[theme].colors[color][shade]
}

🧾 타입 선언으로 자동완성 지원

타입스크립트를 쓰는 경우, 색상 이름과 단계 타입을 미리 정의해두면 IDE 자동완성과 에러 방지에 큰 도움이 됩니다.

export type TailwindColorName =
  | 'gray' | 'blue' | 'green' | 'red' | 'yellow' | 'indigo' | 'purple'

export type TailwindColorShade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900

✅ 사용 예시

const bgColor = useThemeColor('blue', 100)
const textColor = useThemeColor('gray', 900)
const transparent = withAlpha(bgColor, 0.5)

return (
  <View style={{ backgroundColor: transparent, padding: 16 }}>
    <Text style={{ color: textColor }}>Tailwind 스타일 색상 예제</Text>
  </View>
)

🏁 마무리

Tailwind 스타일 색상 시스템을 React Native에 도입하면 코드 일관성, 유지보수성, 확장성이 모두 향상됩니다.
이번 구성은 디자인 시스템의 기반이 되므로 프로젝트 초기부터 도입해보세요!

반응형

+ Recent posts