상태 관리
개요
Flitter의 Provider는 React의 Context API와 유사한 개념으로, 위젯 트리 전체에서 상태를 효율적으로 관리하고 공유할 수 있습니다. Provider는 InheritedWidget을 기반으로 하며, 위젯 트리의 어느 위치에서든 상태에 접근할 수 있게 해줍니다.
┌─────────────────────────────────────────────┐
│ Provider 패턴 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ ChangeNotifierProvider │ │
│ │ (React의 Context.Provider) │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Widget Tree │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Provider.of(context) │ │ │
│ │ │ (React의 useContext) │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────┘
왜 Provider가 필요한가?
복잡한 애플리케이션에서는 다음과 같은 상황이 자주 발생합니다:
- 깊은 위젯 트리: 여러 단계를 거쳐 데이터를 전달해야 하는 prop drilling 문제
- 상태 공유: 여러 위젯이 동일한 상태를 사용하고 업데이트해야 하는 경우
- 전역 상태: 사용자 인증 정보, 테마 설정 등 앱 전체에서 사용되는 데이터
- 성능 최적화: 상태 변경 시 필요한 위젯만 재빌드하여 성능 향상
핵심 개념
1. Provider Key 정의하기
Provider를 식별하기 위한 고유한 키를 정의합니다:
const COUNTER_KEY = Symbol('CounterProvider');
2. Provider로 값 제공하기
Provider
를 사용하여 위젯 트리에 값을 제공합니다:
// 간단한 값 제공
Provider({
value: { count: 0 },
providerKey: COUNTER_KEY,
child: MyApp({})
})
// Provider 함수로 감싸기
function CounterProvider({ child }: { child: Widget }) {
return Provider({
value: { count: 0, increment: () => {} },
providerKey: COUNTER_KEY,
child
});
}
3. Provider.of로 값 가져오기
Provider.of
를 사용하여 Provider 값에 접근합니다:
// Provider.of 사용
const counter = Provider.of(COUNTER_KEY, context);
// 위젯 내에서 사용
class MyWidget extends StatelessWidget {
build(context: BuildContext): Widget {
const counter = Provider.of(COUNTER_KEY, context) as CounterData;
return Text(`Count: ${counter.count}`);
}
}
// Provider 함수에 of 메서드 추가 (편의성)
function CounterProvider({ child }: { child: Widget }) {
return Provider({
value: { count: 0 },
providerKey: COUNTER_KEY,
child
});
}
CounterProvider.of = (context: BuildContext) => {
return Provider.of(COUNTER_KEY, context) as CounterData;
};
코드 예제
기본 Provider 사용
Provider를 사용한 카운터 앱
여러 위젯에서 동일한 상태를 공유하고 업데이트하는 예제
1. Provider Key 정의
const COUNTER_KEY = Symbol('CounterProvider');
2. Provider로 값 제공하기
Provider({
value: { count: this.count },
providerKey: COUNTER_KEY,
child: MyApp({})
})
3. Provider.of로 값 가져오기
// 직접 사용
const data = Provider.of(COUNTER_KEY, context) as CounterData;
// Provider 함수의 of 메서드 사용 (편의성)
const data = CounterProvider.of(context);
Provider 함수 패턴
function CounterProvider({ child }: { child: Widget }) {
return Provider({
value: { count: 0 },
providerKey: COUNTER_KEY,
child
});
}
CounterProvider.of = (context: BuildContext) => {
return Provider.of(COUNTER_KEY, context) as CounterData;
};
다중 Provider 사용
여러 Provider를 중첩하여 사용할 수 있습니다:
const THEME_KEY = Symbol('ThemeProvider');
const USER_KEY = Symbol('UserProvider');
const CART_KEY = Symbol('CartProvider');
function AppProviders({ child }: { child: Widget }) {
return Provider({
value: new ThemeController(),
providerKey: THEME_KEY,
child: Provider({
value: new UserController(),
providerKey: USER_KEY,
child: Provider({
value: new CartController(),
providerKey: CART_KEY,
child
})
})
});
}
// 사용
AppProviders({
child: MyApp({})
})
실습 예제
1. 간단한 카운터 예제
const COUNTER_KEY = Symbol('CounterProvider');
interface CounterData {
count: number;
}
// StatefulWidget으로 상태 관리
class CounterApp extends StatefulWidget {
createState() {
return new CounterAppState();
}
}
class CounterAppState extends State<CounterApp> {
count = 0;
increment() {
this.setState(() => {
this.count++;
});
}
build(context: BuildContext): Widget {
return Provider({
value: { count: this.count },
providerKey: COUNTER_KEY,
child: Column({
mainAxisAlignment: MainAxisAlignment.center,
children: [
CounterDisplay({}),
SizedBox({ height: 20 }),
GestureDetector({
onClick: () => this.increment(),
child: Container({
padding: EdgeInsets.all(12),
decoration: new BoxDecoration({
color: '#3b82f6',
borderRadius: BorderRadius.circular(8)
}),
child: Text("증가", {
style: new TextStyle({
color: '#ffffff'
})
})
})
})
]
})
});
}
}
// Provider에서 값을 읽는 별도 위젯
class CounterDisplay extends StatelessWidget {
build(context: BuildContext): Widget {
const data = Provider.of(COUNTER_KEY, context) as CounterData;
return Text(`카운트: ${data.count}`, {
style: new TextStyle({
fontSize: 24,
color: '#ffffff'
})
});
}
}
2. Provider 함수 패턴
const THEME_KEY = Symbol('ThemeProvider');
interface ThemeData {
isDarkMode: boolean;
primaryColor: string;
}
// Provider 함수 정의
function ThemeProvider({ child }: { child: Widget }) {
return Provider({
value: {
isDarkMode: true,
primaryColor: '#3b82f6'
},
providerKey: THEME_KEY,
child
});
}
// of 메서드 추가
ThemeProvider.of = (context: BuildContext) => {
return Provider.of(THEME_KEY, context) as ThemeData;
};
// 사용 예제
class ThemedApp extends StatelessWidget {
build(context: BuildContext): Widget {
return ThemeProvider({
child: MainContent({})
});
}
}
class MainContent extends StatelessWidget {
build(context: BuildContext): Widget {
const theme = ThemeProvider.of(context);
return Container({
color: theme.isDarkMode ? '#1a1a1a' : '#ffffff',
child: Text("테마 적용 예제", {
style: new TextStyle({
color: theme.primaryColor,
fontSize: 24
})
})
});
}
}
3. 복잡한 상태 관리 예제
const APP_STATE_KEY = Symbol('AppStateProvider');
class AppController {
user: User | null = null;
isDarkMode = false;
items: Item[] = [];
constructor() {
// 초기화 로직
}
login(email: string, password: string) {
// 로그인 로직
this.user = new User("1", "사용자", email);
}
logout() {
this.user = null;
}
toggleTheme() {
this.isDarkMode = !this.isDarkMode;
}
addItem(item: Item) {
this.items.push(item);
}
}
// Provider 함수
function AppStateProvider({ child }: { child: Widget }) {
return Provider({
value: new AppController(),
providerKey: APP_STATE_KEY,
child
});
}
AppStateProvider.of = (context: BuildContext) => {
return Provider.of(APP_STATE_KEY, context) as AppController;
};
// StatefulWidget과 함께 사용
class MyApp extends StatefulWidget {
createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
controller = new AppController();
build(context: BuildContext): Widget {
return Provider({
value: this.controller,
providerKey: APP_STATE_KEY,
child: AppContent({
onLogin: (email: string, password: string) => {
this.setState(() => {
this.controller.login(email, password);
});
},
onThemeToggle: () => {
this.setState(() => {
this.controller.toggleTheme();
});
}
})
});
}
}
주의사항
1. Provider 범위와 접근성
Provider는 위젯 트리에서 제공된 위치보다 하위에서만 접근 가능합니다:
// ❌ 나쁜 예: Provider가 제공되기 전에 접근 시도
class BadExample extends StatelessWidget {
build(context: BuildContext): Widget {
const data = Provider.of(MY_KEY, context); // 에러!
return Provider({
value: { count: 0 },
providerKey: MY_KEY,
child: Text(`Count: ${data.count}`)
});
}
}
// ✅ 좋은 예: Provider 하위에서 접근
class GoodExample extends StatelessWidget {
build(context: BuildContext): Widget {
return Provider({
value: { count: 0 },
providerKey: MY_KEY,
child: ChildWidget({}) // 여기서 접근 가능
});
}
}
class ChildWidget extends StatelessWidget {
build(context: BuildContext): Widget {
const data = Provider.of(MY_KEY, context);
return Text(`Count: ${data.count}`);
}
}
2. 타입 안전성 유지
Provider 사용 시 타입 캐스팅을 명확히 해야 합니다:
// 타입 정의
interface UserData {
id: string;
name: string;
email: string;
}
const USER_KEY = Symbol('UserProvider');
// Provider 함수에 제네릭 사용
function UserProvider({ child }: { child: Widget }) {
const userData: UserData = {
id: '1',
name: '사용자',
email: '[email protected]'
};
return Provider({
value: userData,
providerKey: USER_KEY,
child
});
}
// 타입 안전한 of 메서드
UserProvider.of = (context: BuildContext): UserData => {
return Provider.of(USER_KEY, context) as UserData;
};
// 사용
const user = UserProvider.of(context);
console.log(user.name); // 타입 안전!
3. 동적 Provider 값 업데이트
Provider의 값을 업데이트하려면 StatefulWidget과 함께 사용해야 합니다:
const SETTINGS_KEY = Symbol('SettingsProvider');
interface Settings {
fontSize: number;
isDarkMode: boolean;
}
class SettingsWidget extends StatefulWidget {
createState() {
return new SettingsWidgetState();
}
}
class SettingsWidgetState extends State<SettingsWidget> {
settings: Settings = {
fontSize: 16,
isDarkMode: false
};
updateFontSize(size: number) {
this.setState(() => {
this.settings = { ...this.settings, fontSize: size };
});
}
toggleDarkMode() {
this.setState(() => {
this.settings = {
...this.settings,
isDarkMode: !this.settings.isDarkMode
};
});
}
build(context: BuildContext): Widget {
return Provider({
value: this.settings,
providerKey: SETTINGS_KEY,
child: SettingsContent({
onFontSizeChange: (size) => this.updateFontSize(size),
onThemeToggle: () => this.toggleDarkMode()
})
});
}
}
다음 단계
Provider 패턴을 마스터했다면, 다음 주제들을 학습해보세요:
- 애니메이션 기초 - AnimationController를 활용한 동적 UI 구현
- 고급 위젯 활용 - 커스텀 렌더링 로직 구현
- 실전 예제 - 실제 프로젝트에서의 Provider 활용 사례