AnimatedContainer로 부드러운 애니메이션

이번 튜토리얼에서는 Flitter의 AnimatedContainer를 사용해 웹페이지에 생동감을 불어넣는 부드러운 애니메이션을 만들어봅시다.

🎯 학습 목표

이 튜토리얼을 완료하면 다음을 할 수 있게 됩니다:

  • AnimatedContainer의 기본 사용법 이해하기
  • duration과 curve로 애니메이션 속도와 느낌 조절하기
  • 크기, 색상, 위치 애니메이션 구현하기
  • StatefulWidget과 AnimatedContainer 조합하기
  • 사용자 상호작용으로 애니메이션 트리거하기

🚀 애니메이션이란?

웹에서 애니메이션은 사용자 경험을 크게 향상시킵니다. 버튼이 클릭될 때 부드럽게 변화하고, 카드가 호버될 때 살짝 확대되는 것들이 모두 애니메이션의 예시입니다.

Flitter에서는 두 가지 방식의 애니메이션을 제공합니다:

  • 암시적 애니메이션: AnimatedContainer, AnimatedOpacity 등 (오늘 배울 내용)
  • 명시적 애니메이션: AnimationController 사용 (다음 튜토리얼에서)

🎨 AnimatedContainer 기본 개념

AnimatedContainer는 Container의 모든 속성을 애니메이션할 수 있는 특별한 위젯입니다. 가장 큰 장점은 속성 값만 바꿔주면 자동으로 부드럽게 변화한다는 점입니다.

기본 구조

AnimatedContainer({
  duration: 300,        // 애니메이션 지속 시간 (밀리초)
  width: 100,          // 변화할 속성들
  height: 100,
  color: '#4ECDC4',
  curve: "easeInOut",  // 애니메이션 곡선 (선택사항)
  child: Text("내용")
})

AnimatedContainer의 주요 속성들

필수 속성:

  • duration (number): 애니메이션 지속 시간 (밀리초)

애니메이션 가능한 속성들:

  • width, height (number): 컨테이너 크기
  • color (string): 배경색 (decoration과 함께 사용 불가)
  • decoration (BoxDecoration): 테두리, 그림자, 그라데이션 등
  • margin, padding (EdgeInsets): 외부/내부 여백
  • alignment (Alignment): 자식 위젯 정렬
  • constraints (Constraints): 제약 조건
  • transform (Matrix4): 변환 행렬 (회전, 크기, 이동)

기타 속성들:

  • curve (string): 애니메이션 곡선
  • child (Widget): 애니메이션되지 않는 자식 위젯
  • clipped (boolean): 경계 클리핑 여부

📋 단계별 실습

1단계: 클릭하면 크기가 변하는 박스

먼저 클릭할 때마다 크기가 변하는 간단한 박스를 만들어봅시다:

import { AnimatedContainer, Text, GestureDetector } from "@meursyphus/flitter";
import { StatefulWidget, State } from "@meursyphus/flitter";

class ExpandingBox extends StatefulWidget {
  createState() {
    return new ExpandingBoxState();
  }
}

class ExpandingBoxState extends State<ExpandingBox> {
  isLarge = false;

  build(context) {
    return GestureDetector({
      onClick: () => {
        this.setState(() => {
          this.isLarge = !this.isLarge;
        });
      },
      child: AnimatedContainer({
        duration: 500,
        width: this.isLarge ? 200 : 100,
        height: this.isLarge ? 200 : 100,
        color: '#4ECDC4',
        child: Text(this.isLarge ? "큰 박스!" : "작은 박스")
      })
    });
  }
}

// 팩토리 함수로 내보내기
export default function ExpandingBox() {
  return new _ExpandingBox();
}

2단계: 색상도 함께 변화시키기

크기뿐만 아니라 색상도 동시에 변화시켜봅시다:

class ColorfulBox extends StatefulWidget {
  createState() {
    return new ColorfulBoxState();
  }
}

class ColorfulBoxState extends State<ColorfulBox> {
  isExpanded = false;

  build(context) {
    return GestureDetector({
      onClick: () => {
        this.setState(() => {
          this.isExpanded = !this.isExpanded;
        });
      },
      child: AnimatedContainer({
        duration: 600,
        width: this.isExpanded ? 180 : 120,
        height: this.isExpanded ? 180 : 120,
        color: this.isExpanded ? '#FF6B6B' : '#4ECDC4',
        curve: "bounceOut",  // 바운스 효과
        child: Text(
          this.isExpanded ? "🎉 확장됨!" : "👆 클릭하세요"
        )
      })
    });
  }
}

3단계: 테두리와 둥근 모서리 애니메이션

더 정교한 애니메이션을 위해 BoxDecoration을 사용해봅시다:

import { AnimatedContainer, Text, GestureDetector, BoxDecoration, Border, BorderSide } from "@meursyphus/flitter";

class FancyBox extends StatefulWidget {
  createState() {
    return new FancyBoxState();
  }
}

class FancyBoxState extends State<FancyBox> {
  isActive = false;

  build(context) {
    return GestureDetector({
      onClick: () => {
        this.setState(() => {
          this.isActive = !this.isActive;
        });
      },
      child: AnimatedContainer({
        duration: 400,
        width: this.isActive ? 200 : 150,
        height: this.isActive ? 120 : 80,
        decoration: BoxDecoration({
          color: this.isActive ? '#FFE66D' : '#A8E6CF',
          borderRadius: this.isActive ? 25 : 10,
          border: Border.all({
            color: this.isActive ? '#FF6B6B' : '#4ECDC4',
            width: this.isActive ? 3 : 1
          })
        }),
        child: Text(
          this.isActive ? "✨ 활성화!" : "💤 비활성화"
        )
      })
    });
  }
}

4단계: 여백(Padding)과 정렬 애니메이션

내부 여백과 정렬도 애니메이션할 수 있습니다:

import { EdgeInsets, Alignment } from "@meursyphus/flitter";

class PaddingBox extends StatefulWidget {
  createState() {
    return new PaddingBoxState();
  }
}

class PaddingBoxState extends State<PaddingBox> {
  isPadded = false;

  build(context) {
    return GestureDetector({
      onClick: () => {
        this.setState(() => {
          this.isPadded = !this.isPadded;
        });
      },
      child: AnimatedContainer({
        duration: 300,
        width: 200,
        height: 200,
        color: '#E8F4FD',
        padding: this.isPadded 
          ? EdgeInsets.all(40) 
          : EdgeInsets.all(10),
        alignment: this.isPadded 
          ? Alignment.center 
          : Alignment.topLeft,
        child: Container({
          color: '#2196F3',
          child: Text("패딩 변화!")
        })
      })
    });
  }
}

⚙️ duration과 curve 완전 이해하기

duration (지속 시간)

애니메이션이 완료되는 데 걸리는 시간을 밀리초(ms) 단위로 지정합니다.

duration: 200   // 빠른 애니메이션 (0.2초)
duration: 500   // 보통 속도 (0.5초)
duration: 1000  // 느린 애니메이션 (1초)

권장 사항:

  • 너무 짧으면 (< 150ms): 애니메이션을 인지하기 어려움
  • 너무 길면 (> 1000ms): 사용자가 답답함을 느낌
  • 일반적 권장: 200-800ms

curve (애니메이션 곡선)

애니메이션의 진행 속도를 시간에 따라 어떻게 변화시킬지 결정합니다.

기본 곡선들:

curve: "linear"      // 일정한 속도
curve: "easeIn"      // 부드러운 시작, 빠른 끝
curve: "easeOut"     // 빠른 시작, 부드러운 끝
curve: "easeInOut"   // 부드러운 시작과 끝

특수 효과 곡선들:

curve: "bounceOut"   // 바운스 효과
curve: "bounceIn"    // 역방향 바운스
curve: "bounceInOut" // 양방향 바운스

curve: "elasticOut"  // 탄성 효과
curve: "elasticIn"   // 역방향 탄성
curve: "elasticInOut" // 양방향 탄성

curve: "backOut"     // 오버슛 효과
curve: "backIn"      // 역방향 오버슛
curve: "backInOut"   // 양방향 오버슛

실제 curve 비교 예제

class CurveComparison extends StatefulWidget {
  createState() {
    return new CurveComparisonState();
  }
}

class CurveComparisonState extends State<CurveComparison> {
  isAnimated = false;

  build(context) {
    return Column({
      children: [
        // Linear
        GestureDetector({
          onClick: () => this.toggleAnimation(),
          child: AnimatedContainer({
            duration: 1000,
            curve: "linear",
            width: this.isAnimated ? 300 : 100,
            height: 50,
            color: '#FF5722',
            child: Text("Linear")
          })
        }),
        
        // EaseInOut
        GestureDetector({
          onClick: () => this.toggleAnimation(),
          child: AnimatedContainer({
            duration: 1000,
            curve: "easeInOut",
            width: this.isAnimated ? 300 : 100,
            height: 50,
            color: '#2196F3',
            child: Text("EaseInOut")
          })
        }),
        
        // BounceOut
        GestureDetector({
          onClick: () => this.toggleAnimation(),
          child: AnimatedContainer({
            duration: 1000,
            curve: "bounceOut",
            width: this.isAnimated ? 300 : 100,
            height: 50,
            color: '#4CAF50',
            child: Text("BounceOut")
          })
        })
      ]
    });
  }
  
  toggleAnimation() {
    this.setState(() => {
      this.isAnimated = !this.isAnimated;
    });
  }
}

🎯 실습 도전 과제

TODO 1: 3단계 변화 버튼 만들기

클릭할 때마다 3가지 상태로 순환하는 버튼을 만들어보세요:

class TripleStateButton extends StatefulWidget {
  createState() {
    return new TripleStateButtonState();
  }
}

class TripleStateButtonState extends State<TripleStateButton> {
  currentState = 0; // 0, 1, 2

  getStateConfig() {
    const states = [
      { width: 120, height: 60, color: '#3498db', text: "시작" },
      { width: 160, height: 80, color: '#f39c12', text: "진행중" },
      { width: 200, height: 100, color: '#27ae60', text: "완료" }
    ];
    return states[this.currentState];
  }

  build(context) {
    const config = this.getStateConfig();
    
    return GestureDetector({
      onClick: () => {
        this.setState(() => {
          this.currentState = (this.currentState + 1) % 3;
        });
      },
      child: AnimatedContainer({
        duration: 400,
        curve: "easeInOut",
        width: config.width,
        height: config.height,
        color: config.color,
        child: Text(config.text)
      })
    });
  }
}

TODO 2: 호버 효과가 있는 카드

마우스를 올렸을 때 살짝 커지는 카드를 만들어보세요:

class HoverCard extends StatefulWidget {
  createState() {
    return new HoverCardState();
  }
}

class HoverCardState extends State<HoverCard> {
  isHovered = false;

  build(context) {
    return GestureDetector({
      onMouseEnter: () => {
        this.setState(() => {
          this.isHovered = true;
        });
      },
      onMouseLeave: () => {
        this.setState(() => {
          this.isHovered = false;
        });
      },
      child: AnimatedContainer({
        duration: 200,
        curve: "easeOut",
        width: this.isHovered ? 270 : 250,
        height: this.isHovered ? 170 : 150,
        decoration: BoxDecoration({
          color: '#ffffff',
          borderRadius: 10,
          border: Border.all({
            color: this.isHovered ? '#2196F3' : '#e0e0e0',
            width: this.isHovered ? 2 : 1
          })
        }),
        padding: EdgeInsets.all(20),
        child: Text("호버해보세요!")
      })
    });
  }
}

TODO 3: 진행률 표시 바

버튼을 클릭할 때마다 진행률이 증가하는 프로그레스 바를 만들어보세요:

class ProgressBar extends StatefulWidget {
  createState() {
    return new ProgressBarState();
  }
}

class ProgressBarState extends State<ProgressBar> {
  progress = 0; // 0 ~ 100

  build(context) {
    return Column({
      children: [
        Container({
          width: 300,
          height: 20,
          decoration: BoxDecoration({
            color: '#f0f0f0',
            borderRadius: 10
          }),
          child: Stack({
            children: [
              AnimatedContainer({
                duration: 500,
                curve: "easeOut",
                width: (this.progress / 100) * 300,
                height: 20,
                decoration: BoxDecoration({
                  color: '#4CAF50',
                  borderRadius: 10
                })
              })
            ]
          })
        }),
        
        GestureDetector({
          onClick: () => {
            this.setState(() => {
              this.progress = Math.min(this.progress + 20, 100);
              if (this.progress >= 100) {
                // 완료 후 리셋
                setTimeout(() => {
                  this.setState(() => {
                    this.progress = 0;
                  });
                }, 1000);
              }
            });
          },
          child: Container({
            width: 100,
            height: 40,
            color: '#2196F3',
            child: Text("진행 +20%")
          })
        }),
        
        Text(`진행률: ${this.progress}%`)
      ]
    });
  }
}

🎨 예상 결과

완성하면 다음과 같은 기능들이 작동해야 합니다:

  1. 기본 박스: 클릭할 때마다 크기가 부드럽게 변함
  2. 컬러풀 박스: 크기와 색상이 동시에 변화
  3. 고급 박스: 테두리, 둥근 모서리까지 애니메이션
  4. 3단계 버튼: 3가지 상태를 순환하며 변화
  5. 호버 카드: 마우스 호버 시 부드러운 반응
  6. 진행률 바: 클릭에 따라 진행률이 시각적으로 증가

💡 추가 도전

더 도전하고 싶다면:

  1. 연쇄 애니메이션: 여러 박스가 순서대로 애니메이션되도록 하기
  2. 복합 애니메이션: padding, margin, 그림자까지 모두 애니메이션하기
  3. 조건부 애니메이션: 특정 조건에서만 애니메이션 실행하기
  4. 무한 애니메이션: 자동으로 반복되는 애니메이션 만들기

⚠️ 흔한 실수와 해결법

1. 너무 빠른 애니메이션

// ❌ 너무 빨라서 보이지 않음
duration: 50

// ✅ 적절한 속도
duration: 300

2. setState() 사용 실수

// ❌ setState() 없이 직접 변경
this.isExpanded = !this.isExpanded;

// ✅ setState() 사용
this.setState(() => {
  this.isExpanded = !this.isExpanded;
});

3. curve 문자열 오타

// ❌ 오타
curve: "easeInout"  // 'O' 대문자여야 함

// ✅ 정확한 문자열
curve: "easeInOut"

4. color와 decoration 동시 사용

// ❌ 동시 사용 불가
AnimatedContainer({
  color: '#FF0000',
  decoration: BoxDecoration({
    color: '#0000FF'  // 충돌!
  })
})

// ✅ decoration만 사용
AnimatedContainer({
  decoration: BoxDecoration({
    color: '#FF0000'
  })
})

5. 위젯에 new 키워드 사용

// ❌ 위젯에 new 사용 금지
new AnimatedContainer({ ... })

// ✅ 팩토리 함수 사용
AnimatedContainer({ ... })

🎓 핵심 정리

  1. AnimatedContainer: Container의 모든 속성을 애니메이션할 수 있는 위젯
  2. duration: 애니메이션 지속 시간 (밀리초), 200-800ms 권장
  3. curve: 애니메이션 진행 곡선, 자연스러운 움직임 연출
  4. StatefulWidget: 상태 변화로 애니메이션 트리거
  5. GestureDetector: 사용자 상호작용 처리

AnimatedContainer는 Flitter에서 가장 사용하기 쉬운 애니메이션 위젯입니다. 복잡한 애니메이션 컨트롤러 없이도 부드럽고 자연스러운 애니메이션을 만들 수 있어, UI/UX를 크게 향상시킬 수 있습니다.

🚀 다음 단계

다음 튜토리얼에서는 다양한 Animated 위젯들을 배워봅시다:

  • AnimatedOpacity로 투명도 애니메이션
  • AnimatedPadding으로 여백 애니메이션
  • AnimatedAlign으로 위치 애니메이션
  • 여러 애니메이션 동시 실행하기

다음: 다양한 Animated 위젯들 →