Skip to main content

18 posts tagged with "flutter"

flutter tag description

View All Tags

앱스토어 빌드 이슈

· 2 min read
Junseok Yang
Football Loving Programmer
  • 앱스토어 빌드 실패 트러블슈팅

🚨 발생한 문제/에러

1. 문제/에러 정의

xcode에서 ios 폴더를 연 후 product / archive 를 통해서 apple connect에 배포를 해야되는데 배포가 되지 않고 아래 에러와 함께 빌드 실패가 나옴

Signing for "Runner" requires a development team. Select a development team in the Signing & Capabilities editor.

2. 시도한 해결 방법

  • flutter clean & flutter pub get
  • 프로젝트 다시 clone하기
  • pod install

3. 최종 해결 방법

  • 튜터님이 알려준 공식홈페이지의 가르침대로 했더니 잘 됐다!

Build and release an iOS app

  • xcode 에서 open existing project → targets / runnser → Identity : build 숫자 하나씩 올려주기

  • flutter build ipa 터미널에서 실행 → 배포하기 위한 파일들을 만들어 줌

  • 실행 후에 이런 안내 메시지를 만들어 줌

    error_log.png

  • transporter 앱을 다운받음

  • transporter 앱에 build/ios/ipa/투두모두.ipa” 를 드래그 앤 드랍 해버리면 빌드 끝!

4. 새롭게 알게 된 점

  • 빌드를 반드시 xcode - product / archive 로만 해야 되는 줄 알았는데 직접 파일을 transporter를 통해서 업로드할 수 있었음

💭 오늘의 회고

잘한 점 👍

  • 배포 성공

개선할 점 🔨

  • 배포에 너무 오래 걸림

배운 점 💡

✏️ 참고 자료

플러터 테마, 라우터, 반응형 디자인

· 7 min read
Junseok Yang
Football Loving Programmer
  • 플러터 테마 익스텐션 설정
  • GoRouter를 활용한 플러터 라우팅
  • 반응형 디자인 만들기

✍️ 주요 학습 내용

새로 알게된 개념

  • ThemeExtension을 확장해서 테마를 확장시킬 수 있음

extends ThemeExtension<클래스이름>

class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
AppThemeExtension({
required this.main,
required this.mainLight,
required this.sub,
required this.background,
});

final Color main;
final Color mainLight;
final Color sub;
final Color background;

@override
ThemeExtension<AppThemeExtension> lerp(
covariant AppThemeExtension? other,
double t,
) {
if (other == null) {
return this;
}
print(t);
return AppThemeExtension(
main: Color.lerp(main, other.main, t)!,
mainLight: Color.lerp(mainLight, other.mainLight, t)!,
sub: Color.lerp(sub, other.sub, t)!,
background: Color.lerp(background, other.background, t)!,
);
}
}

class LightTheme extends AppThemeExtension {
LightTheme({
super.main = Colors.red,
super.mainLight = const Color(0xAAFF0000)
uper.sub = const Color(0xFFFFF000), // 0xFF(투명도)FF(R)F0(G)00(B)
super.background = Colors.white,
})
}

class DartTheme extends AppThemeExtension {
DartTheme({
super.main = const Color(0XFF0000FF),
super.mainLight = const Color.fromARGB(170, 151, 151, 168),
super.sub = const Color(0xFFFF00FF),
super.background = Colors.black,
});
}

theme.dart

import 'package:flutter/material.dart';
import 'package:flutter_theme_example/custom_theme.dart';

final lightTheme = _theme(Brightness.light, LightTheme());
final darkTheme = _theme(Brightness.dark, DartTheme());

ThemeData _theme(Brightness brightness, AppThemeExtension ext) {
return ThemeData(
brightness: brightness,
useMaterial3: true,
scaffoldBackgroundColor: ext.background,
colorScheme: ColorScheme.fromSeed(
brightness: brightness,
seedColor: ext.main,
),
extensions: [ext],
);
}

// extension 클래스를 확장해 주는 기능
// BuildContext 클래스를 확장해 주는 것
// 추가적인 메서드를 구현할 수 있게 해 줌
extension BuildContextExtension on BuildContext {
ThemeData get theme => Theme.of(this);
AppThemeExtension get appColor => theme.extension<AppThemeExtension>()!;
}

// Theme.of(context).primaryColor 로 접근을 했지만
// context.theme
// context.appColor 이렇게 접근이 가능함

Color Scheme

속성 이름설명
primary앱의 주 색상. 버튼, 활성화된 상태 등에 사용됨.
onPrimaryprimary 위에 놓일 텍스트/아이콘 색상.
secondary보조 색상. 강조 요소 등에 사용.
onSecondarysecondary 위에 놓일 텍스트/아이콘 색상.
background전체 화면 배경 색상.
onBackground배경 위의 텍스트/아이콘 색상.
surface카드, 시트(sheet), 다이얼로그 등 표면 요소의 배경색.
onSurfacesurface 위에 놓이는 텍스트/아이콘 색상.
error오류 메시지 등에 사용될 색상.
onErrorerror 위에 표시될 텍스트/아이콘 색상.
primaryContainerprimary의 변형 색상.
onPrimaryContainerprimaryContainer 위에 표시될 텍스트 색상.
...(MD3에서는 더 많은 필드 제공)
  • 필요하면 Theme.of(Context).colorScheme.primary 로 접근 가능

GoRouter

패키지 설치

flutter pub add go_router

/post/1

→ 주소창에서 바로 상세 페이지로 이동해도 뒤로 가기를 하면 PostListPage가 나옴. 왜냐하면 GoRouter에서 / → /Post → /Post/id 이렇게 스택을 쌓았기 때문

Router.dart

import 'package:fluuter_go_router/pages/error_page.dart';
import 'package:fluuter_go_router/pages/home_page.dart';
import 'package:fluuter_go_router/pages/post_detail_page.dart';
import 'package:fluuter_go_router/pages/post_list_page.dart';
import 'package:fluuter_go_router/pages/search_page.dart';
import 'package:go_router/go_router.dart';

final router = GoRouter(
initialLocation: '/',
errorBuilder: (context, state) {
return ErrorPage();
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
routes: [
GoRoute(
path: 'post',
builder: (context, state) => PostListPage(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) {
//
final id = int.tryParse(state.pathParameters['id'] ?? '');
if (id == null) {
return ErrorPage();
}
return PostDetailPage(id: id);
},
),
],
),
GoRoute(
path: 'search',
builder: (context, state) {
final text = state.uri.queryParameters['text'] ?? '';
if (text.trim().isEmpty) {
return ErrorPage();
}
return SearchPage(text: text);
},
),
],
),
],
);

post/:id

int.tryParse(state.pathParameters[’id’] ?? ‘’ 로 접근

search?text=text

final text = state.uri.queryParameters[’text’] ?? ‘’
if(text.trim().isEmpty){
return ErrorPage();
}
return SearchPage(text : text);

반응형 디자인

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
// constraints : BoxConstraints
builder: (context, constraints) {
if (constraints.maxWidth >= 768) {
// 태블릿용 위젯 리턴
return HomeBodyTablet();
}
if (constraints.maxWidth >= 600) {
// 소형태블릿용 위젯 리턴
return HomeBodyTabletSm();
}
if (constraints.maxWidth >= 480) {
// 모바일 가로모드용 위젯 리턴
return HomeBodyLandscape();
}
// 모바일 세로
return HomeBody();
},
);
}
}

MediaQuery 사용

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// width와 height를 가지고 있는 Size 클래스
Size size = MediaQuery.sizeOf(context);
double deviceWidth = size.width;
if (deviceWidth >= 768) {
// 태블릿용 위젯 리턴
return HomeBodyTablet();
}
if (deviceWidth >= 600) {
// 소형태블릿용 위젯 리턴
return HomeBodyTabletSm();
}

if (deviceWidth >= 480) {
// 모바일 가로모드용 위젯 리턴
return HomeBodyLandscape();
}
// 모바일 세로
return HomeBody();
}
}

그리고 SizedBox(width : 250, child : widget) 이렇게 활용해서 테블릿으로 바뀔 때 왼쪽 위젯의 길이가 고정되고 오른쪽은 double.infinity 를 활용해서 길이를 늘리는 방식이 인상깊었음

사용법

import 'package:flutter/material.dart';
import 'package:flutter_responsive_ui/chat_detail_view.dart';
import 'package:flutter_responsive_ui/chat_list_view.dart';
import 'package:flutter_responsive_ui/responsive_view.dart';

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ResponsiveView(
mobile: _HomePageMobile(),
tablet600: _HomePageTablet600(),
);
}
}

class _HomePageMobile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: ChatListView());
}
}

class _HomePageTablet600 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
//
SizedBox(width: 250, child: ChatListView()),
Expanded(child: ChatDetailView()),
],
),
);
}
}

  • 홈페이지에서 기기별 불러오는 위젯을 달리해 반응형 디자인을 할 수 있었음
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_responsive_ui/chat_list_item.dart';

final colors = List.generate(100, (index) {
final random = Random();

return Color.fromARGB(
255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256),
);
});

class ChatListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 20),
itemBuilder: (context, index) {
//
return ChatListItem(color: colors[index], isSend: false);
},
);
}
}

  • 랜덤한 색상을 줄 때 0-255까지의 값을 랜덤으로 줘서 랜덤 색상을 만드는 방법이 인상적이었음

📝 코드 스니펫

// 오늘 배운 주요 코드
final colors = List.generate(100, (index){
final random = Random();
return Color.fromARGB(255,
random.nextInt(256),
random.nextInt(256),
random.nextInt(256));
})

📚 내일 학습할 내용

  • 개인과제 시작

💭 오늘의 회고

잘한 점 👍

  • 강의에 집중할 수 있었음

개선할 점 🔨

  • 아침 지각하지 않기!

배운 점 💡

  • 반응형 디자인을 할 때 기기별 규격 사이즈 기준

✏️ 참고 자료

Flutter IOS 시뮬레이터 빌드 에러

· 5 min read
Junseok Yang
Football Loving Programmer

플러터 숙련 주차 강의를 듣고 ‘위치 기반 채팅 앱’이라는 팀 프로젝트를 시작했다. 시작하고 나서 이상하게 빌드 오류가 생겼다. IOS 시뮬레이터로 빌드를 하려고 하면 빌드 중이라는 메시지가 나오고 계속 머물러 있었다.

1. 문제/에러 정의

// 에러 메시지
Junseoks-MacBook-Pro:wanna_exercise_app junseokyang$ flutter run
Launching lib/main.dart on iPhone 16 Pro in debug mode...
Running pod install... 40.9s
Running Xcode build... ⢿
  • 시뮬레이터를 켜고 앱을 실행시키려고 하면 이런 메시지가 뜨면서 Xcode build 에서 멈췄다.

2. 시도한 해결 방법

  • Chat GPT에게 물어본 결과 몇 가지 시도해 볼만한 내용들을 알려줬다.
  1. Flutter clean & 의존성 재설치
cd ios
rm -rf Pods Podfile.lock
cd ..
flutter clean
flutter pub get
cd ios
pod install --repo-update
cd ..
flutter run

  • Xcode build에서 시간이 오래 걸리니까 ios 폴더 안에 있는 Podfile.lock 파일을 제거하고 다시 의존성 파일들을 다운하는 것 같다.

⇒ 하지만 해결되지 않았다.

  1. DerivedData 삭제
  • Xcode 빌드 캐시(DerivedData)가 꼬여서 무한 대기 상태가 되는 경우가 많음
rm -rf ~/Library/Developer/Xcode/DerivedData
  • 이후 다시 flutter run 을 실행했지만 여전히 안 됐음
  1. Xcode 커맨드라인 도구 및 라이선스 확인
  • 최신 도구 설치
xcode-select --install
  • 라이선스 동의
sudo xcodebuild -license accept
  1. Verbose 모드로 원인 파악
  • 빌드 로그를 보면서 원인을 찾기
flutter run --verbose
  1. Xcode에서 직접 빌드해 보기
  • ios/Runner.xcworkspace 파일을 열고
  • 상단의 Scheme를 Runner > Any iOS Device (arm64) 로 설정
  • Product → Clean Build Folder
  • Product → Build

⇒ 이 과정을 통해 Xcode가 뱉는 네이티브 에러(코드 사인, 플러그인 설정 누락 등)를 직접 확인할 수 있음


  • 3번 과정에서 이런 에러 메시지가 나옴
Junseoks-MacBook-Pro:wanna_exercise_app junseokyang$ xcode-select --install
xcode-select: note: Command line tools are already installed. Use "Software Update" in System Settings or the softwareupdate command line interface to install updates
  1. CLT 업데이트 확인하기
softwareupdate --list
softwareupdate --install "Command Line Tools for Xcode-XX.X"

("Command Line Tools for Xcode-XX.X" 이름은 --list 출력에서 정확히 복사해 옵니다.)

→ 여기서 18.4 로 업그레이드를 함

  1. Xcode 자체 업데이트
  • App Store(앱스토어)를 열어 Xcode를 최신 버전으로 업데이트
  1. Developer 경로 확인
xcode-select -p

//
/Applications/Xcode.app/Contents/Developer
  1. 라이센스 재동의
  • 때떄로 라이선스 문제가 겹쳐 있기도 함
sudo xcodebuild -license accept

3. 최종 해결 방법

  • xcode workspace에서 직접 빌드를 해보고 Build successed가 나옴
  • 컴파일 단계 (소스 코드 → 바이너리)에는 문제가 없다는 의미
  • 이후의 앱 실행 단계 (시뮬레이터 → flutter 툴 )에서 걸리고 있는 것
[        ] executing: [/Users/junseokyang/Desktop/project/flutter/wanna_exercise_app/ios/]
xcrun xcodebuild -configuration Debug VERBOSE_SCRIPT_LOGGING=YES -workspace
Runner.xcworkspace -scheme Runner
BUILD_DIR=/Users/junseokyang/Desktop/project/flutter/wanna_exercise_app/build/ios -sdk
iphonesimulator -destination id=69DE6B74-91BF-4BEC-83C3-00B78FC881C7
SCRIPT_OUTPUT_STREAM_FILE=/var/folders/qg/lm1gtz4d0k94_jc8mdhsllym0000gn/T/flutter_tools.P
Dubxy/flutter_ios_build_temp_dirXXjEBy/pipe_to_stdout -resultBundlePath
/var/folders/qg/lm1gtz4d0k94_jc8mdhsllym0000gn/T/flutter_tools.PDubxy/flutter_ios_build_te
mp_dirXXjEBy/temporary_xcresult_bundle -resultBundleVersion 3
FLUTTER_SUPPRESS_ANALYTICS=true COMPILER_INDEX_STORE_ENABLE=NO
  • 여전히 이런 메시지가 나오면서 빌드에 시간이 걸렸는데 Xcode에서 Product → Build 할 때 내부적으로 실행되는 xcodebuild와 거의 동일한 작업을 수행하는 것을 알았음
  • Xcode 에서도 시간이 오래 걸려서 기다렸는데 해결이 됨
  • 진행 상황 확인
    • 터미널에서 CPU 사용량이 높은지 확인하려면 Activity Monitor에서 xcodebuild 프로세스가 돌아가는지 보기
    • 디스크 / CPU를 쓰고 있다면 ‘빌드 중’인 상태

4. 새롭게 알게 된 점

  • 생각보다 빌드 시간이 오래 걸린다는 것을 알게 됨
    • 20분 이상 걸렸음

5. 다음에 비슷한 문제를 만난다면?

  • Xcode 버전을 업그레이드하고 빌드할 때 조금 더 기다려 보기

운동하실? 앱 기획하기

· 2 min read
Junseok Yang
Football Loving Programmer
  • 플러터 숙련 주차 과제는 위치 기반 채팅앱 만들기였다.
  • 그래서 예전에 동네풋살 창업을 하면서 즉흥적으로 당일 운동을 하고 싶은 사람들을 연결해 주는 앱을 만들면 수요가 있지 않을까 생각했던 게 있어서 그 아이디어를 구체화시키고 팀원들이 내 아이디어에 동의를 해줘서 나의 아이디어로 과제를 시작하게 됐다.

서비스 이름은 ‘운동하실?’

기획의도는 갑자기 오늘 운동이 하고 싶을 때!

한 번의 클릭으로 오늘 운동이 하고 싶다는 의사를 나타내고 위치 기반으로 동네별로 어떤 운동을 하고 싶은 사람이 몇 명이 있는지 보여주고 그 사람들을 채팅방에 연결해 주는 서비스였다.

내가 생각했던 핵심은 정말 앱이 단순해야 한다는 것이다. 앱을 키고 버튼 한 번만 누르면 모든 게 끝날 수 있도록 단순하게 자기의 운동의사를 사람들에게 알려주는 것이다.


오늘은 피그마를 통해서 UI 기획을 하고 Github pull requests extension을 활용해서 프로젝트 관리를 했다. 내가 처음부터 직접 하니까 더 배울 수 있는 내용도 많았고 좋았다. 내일도 더욱더 구체화해서 잘 해야겠다.

지역 검색 앱 Readme 및 트러블슈팅

· 8 min read
Junseok Yang
Football Loving Programmer
  • readme 파일 작성
  • 트러블 슈팅 기록하기

✍️ Readme 파일 작성

지역 검색 앱 만들기

📖 목차

  1. 프로젝트 소개
  • 본 프로젝트는 flutter 숙련주차 개인과제로 Open API를 활용해서 지역검색 기능을 구현하는 프로젝트이다.
  1. 주요기능

    • Naver 검색 Open API 활용
    • Riverpod을 활용한 MVVM 구조
  2. 개발기간

  • 2025년 4월 18일 - 2025년 4월 22일
  1. 기술스택
  • Flutter
  1. 프로젝트 파일 구조
lib/

├── main.dart

├── data/
│ ├── dto/
│ │ │
│ │ ├── location_response_dto.dart
│ ├── model/
│ │ │
│ │ ├── location.dart
│ ├── repository/
│ │ │
│ │ ├── location_repository.dart
├── pages/
│ ├── detail/ // 상세 페이지
│ │ │
│ │ └── detail_page.dart
│ ├── home/ // 홈 페이지
│ │ ├── widgets/
│ │ │ │
│ │ │ ├── home_card.dart
│ │ ├── home.dart
│ │ │
│ │ └── home_view_model.dart


Trouble Shooting

1. 플러터 문법 실수

  • 에러 메시지

════════ Exception caught by widgets library ═══════════════════════════════════
Incorrect use of ParentDataWidget.
  • 문제점
return Expanded(child: Column(children: [Text('hello')]));

Expanded 위젯은 Column, Row, Flex 같은 Flex 계열 위젯의 자식으로만 사용할 수 있음.


2. ProviderScope 작성해 주기

  • MVVM 패턴이 익숙하지 않은 탓에 쉬운 실수를 해 버렸다.
  • 에러 메시지
Bad State: No ProviderScope found

⇒ 앱 위젯 트리 어디에도 ProviderScope 가 없어서 ref.read , ref.watch 등을 사용할 수가 없음

✅ 문제 원인

앱의 가장 최상단에서 ProviderScope로 앱을 감싸지 않았다면, Riverpod은 동작할 수 없어서 위와 같은 에러가 발생

✅ 해결 방법

  • 최상단에서 ProviderScope 로 감싸주기
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}

3. title에 html 태그가 포함되어 있음

🤬 에러

  • Naver 검색 Open API에서 불러오는 데이터의 title<b></b>와 같은 html 태그가 들어가 있어서 그것이 그대로 나타나게 됨
  • <b>전주</b>비빔밥 이런 식으로 텍스트가 나오게 됨

✅ 시도한 방법

  • html 태그가 적용된 텍스트가 나올 수 있는 방법을 찾아 봄
  • flutter_html 을 통해서 태그가 적용된 텍스트를 보여줄 수 있음
  • flutter pub add flutter_html
import 'package:flutter_html/flutter_html.dart';

...

String htmlData = "<b>굵게</b> <i>기울임</i> <u>밑줄</u>";

Html(
data: htmlData,
)

✅ 마주한 문제점 1

  • 위의 전주는 태그로 감싸져 있어서 태그에 스타일을 주면 됐는데 그 이외의 비빔밥은 태그로 안감싸져 있어서 스타일을 줄 수가 없었다.
  • 아래처럼 b 태그는 스타일을 줄 수 있었지만 태그로 감싸지지 않은 텍스트는 스타일을 줄 수가 없었다.
Html(
data: htmlData,
style: {
"h1": Style(
fontSize: FontSize.xxLarge,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
"b": Style(
color: Colors.red,
),
},
),

✌️ 해결법

    • 를 통해서 전체에 스타일 속성을 줄 수 있었음
Html(
data: "<b>아중리</b>육전골",
style: {
"*": Style(
fontSize: FontSize(20),
color: Colors.black,
),
},
)


✅ 마주한 문제점 2

  • 또 Html 위젯은 자체적으로 패딩과 마진이 들어가 있어서 텍스트 위젯과 레이아웃이 맞지 않았다.
  • 따라서 padding과 margin을 0으로 줘서 해결할 수 있었음
Html(
data: title,
style: {
"*": Style(fontSize: FontSize(20), fontWeight: FontWeight.bold),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
},
),

4. Navigator 활용법

  • 이것은 트러블 슈팅이기보다는 페이지 이동에 사용되는 Navigator 가 아직 익숙하지 않아서 개념정리를 위해 이곳에 한번 더 적고자 한다.
  • 보내는 쪽
    • 페이지의 매개변수를 통해서 보낼 수 있음
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(data: "Hello from HomePage"),
),
);

  • 받는 쪽
    • 페이지의 매개변수를 통해서 받을 수 있음
class DetailPage extends StatelessWidget {
final String data;

const DetailPage({required this.data, super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Detail Page")),
body: Center(child: Text("받은 데이터: $data")),
);
}
}

  • 데이터를 보내는 쪽
Navigator.pop(context, "some result");
  • 데이터를 받는 쪽
final result = await Navigator.push(...);
print(result); // "some result"

5. Flutter Webview 에러

  • 에러 메시지
Exception has occurred.
_AssertionError ('package:flutter_inappwebview_platform_interface/src/in_app_webview/platform_inappwebview_widget.dart': Failed assertion: line 215 pos 7: 'InAppWebViewPlatform.instance != null': A platform implementation for flutter_inappwebview has not been set. Please ensure that an implementation of InAppWebViewPlatform has been set to InAppWebViewPlatform.instance before use. For unit testing, InAppWebViewPlatform.instance can be set with your own test implementation.)

✌️ 해결법

  • 아래 코드를 사용하니 해결이 되었음
flutter clean
flutter pub get
flutter run

에러의 원인

✅ 1. 빌드 캐시 문제

flutter buildflutter run을 여러 번 반복하다 보면, Flutter는 이전에 만든 빌드 결과물(.dart_tool, build/, .gradle 등)을 재사용하려고 하는데 라이브러리를 추가하거나 설정 파일이 바뀌었을 때, 예전 캐시가 잘못된 상태로 남아 있을 수 있음

flutter clean은 이걸 전부 지워버려서 깨끗한 상태로 다시 빌드하게 만드는 명령어

✅ 2. 네이티브(Android/iOS) 쪽 연결 안 됐던 경우

flutter_inappwebview는 Dart 코드만 있는 라이브러리가 아니라, 네이티브(Android, iOS) 코드랑 연결되는 플러그인임

  • flutter pub get으로 의존성은 가져왔는데,
  • 네이티브 플랫폼(Android/iOS)의 플러그인 설정이 빌드에 적용되지 않았던 상태일 수 있어

flutter clean + flutter run을 하면 네이티브 빌드부터 싹 다시 하기 때문에 해결이 됨.

✅ 3. pubspec.lock이 꼬였던 경우

라이브러리 버전 충돌이나 의존성 트리 꼬임으로 인해, 실제 설치된 플러그인이 정상적으로 연결되지 않았던 상황이 있었을 수도 있음

flutter clean이 이것까지 깔끔하게 리셋해줘.

⇒ 라이브러리 추가 후 이상한 에러가 날 땐 flutter clean 이 만병통치약

깃헙 프로젝트로 개인과제 관리하기

· 2 min read
Junseok Yang
Football Loving Programmer
  • 오늘은 개인과제를 시작하기에 앞서서 기능별로 github 이슈를 만들어서 이슈별로 브랜치를 만들어서 진행을 하고자 했다.

✍️ 이슈 만들기

이슈 작성 원칙

  • 제목
[Home] AppBar Title 표시
  • 내용
### 🪄 구현 항목
- AppBar Title 표시

PR 작성 원칙

  • 제목
[Home] AppBar Title 표시
  • 내용
### 🚀 개요
AppBar Title을 표시한다.

### 🔧 변경사항
- AppBar 생성
- AppBar 타이틀 변경

### 실행 화면
(이미지 첨부)

### 💡issue : #10

이슈 템플릿 설정

레포지토리 → settings → general → 하단의 Features - set up templates

⇒ 반복되는 내용을 템플릿으로 만들어 줬다.


이슈 내용

이슈는 총 7개로 생성을 했다.

  • Home Page UI 구현
  • [Home] 지역 검색 기능 구현
  • [Detail] Detail Page 만들기
  • [feat] 현위치의 주소를 조회한 뒤 네이버 API로 검색하는 기능 구현
  • [Docs] Readme 작성
  • [Docs] 트러블 슈팅 작성
  • [Enhancement] 코드 리펙토링하기

그리고 label

  • feat
  • bug
  • documentation

milestone은

  • home Page 제작
  • Detail Page 제작
  • Write down project documentation

이렇게 3가지로 구성을 했다.


⎘Next Step

  • 이 이슈에 맞춰서 브랜치를 나누고 기능개발이 끝나면 Pull Request를 해 main branch 로 merge를 하는 방식으로 진행할 예정!

지역 검색 앱 과제 시작!

· 6 min read
Junseok Yang
Football Loving Programmer

플러터 숙련 주차 개인 과제는

지역 검색 앱 만들기였다!

과제 Key Point

  • Reverpod 상태관리
  • API 활용하기
  • Firebase를 사용한 위치기반 검색 앱 제작
  • 안정성과 사용성
    • 안정성은 예외처리 잘 하기
    • 사용성 UI와 함께 User experience 잘 고려하기

기능 구현

  1. 네이버 검색 Open API를 이용하여 지역검색 기능 구현
  2. Riverpod을 이용하여 MVVM 구조로 프로젝트를 설계
  3. WebView를 사용하여 모바일 앱에서 웹사이트를 출력
  4. GPS를 사용하여 디바이스의 좌표값을 이용
  5. VWORLD OPEN API 를 이용

필수기능

HomePage 구현

  1. AppBar 구현
  2. ListView 구현
    1. ChildrenWidget
      1. title
      2. category
      3. roadAddress
  3. 네이버 검색 Open API를 애용해 지역 검색 기능 구현
    1. API키로 요청 테스트
    2. Location 모델 클래스 생성
    3. LocationRepository 클래스 생성 후 dio 패키지를 사용하여 검색 메서드 구현
    4. flutter_riverpod 패키지를 사용해 뷰 모델을 만든 후 HomePage 위젯과 연결
    5. DetailPage를 만든 후 Location 객체의 link 속성이 https 라는 문자열로 시작하면 DetailPage로 link를 전달하여 이동

도전 기능

현재 위치의 주소 조회 후 네이버 API로 검색

  1. geolocator 패키지를 사용하여 디바이스의 현재 좌표값을 가져오기
  2. HomePage 상태를 관리하는 뷰모델에 위도, 경도 값으로 VWORLD API에서 주소를 가져와 네이버 검색 API로 요청하는 메서드를 추가하기
  3. HomePage Actions 속성에 Icons.gps_fixed 아이콘 데이터를 사용하는 Icon 위젯을 배치
  4. Icon 위젯 터치 시 현재 위치를 가져와 뷰모델에 요청

Readme 작성

  • 이 프로젝트가 무엇인지? 를 알기 쉽게 전달해주는 중요한 역할
  • 프로젝트를 수행하며 조금씩 추가하기
    • 한 가지 기능을 완성했다면 바로 README에 작성하기
  • 간결하고 알기 쉽게

어떤 내용을 추가하면 좋은가?

  • 무엇을 위한 것인가
  • 이 프로젝트를 만들며 어떤 기술적 의사결정을 했는지
  • 어떻게 작동하는가
  • 어떤 트러블이 있었고 트러블 슈팅은 어떻게 진행했는가
  • 예시
    # 프로젝트 이름

    ## 📖 목차

    1. [프로젝트 소개](#프로젝트-소개)
    2. [팀소개](#팀소개)
    3. [프로젝트 계기](#프로젝트-계기)
    4. [주요기능](#주요기능)
    5. [개발기간](#개발기간)
    6. [기술스택](#기술스택)
    7. [서비스 구조](#서비스-구조)
    8. [와이어프레임](#와이어프레임)
    9. [API 명세서](#API-명세서)
    10. [ERD](#ERD)
    11. [프로젝트 파일 구조](#프로젝트-파일-구조)
    12. [Trouble Shooting](#trouble-shooting)

    ## 👨‍🏫 프로젝트 소개

    ## 팀소개

    ## 프로젝트 계기

    ## 💜 주요기능

    - 기능 1

    - 기능 2

    - 기능 3

    - 기능 4

    ## ⏲️ 개발기간

    - 2024.02.26(월) ~ 2024.04.04(목)

    ## 📚️ 기술스택

    ### ✔️ Language

    ### ✔️ Version Control

    ### ✔️ IDE

    ### ✔️ Framework

    ### ✔️ Deploy

    ### ✔️ DBMS

    ## 서비스 구조

    ## 와이어프레임

    ## API 명세서

    ## ERD

    ## 프로젝트 파일 구조

    ## Trouble Shooting

트러블 슈팅

  • [상황 인지]
    • 어떤 현상을 발견했다.
    • 오류가 발생했거나, 잘 작동하고 있어도 개선이 필요한 부분이거나 등
    • 꼭 ‘문제’가 아니더라도 트러블 슈팅을 작성할 수 있습니다! (프로그래밍에는 끝이 없다!)
  • [고민]
    • 이 상황이 생겼을까?
    • 어떻게 대응하면 좋을까?
  • [적용]
    • 내가 알고 있는 방법 적용하기
    • 팀원 또는 튜터님께서 알려주신 방법 적용하기
  • [결과]
    • 문제가 해결되었다
    • 코드가 개선되었다
    • 그래서 나는 무엇을 배웠는지

과제 진행

지금까지 과제는 급하게 하느라 Readme 및 트러블슈팅 기록을 자세히 기록하지 못했는데 그런 부분들을 보충해 가면서 진행하고 싶다.

그리고 Github 프로젝트를 진행해 내가 만들어야 되는 기능들을 Issue화 시켜서 이슈별로 브랜치를 나누고 푸시 컨벤션을 지난 조별과제에서 배웠던 것처럼 진행을 해야겠다! 비록 혼자서 하는 거지만 조별과제라고 생각하고 브랜치 전략과 이슈를 잘 나눠서 진행을 해봐야겠다!

플라워링 상품등록 페이지 만들기

· 14 min read
Junseok Yang
Football Loving Programmer

📚 오늘의 학습 내용

오늘은 팀 프로젝트에서 내가 맡았던 상품등록 페이지를 제작했다. html에서는 form 태그를 통해서 간단하게 input 태그로 텍스트 입력을 받을 수 있었는데 플러터에서는 이 부분이 익숙하지 않기 때문에 검색하면서 맨땅에 헤딩을 하면서 공부해 나갔다.

✍️ 주요 학습 내용

배운 내용

  • StatefulWidget과 StatelessWidget의 차이
  • 플러터에서 텍스트 입력 받기

새로 알게된 개념

StatefulWidget과 StatelessWidget의 차이

플러터 개발을 하면서 느낀 건 언제 StatefulWidget 을 써야 하고 언제StatelessWidget 를 써야 하는지가 애매했다. 기본적으로는 변하는 화면은 StatefulWidget , 변하지 않는 화면은 StatelessWidget 이라고 이해하고 있는데 TextField를 위젯으로 만든다면 텍스트의 값이 계속해서 변하고 바뀐 값을 보여줘야 하기 때문에 CustomTextField 는 StatefulWidget으로 해야 하는지 아니면 부모 위젯에서 상태값을 관리하니까 StatelessWidget 으로 해야 하나?? 이런 애매함이 있었다.

그래서 ChatGpt한테 질문을 했다.

✅ StatelessWidget을 쓸 때

  • 화면에 한 번 그려지면 변할 일이 없는 UI일 때
  • 예: 단순한 버튼, 텍스트, 아이콘 등

✅ StatefulWidget을 쓸 때

  • 변하는 값을 UI에 반영해야 할 때
  • 예: 버튼 누르면 숫자 증가, 체크박스 토글, 애니메이션, 입력 폼 등

새롭게 알게 된 것은 사용자 입력, 애니메이션, 비동기 처리 등이 필요하면 StatefulWidget 이기 때문에 입력폼은 StatefulWidget 을 사용했다.

✅ 핵심 개념부터 설명할게요

Flutter에서 TextField의 입력 값을 다루는 방법은 두 가지예요:

1. 부모에서 컨트롤러로 관리하는 방식 (더 흔함)

  • TextEditingController를 부모에서 만들고, 하위 위젯에 넘겨줌
  • 이 경우 하위 위젯은 StatelessWidget으로 충분함

2. 하위 위젯이 스스로 상태를 관리하는 방식

  • TextEditingController를 그 위젯 안에서 만들고 dispose도 직접 함
  • 이 경우엔 StatefulWidget이 필요함

⇒ 부모에서 컨트롤러만 만들어 주고 부모에서 관리하는 게 편하다는 것을 알게 됨

코드

🔸 방법 1: 부모(StatefulWidget)에서 컨트롤러 관리 → 자식은 StatelessWidget

class ProductFormPage extends StatefulWidget {
@override
_ProductFormPageState createState() => _ProductFormPageState();
}

class _ProductFormPageState extends State<ProductFormPage> {
final _titleController = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CustomTextField(
label: '상품명',
controller: _titleController,
),
],
),
);
}
}

class CustomTextField extends StatelessWidget {
final String label;
final TextEditingController controller;

const CustomTextField({
required this.label,
required this.controller,
});

@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: InputDecoration(labelText: label),
);
}
}

🔸 방법 2: TextField 위젯 자체가 상태를 관리 → StatefulWidget

class CustomTextField extends StatefulWidget {
final String label;
const CustomTextField({required this.label});

@override
_CustomTextFieldState createState() => _CustomTextFieldState();
}

class _CustomTextFieldState extends State<CustomTextField> {
late TextEditingController _controller;

@override
void initState() {
super.initState();
_controller = TextEditingController();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(labelText: widget.label),
);
}
}

플러터에서 텍스트 입력 받기
  1. 컨트롤러 객체 생성하기 (TextEditingController)

→ 입력값의 상태를 계속 관리를 해줌

  1. TextFormField 에 컨트롤러 연결해 주기
TextEditingController _controller = TextEditingController();

TextFormField(
controller : _controller,
keyboardType : TextInputType.text,
maxLines : 7,
maxLength : 500,
decoration : InputDecoration(labelText : widget.label),
)
  1. KeyboardType 정하기
  • keyboardType은 텍스트필드를 입력할 때 어떤 키보드가 올라올지(숫자, 한자)를 정해주는 것.
  • keyboardType.text

🧠 keyboardType: 어떤 키보드가 올라올지 결정하는 속성

사용자가 입력할 때, 어떤 키보드 레이아웃이 올라올지 정해줘요.

예를 들어:

dart
CopyEdit
TextField(
keyboardType: TextInputType.number,
)

이렇게 하면 👉 숫자 키패드가 올라와요!

📦 대표적인 keyboardType 종류

설명
TextInputType.text기본 키보드 (텍스트 입력용)
TextInputType.number숫자 키보드
TextInputType.emailAddress이메일 입력에 최적화된 키보드
TextInputType.phone전화번호 키보드
TextInputType.multiline여러 줄 입력 (이때 maxLines도 1보다 커야 해요)
TextInputType.urlURL 입력용 키보드
  1. maxLines, maxLength

🧠 maxLines: 몇 줄까지 입력받을 수 있게 할지

입력창이 한 줄만 보일지, 여러 줄이 될지를 정해줘요.

dart
CopyEdit
TextField(
maxLines: 1, // 기본값, 한 줄
)

이렇게 하면 텍스트가 길어져도 한 줄로만 보여요.

dart
CopyEdit
TextField(
maxLines: 5,
)

이렇게 하면 최대 5줄까지 입력 가능! → 설명처럼 긴 문장 받을 때 유용해요.


✅ 글자 수 제한: maxLength

dart
CopyEdit
TextField(
maxLength: 20,
)

이렇게 하면 사용자가 20자까지만 입력할 수 있어요.

입력창 아래에 "0 / 20" 이런 글자 수 카운터도 자동으로 보여줘요.

⚠️ 주의할 점

  • maxLength는 글자 수만 제한해요. → 사용자가 붙여넣기나 자동완성으로 넘어가도 자동으로 잘라주진 않아요.
  • 완전히 잘라내려면 inputFormatters를 써야 해요 (고급 옵션)
  1. dispose() 작성해 주기
class MyTextField extends StatefulWidget {
@override
_MyTextFieldState createState() => _MyTextFieldState();
}

class _MyTextFieldState extends State<MyTextField> {
late TextEditingController _controller;

@override
void initState() {
super.initState();
_controller = TextEditingController(); // 컨트롤러 생성
}

@override
void dispose() {
_controller.dispose(); // ✨ 컨트롤러 메모리 정리
super.dispose(); // 부모 클래스의 dispose도 꼭 호출!
}

@override
Widget build(BuildContext context) {
return TextField(controller: _controller);
}
}

🎯 dispose()란?

위젯이 화면에서 사라질 때, 메모리 정리를 해주는 함수야.


💡 왜 필요할까?

Flutter에서 TextEditingControllerAnimationControllerFocusNode 같은 걸 만들면,

  • *시스템 자원(메모리 등)**을 쓰게 돼.

근데 이걸 그냥 내버려두면 **앱이 느려지거나 메모리 누수(memory leak)**가 생길 수 있어.

그래서 "이 위젯 이제 안 쓰니까, 연결된 자원들도 깔끔하게 정리해줘!"라고 알려줘야 해.

그걸 해주는 게 바로 dispose()야.

✨ 기억할 것!

  • dispose()는 StatefulWidget일 때만 존재해
  • StatelessWidget은 한 번 그려지고 끝이라 정리할 게 없음

유효성 검사

✅ 목표: 상품명, 가격 같은 필드를 입력하고

  • 값을 안 넣으면 경고 메시지 뜨게 하기
  • 조건에 따라 커스텀 메시지도 보여주기
  • 버튼 누를 때 form이 유효하면만 등록 처리

🧩 구조 흐름

  1. TextFormField → 한 줄짜리 텍스트 입력 + 유효성 검사 가능
  2. Form 위젯으로 전체 감싸기
  3. GlobalKey<FormState>로 폼 상태 추적
  4. validator 함수로 각 필드 조건 검사
  5. 유효하지 않으면 붉은 경고 텍스트 + 빨간 밑줄
class _ProductFormPageState extends State<ProductFormPage> {
final _formKey = GlobalKey<FormState>();

final _nameController = TextEditingController();
final _priceController = TextEditingController();
  1. Submit 메서드 제작
void _submitForm() {
if (_formKey.currentState!.validate()) {
// 모든 유효성 통과 시
print("상품명: ${_nameController.text}");
print("가격: ${_priceController.text}");

showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('등록 성공'),
content: Text('상품이 등록되었습니다!'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('확인'),
),
],
),
);
} else {
// 유효성 실패 시 자동으로 에러 메시지 표시됨
}
}

🧠 핵심 설명 요약

기능설명
Form여러 TextFormField를 묶고 유효성 검사를 할 수 있게 함
GlobalKey<FormState>.validate()로 폼 전체의 상태 확인 가능
validator:각 필드에서 조건 검사 후 문자열을 리턴 → 오류 메시지 표시
.validate()모든 필드가 통과하면 true 리턴
경고창showDialog()로 간단하게 AlertDialog 띄우기 가능

🎯 꿀팁

  • TextField → 단순 입력만 가능
  • TextFormField → 폼 기반 입력 + 유효성 검사

Flutter에서 TextField랑 TextFormField는 비슷해 보이지만 역할과 기능이 다르다고 보면 돼.


✅ 간단 비교

항목TextFieldTextFormField
유효성 검사 (validation)❌ 직접 로직 만들어야 함✅ validator로 간편하게 검사 가능
Form과 연동❌ 불가능✅ Form 위젯과 연동 가능
입력값 자동 검사❌ 수동 구현✅ .validate()로 전체 검사
폼 제출❌ 직접 값 추적✅ FormState로 자동 관리
기본 용도단순 입력폼 입력 (회원가입, 상품등록 등)

🎯 언제 어떤 걸 써야 해?

  • ✍️ 그냥 메모나 검색창 같이 간단한 입력만 필요할 때 → TextField
  • 📋 회원가입, 상품등록, 로그인 등 "폼 기반 입력"일 때 → ✅ TextFormField → 👉 유효성 검사 필요할 땐 무조건 TextFormField 써야 편해요!

Validator 제작

//생성자
final String? Function(String?)? validator;

String? titleValidator(String? value) {
if (value == null || value.trim().isEmpty) {
return '상품명을 입력해 주세요!';
}
return null;
}
  • String 혹은 null이 반환된다
  • 반환값은 에러 메시지로서 텍스트필드 아래에 표시된다.

✅ TextFormField의 validator

  • return이 null이면: ✔️ 유효성 검사 통과!
  • return이 String이면: ❌ 검사 실패 → 그 문자열이 자동으로 아래 붉은 경고 텍스트로 표시돼요
dart
CopyEdit
validator: (value) {
if (value == null || value.isEmpty) {
return '상품명을 입력해 주세요'; // ← 이게 빨간 글씨로 필드 아래에 표시됨
}
return null;
}

즉, 이 return 값은 자동으로 화면에 표시되는 용도예요

따로 알림창을 띄우지 않아도 사용자한테 “여기 잘못됐어요!” 하고 알려줘요 👇

🌟 즉, validator는 각 필드의 상태를 체크하고, .validate()는 폼 전체가 유효한지를 체크해요!

입력 필드들은 Form 안으로 넣어야 함

Form이 TextFormField들을 감싸야 validator가 작동하고, 붉은 오류 메시지가 보여요!

✅ 요약

체크 항목설명
✅ TextFormField가 Form 안에 있음?꼭 감싸야 validator 작동해요!
✅ FormState.validate() 호출함?_formKey.currentState!.validate() 필요
✅ validator에 return '오류 메시지' 줬음?오류 시 메시지가 표시됨

🔁 유효성 검사 다시 실행하려면?

예를 들어, 사용자가 입력을 고쳤을 때 에러 메시지가 자동으로 사라지게 하려면:

dart
CopyEdit
onChanged: (_) {
if (_formKey.currentState != null) {
_formKey.currentState!.validate();
}
}

이걸 TextFormField의 onChanged 속성에 넣으면 돼요.

int.tryParse와 int.parse의 차이

✅ int.tryParse()

실패해도 앱이 안 죽어요

❌ int.parse()

실패하면 에러 나서 앱이 죽어요

실습한 내용

  • 텍스트 필드 제작

📝 코드 스니펫

// 오늘 배운 주요 코드
onChanged : (_){
if(_formKey.currentState != null){
_formKey.currentState!.validate();
}

📚 내일 학습할 내용

  • 상품등록 데이터 넘기기

💭 오늘의 회고

  • 아침부터 빡공

잘한 점 👍

  • 프로그래밍에 집중

개선할 점 🔨

배운 점 💡

✏️ 참고 자료

깃헙 푸시 권한 에러

· 9 min read
Junseok Yang
Football Loving Programmer

ChatGPT - GitHub Permission Error 403

깃으로 협업을 하면서 프로젝트를 진행하다 보니 코드충돌하는 것을 잡는 데 대부분의 시간을 사용해 버리게 되는 것 같다. 문제는 이러했다.

조장의 깃 레포지터리에 collaborator로 초대를 받았지만 계속해서 내 코드를 레포지터리에 푸시를 하려고 하면 푸시가 거절되는 에러 메시지가 나왔다.

에러 메시지

remote: Permission to Meezzi/flowerring.git denied to footgrammer.
fatal: unable to access 'https://github.com/Meezzi/flowerring.git/': The requested URL returned error: 403

그래서 전지전능하신 ChatGPT 의 답변으로는

means that GitHub is blocking access because the user footgrammer doesn’t have permission to push (or pull, depending on what you were doing) to the repository Meezzi/flowerring.

→ 내가 초대를 받았음에도 불구하고 푸시할 권한이 없다는 내용이었다.


챗 GPT의 해결책


🔍 원인과 해결 방법

1. GitHub 권한 문제

  • 현재 footgrammer 계정으로 로그인 되어 있는데, 이 계정은 Meezzi/flowerring 저장소에 권한이 없어요.

해결 방법:

  • Meezzi 계정으로 로그인하거나
  • 저장소의 소유자(Meezzi)가 footgrammer 계정에게 Collaborator(협업자) 권한을 줘야 해요.
    • 저장소 > Settings > Collaborators > 초대 → 해결 불가…

2. HTTPS 인증 문제 (토큰 혹은 캐시된 사용자 정보)

  • Git이 잘못된 사용자 정보나 토큰을 사용하고 있을 수 있어요.

해결 방법:

bash
복사편집
git config --global --unset credential.helper

그 다음 다시 push할 때 GitHub 사용자명과 패스워드(또는 Personal Access Token)를 입력해야 해요.

→ 해결 불가


3. SSH를 사용하는 방법

HTTPS 대신 SSH 방식을 쓰면 인증 문제가 적어요.

bash
복사편집
git remote set-url origin git@github.com:Meezzi/flowerring.git

그 전에 SSH 키를 등록해둬야 해요 (GitHub > Settings > SSH and GPG Keys).


이것을 하기 전에 HTTPS로 시도를 했다.

예전에 발급한 Personal Access Token이 있기 때문에 한번 시도를 해 봤다.

3️⃣ 올바른 인증 방식 사용하기

👉 HTTPS로 인증하려면 Personal Access Token(토큰) 필요!

GitHub은 이제 기본 비밀번호 대신 토큰 인증을 사용해요.

  1. GitHub 토큰 생성 페이지로 이동

  2. Generate new token (classic) 클릭

  3. repo 권한 포함한 토큰 생성

  4. clone할 때 아래처럼 사용

    bash
    복사편집
    git clone https://<your_token>@github.com/Meezzi/flowerring.git


SSH로 문제 해결하기

bash
복사편집
git remote set-url origin git@github.com:Meezzi/flowerring.git
git push origin main

1️⃣ 새 SSH 키 생성하기

아래 명령어를 실행하세요.

bash
복사편집
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

  • "your_email@example.com" 부분을 GitHub에 등록된 이메일로 바꿔주세요.
  • 실행하면 경로를 묻는데, 그냥 Enter를 눌러 기본 경로(~/.ssh/id_rsa)에 저장하세요.
  • 비밀번호 입력하는 부분이 나오면 그냥 Enter를 눌러도 되고, 원하면 설정해도 돼요.
    • 나는 그냥 enter로 넘어가 줌

2️⃣ SSH 키를 GitHub에 등록하기

SSH 키를 생성한 후, GitHub에 등록해야 해요.

SSH 키 복사

아래 명령어를 실행해서 SSH 키를 복사하세요.

bash
복사편집
cat ~/.ssh/id_rsa.pub

그러면 다음과 같은 형태의 키가 나올 거예요.

graphql
복사편집
ssh-rsa AAAAB3...yourSSHkey...== your_email@example.com

이걸 전체 복사(Ctrl+C) 하세요.

GitHub에 SSH 키 추가

  1. GitHub SSH 키 등록 페이지로 이동
  2. "New SSH key" 클릭
  3. Title: 원하는 이름 입력 (예: "My Laptop")
  4. Key: 아까 복사한 SSH 키 붙여넣기
  5. Add SSH key 클릭

3️⃣ SSH 연결 테스트

SSH 키가 제대로 등록되었는지 확인하려면, 아래 명령어를 실행하세요.

bash
복사편집
ssh -T git@github.com

성공 메시지 예시

vbnet
복사편집
Hi Meezzi! You've successfully authenticated, but GitHub does not provide shell access.

이 메시지가 나오면 SSH 설정이 완료된 거예요! 🎉


4️⃣ SSH 방식으로 Push 설정하기

이제 origin을 SSH 방식으로 변경하세요.

bash
복사편집
git remote set-url origin git@github.com:Meezzi/flowerring.git

그리고 다시 push 해보세요!

bash
복사편집
git push origin main


이제 SSH를 사용해서 GitHub에 Push할 수 있을 거예요! 🚀


궁금증

HTTPS vs SSH 비교

HTTPS와 SSH는 Git 저장소와 통신하는 두 가지 주요 프로토콜입니다.

HTTPS의 특징

  • 인증 방식: 사용자 이름과 비밀번호 또는 Personal Access Token을 사용
  • 장점:
    • 설정이 간단하고 초보자가 사용하기 쉬움
    • 방화벽이나 프록시에서도 잘 작동
  • 단점:
    • 매번 인증 정보를 입력해야 할 수 있음
    • 토큰 관리가 필요

SSH의 특징

  • 인증 방식: 공개키/개인키 쌍을 사용한 암호화 방식
  • 장점:
    • 한번 설정하면 추가 인증 불필요
    • 더 안전한 보안
    • 자동화된 작업에 적합
  • 단점:
    • 초기 설정이 비교적 복잡
    • 일부 네트워크 환경에서 제한될 수 있음

일반적으로 개발자들은 더 안전하고 편리한 SSH를 선호하며, 특히 팀 프로젝트나 지속적인 개발 작업에서 SSH가 더 효율적입니다.


공개키/개인키 암호화 방식 상세 설명

SSH는 비대칭 암호화를 사용하는데, 이는 공개키와 개인키라는 한 쌍의 키를 사용합니다.

키 쌍의 구성

  • 공개키 (Public Key): 누구나 볼 수 있는 키로, GitHub 서버에 등록됩니다.
  • 개인키 (Private Key): 사용자만 가지고 있는 비밀 키로, 절대 공유하면 안 됩니다.

작동 방식

  1. 사용자가 GitHub에 접속을 시도할 때, GitHub 서버는 암호화된 메시지를 보냅니다.
  2. 이 메시지는 사용자의 공개키로 암호화되어 있어서, 개인키를 가진 사용자만 해독할 수 있습니다.
  3. 사용자의 컴퓨터는 개인키를 사용해 메시지를 해독하고 적절한 응답을 보냅니다.
  4. 이 과정이 성공하면 인증이 완료되고 저장소에 접근할 수 있습니다.

보안상의 이점

  • 비밀번호와 달리 키 쌍은 추측하거나 무차별 대입 공격으로 해킹하기 거의 불가능합니다.
  • 개인키가 노출되지 않는 한 매우 안전한 인증 방식입니다.
  • 중간자 공격(man-in-the-middle attack)으로부터 보호됩니다.

이상으로 오늘의 에러 헨들링 내용은 끝!

깃헙 생각보다 어려운 놈이었다..