본문으로 건너뛰기

디자인 패턴 - MVVM

· 약 10분
정성엽 (lobe)
OTL 앱 개발자

앱을 개발하다 보면 자연스레 디자인 패턴이라는 단어를 접하게 됩니다. 디자인 패턴, 그게 뭘까요?

디자인 패턴이란 개발 과정에서 공통으로 발생하는 문제를 해결하기 위해 사용되는 패턴입니다. 이 말만 듣고는 어떤 문제를 해결하기 위함인지, 어떻게 적용되는지 알 수가 없죠. 우선 한 가지 예를 들어보겠습니다.

계산기 프로그램을 만들려고 하는데, 계산기 화면 구현과 사용자 입력, 입력값 처리 등의 모든 로직을 한 곳에 모두 작성했다고 합시다.

이후에 만약 계산기에 log를 추가해야 하는 요청이 들어와서 코드를 수정하려고 할 때, 눈앞이 캄캄해질 겁니다. 여기를 고치면 저기도 고쳐야 하고... 코드가 순식간에 지저분해집니다.

이때 필요한 것이 디자인 패턴입니다. 디자인 패턴을 적용해 코드의 여러 기능 및 로직을 분리해서 작성하면 유지보수에 도움이 되며 효율적인 코드 작성도 가능해집니다. 이런 디자인 패턴의 중요성은 코드의 복잡성과 의존성이 높은 규모가 큰 서비스를 개발할 때나, 다른 사람들과 협업할 때 더 부각됩니다. 주요 디자인 패턴들에는 MVC, MVP, MVVM 패턴이 있는데, 이 중 MVVM 패턴에 대해 얘기를 나눠보고자 합니다.

MVVM

MVVM 패턴은 아래의 세 가지로 구성되어 있습니다.

  1. Model
  2. View
  3. View Model

이름에서도 알 수 있듯 ModelViewViewModel 패턴인 거죠. 이제 각각의 역할과 동작 원리를 알아볼 텐데, 이해하기 쉽도록 View - View Model - Model 순으로 진행하겠습니다.

View

View는 앱의 UI와 관련된 부분입니다. 이름 그대로 사용자가 보는 화면 속 구조와 형태를 담당합니다. 사용자와의 상호작용을 통해 이벤트가 발생하면, 필요시 View Model을 호출합니다.

View Model

View Model은 View에 필요한 메서드 등이 구현된 곳으로, View의 UI에 있는 기능들이 정의되는 부분입니다. Model의 데이터를 가공해서 View에 전달하면 UI가 갱신됩니다. 오해하지 말아야 할 것은 View Model은 하나의 View에 종속되지 않으며, 필요시 여러 View에 참조될 수도 있습니다.

Model

Model은 앱에서 사용되는 데이터를 정의하고 다루는 부분이며, View Model과 마찬가지로 View로부터 독립적인 존재입니다.

동작

이를 정리하자면 앱은 아래의 흐름대로 동작합니다.

  1. 사용자의 입력이 View에 들어오면 View Model을 호출합니다.
  2. View Model은 필요한 데이터를 Model에 요청합니다.
  3. Model은 요청받은 데이터를 View Model에 응답합니다.
  4. View Model은 응답받은 데이터를 가공해서 저장합니다.
  5. View는 View Model과의 Data Binding으로 인해 갱신됩니다.

이제 MVVM 패턴이 무엇인지 대충 감은 잡힌 것 같은데, 이를 실제 개발에 적용하려니 아직 막막할 수 있습니다. 이해를 돕기 위해 MVVM 패턴이 적용된 간단한 Flutter 앱을 만들어 봅시다.

Flutter 앱에 적용

MVVM 패턴을 적용하여 간단한 메모 앱을 만들어 보려고 합니다. View, View Model, Model로 나누어 각각에 무엇이 들어가야 할지 대략적으로 정리해 보았습니다.

View

  • 메모 페이지(memo_view.dart)
    • 작성한 메모 목록
    • 메모 추가, 삭제 버튼
  • 메모 상세 페이지(memo_detail_view.dart)
    • 제목, 내용 입력 칸
    • 저장 버튼

View Model

  • 메모 뷰 모델(memo_view_model.dart)
    • 작성한 메모 목록 불러오기
    • 메모 추가, 수정, 삭제

Model

  • 메모 모델(memo.dart)
    • 제목, 내용, 수정 날짜

파일 구조는 아래와 같습니다만, 개발자가 정하기 나름이므로 똑같을 필요는 없습니다.

  • lib
    • models
      • memo.dart
    • viewmodels
      • memo_view_model.dart
    • views
      • memo_view.dart
      • memo_detail_view.dart
    • main.dart

먼저 pubspec.yaml에 아래 패키지들을 설치해 줍니다. 버전은 2023년 5월 15일 기준 가장 최신 버전입니다.

provider: ^6.0.5
shared_preferences: ^2.1.1
intl: ^0.18.1

아래에 순서대로 코드를 나타내었습니다.

memo.dart

import 'package:intl/intl.dart';

class Memo {
final String title, content;
final DateTime time;

Memo({
required this.title,
required this.content,
required this.time,
});

static List<Memo> listFromString(
List<String> titlelist, List<String> contentlist, List<String> timelist) {
if (titlelist.isEmpty) return [];
return List.generate(
titlelist.length,
(idx) => Memo(
title: titlelist[idx],
content: contentlist[idx],
time: DateTime.tryParse(timelist[idx]) ?? DateTime.now(),
));
}

static List<String> titleList(List<Memo> memolist) {
return memolist.map((e) => e.title).toList();
}

static List<String> contentList(List<Memo> memolist) {
return memolist.map((e) => e.content).toList();
}

static List<String> timeList(List<Memo> memolist) {
return memolist
.map((e) => DateFormat('yyyy-MM-dd HH:mm:ss').format(e.time))
.toList();
}
}

memo_view_model.dart

import 'package:flutter/material.dart';
import 'package:mvvm_example/models/memo.dart';
import 'package:shared_preferences/shared_preferences.dart';

class MemoViewModel extends ChangeNotifier {
List<Memo> _memolist = [];

List<Memo> get memolist => _memolist;

MemoViewModel() {
getMemo();
}

Future<void> getMemo() async {
final prefs = await SharedPreferences.getInstance();

_memolist = Memo.listFromString(
prefs.getStringList('title') ?? [],
prefs.getStringList('content') ?? [],
prefs.getStringList('time') ?? []);

notifyListeners();
}

void updateMemo({Memo? oldMemo, Memo? newMemo}) {
if (newMemo != null) {
_memolist.remove(oldMemo);
_memolist.insert(0, newMemo);
}
_saveMemo();
}

void removeMemo(Memo memo) {
_memolist.remove(memo);
_saveMemo();
}

Future<void> _saveMemo() async {
final prefs = await SharedPreferences.getInstance();

prefs.setStringList('title', Memo.titleList(memolist));
prefs.setStringList('content', Memo.contentList(memolist));
prefs.setStringList('time', Memo.timeList(memolist));

notifyListeners();
}
}

memo_view.dart

import 'package:flutter/material.dart';
import 'package:mvvm_example/models/memo.dart';
import 'package:mvvm_example/viewmodels/memo_view_model.dart';
import 'package:mvvm_example/views/memo_detail_view.dart';
import 'package:provider/provider.dart';

class MemoView extends StatelessWidget {
const MemoView({super.key});

@override
Widget build(BuildContext context) {
MemoViewModel viewModel = Provider.of<MemoViewModel>(context);
return Scaffold(
appBar: AppBar(
title: const Text('Memo'),
centerTitle: true,
),
body: GridView.builder(
itemCount: viewModel.memolist.length,
padding: const EdgeInsets.all(20),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 3 / 4,
),
itemBuilder: (context, idx) =>
memoCard(context, viewModel.memolist[idx]),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
Memo? newMemo = await Navigator.push(
context, pageRouteBuilder(const MemoDetailView()));
viewModel.updateMemo(newMemo: newMemo);
},
child: const Icon(Icons.add),
),
);
}

Widget memoCard(BuildContext context, Memo memo) {
MemoViewModel viewModel = Provider.of<MemoViewModel>(context);
return InkWell(
onTap: () async {
Memo? newMemo = await Navigator.push(
context, pageRouteBuilder(MemoDetailView(memo: memo)));
viewModel.updateMemo(oldMemo: memo, newMemo: newMemo);
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
memo.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
GestureDetector(
onTap: () => viewModel.removeMemo(memo),
child: const Icon(
Icons.close,
size: 16,
),
),
],
),
Expanded(
child: Text(
memo.content,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w300,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
);
}

PageRouteBuilder<Memo> pageRouteBuilder(Widget page) {
return PageRouteBuilder(
pageBuilder: (_, __, ___) => page,
transitionDuration: const Duration(milliseconds: 200),
reverseTransitionDuration: const Duration(milliseconds: 200),
opaque: false,
transitionsBuilder: (_, animation, __, child) {
return SlideTransition(
position: animation.drive(
Tween(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.ease)),
),
child: child,
);
},
);
}
}

memo_detail_view.dart

import 'package:flutter/material.dart';
import 'package:mvvm_example/models/memo.dart';
import 'package:intl/intl.dart';

class MemoDetailView extends StatefulWidget {
const MemoDetailView({super.key, this.memo});
final Memo? memo;

@override
State<MemoDetailView> createState() => _MemoDetailViewState();
}

class _MemoDetailViewState extends State<MemoDetailView> {
late final TextEditingController title, content;

@override
void initState() {
super.initState();
title = TextEditingController(text: widget.memo?.title);
content = TextEditingController(text: widget.memo?.content);
}

@override
void dispose() {
title.dispose();
content.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
if (title.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 1),
content: Row(
children: const [
Icon(
Icons.close,
color: Colors.red,
),
Text('제목을 입력해주세요'),
],
),
));
} else {
Navigator.pop(
context,
Memo(
title: title.text,
content: content.text,
time: DateTime.now(),
));
}
},
icon: const Icon(Icons.check),
),
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Column(
children: [
form(
controller: title,
hintText: '제목',
),
const SizedBox(height: 12),
Expanded(
child: form(
controller: content,
maxLines: null,
hintText: '내용',
),
),
Container(
height: 60,
alignment: Alignment.center,
child: Text(DateFormat('yyyy.MM.dd HH:mm')
.format(widget.memo?.time ?? DateTime.now())),
),
],
),
),
);
}

Widget form({
TextEditingController? controller,
int? maxLines = 1,
required String hintText,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.circular(8),
),
child: TextFormField(
controller: controller,
maxLines: maxLines,
decoration: InputDecoration(
border: InputBorder.none,
hintText: hintText,
),
),
);
}
}

마무리

지금까지 MVVM 패턴에 대해 알아보고 이를 적용해 간단한 메모 앱까지 만들어 보았습니다. MVVM 패턴을 처음 접하는 분들께 도움이 되었길 바라며 마치겠습니다.

Sessions vs JWT

· 약 15분
김효경 (diana)
SPARCS를 사랑하는 개발 새내기

Sessions vs JWT 입문 (동영상)

세션 vs 토큰 vs 쿠키? 기초개념 잡아드림. 10분 순삭!

Cookies

토큰 vs 쿠키? 는 틀린 질문

쿠키를 이용해서 서버는 사용자의 브라우저에 데이터를 저장한다.

작동 원리

서버에 request를 보내고 받은 response에 cookies가 있을 수 있다.

그렇게 해서 저장해 놓은 cookies를 서버에 request 보낼 때마다 같이 보낸다.

특징

  • 쿠키는 도메인에 따라 제한이 된다.
  • 쿠키는 (서버가 정한 기간에 따라) 유통기한이 있다.
  • 쿠키는 인증 정보뿐만 아니라, 여러 정보를 저장할 수 있다.
    • e.g., 웹사이트 언어 설정을 바꾸면, 서버는 쿠키를 주어서 사용자의 언어 설정을 저장한다. 이 후에, 쿠키는 request와 함께 서버로 보내지고, 서버는 언어 설정에 해당하는 response를 줄 수 있다.
  • 공간 제약이 있다.

세션과 토큰이 필요한 이유

HTTP (웹사이트를 이용할 때 쓰는 프로토콜)은 stateless

서버로 가는 모든 request가 이전 request와 독립적으로 다뤄진다. 즉, 요청이 끝나면, 서버는 그 요청에 대한 정보는 기억하지 못한다.

→ 요청을 보낼 때마다, 사용자에 대한 정보를 알려주어야 한다.

Sessions

작동 과정

  1. 유저명과 비밀번호를 서버에 보낸다
  2. 비밀번호가 맞다면 Session DB에 user 정보를 저장한다.
  3. 각 session마다 별도의 ID가 있고, 이를 쿠키를 통해 브라우저로 돌아와 쿠키에 저장된다.
  4. request를 보낼 때 session ID를 쿠키에 담아 보내게 된다.
  5. 세션 ID를 세션 DB에서 확인한다.
  6. 유저 정보에 해당하는 response를 준다.

특징

  • 중요한 유저 정보는 모두 서버가 가지고 있다.
    • 모든 request에 대해서 쿠키를 확인하여, DB를 찾고 저장하는 작업을 서버가 해야한다. 즉, user가 늘어나면 DB 리소스가 더 필요하다.
  • 쿠키가 세션 ID를 전달하기 위한 매개체 역할을 한다.
  • 세션을 이용하여 안드로이드. iOS를 만들 수 있지만, 쿠키는 사용할 수 없다. → 쿠키 대신 토큰을 사용한다.

장점

  • 다양한 기능을 추가할 수 있다.
    • e.g., 특정 유저를 쫓아내고 싶을 때
    • e.g., 원하지 않는 디바이스에서 강제 로그아웃
    • e.g., 로그인한 계정 개수를 알 수 있고, 제한할 수 있다. (넷플릭스)

단점

  • DB를 사고, 유지해야 한다.
  • 유저가 늘어날 수록 DB도 커진다.
  • 주로 redis를 이용한다.
    • 해당 목적을 수행하기 위한, 빠르고, 저렴한 DB

JWT

토큰 형식

작동과정

  1. 유저명과 비밀번호를 서버에 보낸다.
  2. 유저명과 비밀번호가 맞다면 서버는 ‘사인된 정보’(JWT)를 string 형태로 클라이언트에 보낸다.
  3. request를 보낼 때 ‘사인된 정보’(JWT)를 서버에 보낸다.
  4. 서버는 토큰을 받으면, 해당 사인이 유효한지 체크한다.

특징

  • 세션 DB가 필요없다.
  • 길이에 제약이 없다.
  • JWT는 암호화되지 않았다.

장점

  • 서버가 많은 정보를 저장하고 있다.

단점

  • 다양한 기능을 할 수는 없다.
    • e.g., 강제 로그아웃 X (해당 토큰이 만료되기 전까지 유효)

JWT 입문 (동영상)

JWT 대충 쓰면 님들 코딩인생 끝남

JWT token

JWT(JSON Web Token)에 대해서... :: Outsider's Dev Story

JWT의 구조

JOSE 헤더

JWT Claim Set

Signature

앞에서 보았듯이 JOSE 헤더와 JWT Claim Set은 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이다. 그래서 누구나 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. 토큰을 사용하는 경우 이 토큰을 다른 사람이 위변조할 수 없어야 하므로 JOSE 헤더와 JWT Claim Set가 위변조되었는지를 검증하기 위한 부분이 Signature 부분이다. JOSE 헤더와 JWT Claim Set는 JOSE 헤더와 JWT Claim Set를 base64로 인코딩해서 만든 두 값을 마침표(. )로 이어 붙이고 JOSE 헤더에서 alg 로 지정한 알고리즘 HS256  즉, HMAC SHA-256으로 인코딩하면 JWT 토큰의 세 번째 부분인 Signature를 만든다.

💡 추가 설명 > JWT(Json Web Token) 알아가기 > header를 디코딩한 값 + “.” + payload를 디코딩한 값을 위처럼 합치고 이를 your-256-bit-secret, 즉, 서버가 가지고 있는 개인키를 가지고 암호화한 것이 Signature입니다. 따라서 signature는 서버에 있는 개인키로만 암호화를 풀 수 있으므로 다른 클라이언트는 임의로 Signature를 복호화할 수 없습니다.

JWT Claim Set

JWT의 구조를 설명하기 위해서 앞에서는 직접 토큰을 만들었지만 실제로 사용하게 되면 언어별로 있는 JWT 라이브러리를 사용해서 토큰을 만들 것이므로 이 과정을 직접 수행하는 경우는 별로 없다. JWT 사이트에 언어별 추천 라이브러리가 있고 현재 지원상황까지 표시되어 있으므로 비교해서 사용하면 된다. 이런 라이브러리는 취약점에 대한 대처가 중요하므로 세부 내용을 자세히 아는 것이 아니라면 그냥 여기 나와 있는 라이브러리를 사용하기를 추천한다. 이전에 작성한 글에서는 Node.js JWT 라이브러리로 jwt-simple을 사용했지만, 개발이 거의 진행되고 있지 않아서 지금은 jsonwebtoken으로 갈아탔다.

JWT에서 토큰의 정보를 클레임이라고 부르기 때문에 이 정보를 모두 가지고 있는 바디 부분을 Claim Set이라고 부르고 Claim Set은 키 부분인 Claim Name과 값 부분인 Claim Value의 여러 쌍으로 이루어져 있다. Claim Name으로 사용 가능한 값에는 3가지 분류가 있는데 등록된 클레임 이름(Registered), 공개 클레임 이름(Public), 비밀 클레임 이름(Private)이다. 등록된 클레임 이름은 IANA JSON Web Token Claims에 등록된 이름이고 필수값은 아니지만 공통으로 사용하기 위한 기본값이 정해져 있다. 아래 목록이 등록된 클레임 이름인데 모두 선택사항이다.

  • iss: 토큰을 발급한 발급자(Issuer)
  • sub: Claim의 주제(Subject)로 토큰이 갖는 문맥을 의미한다.
  • aud: 이 토큰을 사용할 수신자(Audience)
  • exp: 만료시간(Expiration Time)은 만료시간이 지난 토큰은 거절해야 한다.
  • nbf: Not Before의 의미로 이 시간 이전에는 토큰을 처리하지 않아야 함을 의미한다.
  • iat: 토큰이 발급된 시간(Issued At)
  • jti: JWT ID로 토큰에 대한 식별자이다.

공개된 클레임이름은 토큰에서 사용하기 위해서 정의했지만, 충돌을 방지하기 위해서 공개된 이름이고 비밀 클레임이름은 서버와 클라이언트가 협의로 사용하는 이름을 의미한다.

JWT의 사용

JWT가 다른 토큰하고 가장 다른 부분은 토큰 자체가 데이터를 가지고 있다는 점이다. API 서버를 직접 구현한 적이 많지는 않지만, 일반적으로 토큰 기반의 인증을 구현한다면 API 요청 시 헤더나 파라미터에 엑세스토큰을 가져오도록 하고 이 토큰을 보고 인증한다.(서비스에 따라서 앱 ID나 비밀키를 같이 사용하기도 하지만 여기서는 JWT 범주가 아니므로 여기서는 엑세스토큰만 얘기한다.)

일반적인 토큰의 흐름을 생각한다면 API 요청 시에 들어온 토큰을 보고 이 토큰이 유효한지 확인하게 된다. 보통은 데이터베이스에 토큰을 저장해 놓고 만료시간이나 토큰의 사용자 등을 저장해 놓고 유효한 토큰인지 등을 검사하고 유효한 경우 해당 사용자라고 인식하고 이 사용자의 권한으로 사용할 수 있는 정보를 조회하게 된다. 요청마다 데이터베이스를 조회하는 것은 비용이 꽤 크므로 캐시서버 등을 두어 성능을 높이기도 한다.

Auth0 - jwt token을 만든 단체

Application Session Management

django에서 session 구현

Django Tutorial Part 8: User authentication and permissions - Learn web development | MDN

모바일 앱에서 Sessions vs JWT

세션이란? (feat.모바일 앱에서 세션)

모바일 앱에서 세션 관리

결론부터 말하면, 모바일 환경에서도 세션과 유사한 방식을 사용할 수 있지만, 잘 사용하지는 않는다.HTTP 프로토콜을 사용하면, 결국 논리성의 연속성을 유지하기 위해서 세션과 비슷한 개념이 모바일에도 필요하다. 하지만, 세션이나 쿠키와 같은 방식을 주로 사용하지는 않는 것으로 알고 있다.

모바일과의 네트워킹에서 브라우저에서의 세션과 다른 방식이 사용되는 이유는 다음과 같다.

1. 정보 유지 방식이 다르다

웹에서는 세션을 발급 받으면, 이를 쿠키에 넣어두고 다시 서버와 소통할 때 사용한다.하지만, 앱은 그 자체로 파일 시스템에 엑세스 할 수 있을 뿐 아니라, 내부 DB도 가질 수 있기 때문에 쿠키를 통해 통신할 필요가 없다.서버에서 받은 유저 식별 데이터를 따로 저장해서 통신할 때 사용하면 된다. 이 값은 보통 앱이 종료되고 재실행 돼도, 로그아웃하지 않는 이상 지워지거나 갱신되지 않는다.즉, 동작 방식은 동일하지만, 세션처럼 관리될 필요는 없는 것이다.TTL을 따로 설정할 필요가 없고, 세션같이 종료시에 expire 될 필요도 없다.또한 앱에서는 모든 정보를 서버에게 요청하는 것이 아니라, 많은 양 클라에서 가지고 있고 서버에서 받은 내용도 캐싱 되는 경우가 대부분이기 때문에, 논리적 연결성 또한 기본적으로 성립되어 있다.

2. 보안 수준

브라우저에서는 기본적으로 경로가 주소에 공개되어 있고, 환경 자체가 쿠키 값을 파악하거나 중간 탈취할 가능성이 크기 때문에, 세션을 사용하는 것이 필요하다.하지만 앱은 기본적으로 사용자와 1대1 매칭되며, 정보 탈취 가능성이 낮기 때문에 해당 방식과 유사한 방법을 사용한다.

※ 단, 웹뷰등으로 구현하는 하이브리드 앱에서는 세션 관리가 필요하다.

Flutter에서의 State management

· 약 8분

안녕하세요! 현재 뉴아라 앱 개발을 담당하고 있는 김상오(alvin)입니다. 앱을 개발하면서 고려해야할 사항들은 매우 다양하지만 그 중에서도 중요하게 다루어져야 하는 부분 중 하나가 바로 상태 관리(State management)라고 생각합니다. Flutter에서는 다양한 state 관리 방법을 지원하고 있으며 현재 뉴아라 및 다른 스팍스 앱에서는 상태 관리에 Provider라는 디자인패턴을 활용하고 있습니다. 저는 이번 기술블로그를 통해 Provider 및 Flutter에서 제공하는 다른 state 관리 방법에는 무엇이 있는 지에 대해 알아보고자 할 것입니다. 각각의 state 관리 방법에 대한 설명 및 간단한 Counter 애플리케이션 코드를 통해 예시를 제공할 것입니다.

setState() 함수 이용하기

아래 소개될 다른 방법보다 비교적 low-level한 상태 관리 방법입니다. setState() 함수를 이용하면 단일 위젯에서 state를 간단하고 편리하게 관리할 수 있습니다. 아래 예시는 setState() 함수를 이용한 Counter 앱 예시 코드입니다. _counter 변수의 값을 setState() 함수를 이용하여 업데이트하는 것을 확인할 수 있습니다.

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++; // setState()를 이용하는 부분
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
}

위 코드에서 알 수 있듯이 setState() 함수는 다른 state 관리 방법보다 간편합니다. 따라서 단일 위젯에서 state를 관리하는 경우에 setState() 함수가 가장 좋은 방법이 될 수 있습니다. 그러나 이 방법은 앱의 규모가 커짐에 따라 state 관리가 복잡해지면 사용에 부적합해집니다. 특정 위젯이 다른 위젯들과 state를 공유해야 하는 경우에 setState() 함수로만 구현하기에는 어렵습니다. 그리고 setState()를 호출하는 경우 해당 위젯과 그 하위에 있는 위젯들이 다시 빌드되는데 위젯트리가 깊을 경우 비효율적일 수 있습니다.

Provider

Provider는 BuildContext를 이용한 의존성 주입(Dependency Injection) 패턴을 기반으로 합니다. 의존성 주입이란 위젯에서 필요한 객체, state를 외부에서 주입하여 의존성을 해결하는 디자인 패턴입니다. 이를 통해 코드의 결합도를 낮출 수 있게 됩니다. Provider에서는 의존성 주입을 통해 상위 위젯에서 생성된 객체 및 state를 하위 위젯으로 전달할 수 있고 이를 통해 하위 위젯에서 필요로 하는 데이터를 쉽게 사용할 수 있습니다. Provider에서는 의존성 주입을 위해 Provider.of, Consumer 등의 위젯을 제공합니다.

Provider를 이용한 Counter 앱 구현은 아래와 같습니다.

class Counter with ChagneNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners(); // state가 변경되었음을 구독자들에게 알려주는 역할
}
}
import ...

void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp();
),
);
}

class MyApp extends StatelessWidget {
...
return MaterialApp(
home: Home(),
);
}

...
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
...
Text(
Provider.of<Counter>(context, listen: true).counter,
),
);
}
}

GetX

GetX는 단순하고 직관적인 API를 제공하여 효율적으로 state 관리를 할 수 있도록 도와줍니다.

GetX를 통한 state 관리 방식은 크게 두 가지로

  1. 단순 상태 관리
  2. 반응형 상태 관리

가 있습니다. 위 두가지 방식의 차이점은 단순 상태관리의 경우 설정한 값이 변하지 않아도 무조건 호출이 됩니다. 그러나 반응형 상태관리의 경우에는 값이 변해야 호출이 이루어 집니다.

첫 번째 단순 상태 관리와 같은 경우 아래의 예시와 같은 방식입니다.

import 'package:get/get.dart';

// 단순 상태 관리를 위한 controller를 생성해줍니다
class SimpleController extends GetxController {
int counter = 0;

void increment() {
counter++;
update(); // update는 SimpleController가 등록된 모든 코드에 업데이트를 알리는 역할을 담당합니다
}
}
class Home extends StatelessWidget {
...
Widget build(BuildContext context) {
Get.put(SimpleController()); // controller 등록
return Scaffold(
...
body: Center(
child: GetBuilder<SimpleController>( // GetBuilder 아래의 모든 위젯은 controller의 데이터를 읽을 수 있습니다
builder: (controller) {
return ElevatedButton(
child: Text(controller.counter),
onPressed: () => controller.increment(),
),
},
),
),
);
}
}

GetBuilder()를 사용하지 않을 경우 Get.find를 사용할 수도 있습니다

다음으로 반응형 상태 관리에 대한 예시 코드입니다.

import 'package:get/get.dart';

class ReactiveController extends GetxController {
RxInt counter = 0.obs; // 변수의 타입은 Rx[TypeName] 입니다. 또한 변수의 값에 .obs를 붙입니다
// update()를 부르지 않아도 됩니다

void increase() {
counter++;
}
}

단순 상태 관리에서는 GetBuilder를 사용했다면 반응형 상태 관리에서는 Obx, GetX로 두 가지의 방법이 있습니다.

Obx를 이용한 counter 값 출력은

Obx(() => Text(
'${Get.find<ReactiveController().counter.value>}', // .value를 붙여야 합니다
),),

GetX를 이용하면

GetX(
builder: (_) {
return Text(
'${Get.find<ReactiveController().count.value>}',
);
},
),

GetX의 경우 위젯의 원하는 부분만 업데이트하는 것 및 사용법을 배우는 것도 쉽다는 장점이 있습니다. 그러나 BuildContext를 이용한 의존성 주입이 사용되지 않고 BuildContext가 글로벌하게 사용되므로 위험성이 있습니다.

마무리하며

이번에 flutter state 관리 방법에 대해서 조사하며 다양한 state 관리 방법을 비교한 글을 볼 수 있었고 현재 뉴아라 및 다른 스팍스 앱에서 사용하고 있는 Provider가 가장 적합한 방법일 것 같다는 생각이 들게 되었습니다. 상태 관리 방법은 위에서 언급한 것들 외에도 InheritedWidget, BLoC 등 더 있고 다음 기술블로그 때 다뤄보도록 하겠습니다!

SEO

· 약 11분
고예준 (arcticfox)
종강 주세요

SPARCS에서 NewAra front 개발자로 활동중인 고예준(arcticfox)입니다. 최근 NewAra 서비스 글 공유에 관해서 찾아보던 도중 메타 태그에 관해서 많은 것들을 읽어 보았습니다. 그런데 대부분의 글에서 빠지지 않던 키워드가 있었습니다. 바로 ‘SEO’입니다. 그래서 이번 글에서는 SEO가 무엇인지 다루어 보려고 합니다.

SEO란?

seo

SEO(Search Engine Optimization, 검색 엔진 최적화)란 우리가 만든 웹 사이트가 검색 결과에 더 잘 보이도록 사이트를 최적화 하는 과정을 말한다. 검색 엔진은 기본적으로 웹을 크롤링하며 페이지에 있는 링크들을 따라가고, 찾은 컨텐츠들의 색인을 생성한다. 우리가 검색 엔진에 키워드를 검색했을 때 보이는것은 이 때 저장된 컨텐츠의 색인이다. 검색 엔진의 크롤러는 일정한 규칙을 따라서 링크를 이동하기에, SEO를 진행하며 해당 규칙을 밀접하게 만들면 웹 사이트의 검색 결과가 보다 높은 곳에 노출되도록 만들 수 있다. 일부 검색 엔진들은 SEO 가이드 라인을 제공하긴 하지만 우리가 흔히 사용하는 대형 검색 엔진들의 경우 결과 랭킹은 대부분 영업 비밀에 해당한다. 따라서 SEO는 검색 엔진에 대한 경험적 지식과 논문과 특허 등에서 가져온 이론적 지식이 필요하다.

SEO는 왜 필요할까?

우리는 궁금한것을 찾아보거나 무언가를 구매하고 싶을 때 가장 먼저 하는것은 구글과 네이버와 같은 검색엔진에 검색을 해보는것이다. 그리고 일반적으로 우리는 검색 결과의 상단에 있는 사이트나 이미지를 클릭해 본다. SEO를 한다는 것은 사용자가 검색 엔진에 특정 키워드를 검색 했을 때 우리의 사이트를 검색 결과에 상단에 보여줄 수 있도록 하는 작업이다. 이를 통해 웹사이트의 사용자의 유입을 늘릴 수 있다. 또한, SEO를 이용해 사용자들이 어떤 키워드를 중심으로 우리의 웹사이트를 방문하고 있는지 파악할 수 있다.

SEO의 분류

SEO는 최적화를 하는 방법에 따라 크게 2가지로 구분된다.

  • On-Page SEO
    • On-Page SEO는 웹 사이트 내부에서 검색엔진의 최적화를 위해 진행되는 일련의 작업을 의미한다.
    • ex: H태그, 메타태그, 이미지 최적화, 콘텐츠 최적화, 글다의 수 , 내부링크
  • Off-Page SEO
    • Off-Page SEO는 웹 사이트의 외부에서 검색 엔진 최적화를 위해 진행하는 일련의 작업을 의미한다.
    • ex: 관련성, 신뢰성, 권한, 사이트로 향하는 백링크

SEO의 방향성

seo의 방향성은 크게 세가지 방향으로 나눌 수 있다

  • 기술적
    • 콘텐츠 마크업 작성시 semantic html을 사용하면 색인하길 원했던 컨텐츠를 지정할 수 있다.
  • 콘텐츠 작성
    • 방문자 층에세 적합한 언어로 콘텐츠를 작성한다. 텍스트 뿐만 아니라 이미지도 사용하여 크롤러의 주체가 이해할 수 있도록 콘텐츠를 작성한다.
    • 많은 사람들이 검색 해볼만한 키워드를 찾아내어 컨텐츠 작성 시 적절히 삽입한다.
  • 인기도
    • 접속자가 많은 사이트에 사이트를 링크 시티면 더 많은 트랙픽을 발생시켜 노출되는 순위를 높일 수 있다.

구글 검색 엔진 최적화

대한민국에서 가장 많이 사용되고 있는 검색 엔진 3가지는 ‘Naver’, ‘Google’, ‘Daum’ 이다. 유저 점유율만으로 본다면 ‘Naver’가 ‘Google’, ’Daum’ 보다 높지만 이용자의 성향에 따라 ‘Google’이 더 높은 경우도 존재한다. 이 글에서는 구글의 검색 엔진 최적화에 대해서 조금 더 알아보고자 한다.

구글 검색 엔진 최적화는 왜 필요한가?

키워드에 따라 차이가 있지만 동일한 키워드를 네이버, 다음, 구글 3곳 포털에 넣어보면 네이버와 다음은 구글에 비해 광고성 메시지와 검색 엔진 내 플랫폼 게시글들을 보다 많이 노출시킨다. 따라서 정말 원하는 정보를 찾고 이를 구분하는데 시간이 오래 걸린다. 물론 구글도 광고성 메시지가 존재하긴하지만 정보를 찾는데 신경 쓰일 만큼은 아니고 해당 티워드와 연관성이 높은 ‘웹사이트’를 주로 노출시키게 된다. 따라서 구글을 사용하는 사용자의 빈도수는 점점 증가하게 된다. 쇼핑의 영역에서는 구글보다 네이버와 다음이 더 좋은 정보를 제공할 수 있다. 하지만 구글은 지식, 컨텐츠 적인 측면에서는 네이버나 다음보다 확실히 유리하다. 따라서 콘텐츠를 기반으로 하는 웹사이트라면 이런 영역에서 점유율을 높이기 위해 구글 검색 엔진 최적화가 필요하다.

그렇다면 구글이라는 검색 엔진에 맞춰 최적화를 진행하면 얻을 수 있는 효과, 혜택은 무엇일까? 바로 구글에 키워드를 검색 했을 때 우리의 웹사이트의 결과를 유저들이 일반적으로 가장 많이 보는 상단에 노출시켜 주는 것이다. 이러한 혜택이 우리가 원하는 키워드에 전부 적용이 된다면, 광고 보다 더욱 큰 효과를 가져올 수 있다.

어떻게 최적화 할것인가

앞서 말한것처럼 대부분의 검색 엔진들은 ‘우리 검색 엔진의 로직은 이렇게 되어있어요’라고 공개하지 않는다. 구글 또한 검색 엔진 로직을 공개하지 않았다. 하지만 구글에서는 ‘이렇게 하면 우리 검색엔진에서 최적화가 될 수 있습니다’ 라는 가이드를 제공해준다. 구글의 검색 엔진 최적화 문서를 살펴보면 ‘사이트 표기, 콘텐츠 구조, 메타 태그, 사이트 계층 구조, 이미지 최적화, 모바일 친화성’등 이러한 것들은 기본적으로 꼭 해야 된다 등의 내용들을 알려준다.

SEO 기본 가이드: 기본사항 | Google 검색 센터  |  문서  |  Google Developers

기본 가이드를 살펴보면 크게 두가지 영역으로 나누어져 있다. 기술 영역과 콘텐츠 영역이다. 이를 모두 충족해야 하는것은 아니다. 우리가 만든 사이트가 어떤 컨텐츠를 제공하고 어떤 목적을 가지고 있는지에 따라 어디에 비중을 더 두는 것이 유리할 지 생각해보고 조정하는 과정이 필요하다.

마무리

이번 글을 통해서 SEO란 무엇이고 왜 필여한지 , 어떻게 하는것인지 알아볼 수 있어다. 현재 NewAra의 경우 NewAra 내부에 작성한 컨텐츠들은 카이스트 내부인들만이 접근 권한을 가지고 있어 외부에 공개하지 않고 있기에 SEO를 적용할 필요성은 없다. 나중에 기회가 된다면 SEO를 적용시킨 커뮤니티 또한 제작해보고 싶다.

긴 글 읽어주셔서 감사합니다!

알아두면 조금 쓸데있는 VSC를 위한 정규표현식 치환

· 약 16분
황제욱 (jeuk)
달면 삼키고 쓰면 배운다

왜 정규표현식을 사용하는가?

어떤 문자열이 올바른 이메일인지 어떻게 알 수 있을까요? 실제로 메일을 보내고 그 내용을 사용자가 받아보았는지 확인하는 방법도 있겠지만 보내기 전에 이메일이 어느정도 올바른 형식인지 확인할 수 있으면 더 좋을 것입니다. 예를 들어 qwerty는 굳이 이메일을 보내지 않아도 유효한 이메일이 아님을 알 수 있습니다. 그 이유는 이메일은 특정한 형식을 만족하기 때문입니다. 1

^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$

JavaScript의 경우 위와 같은 특수한 문자열을 이용해 문자열이 적어도 올바른 이메일이 되기 위한 형식을 갖추었는지 확인할 수 있습니다. 직접 여러 문자열을 넣어 확인해보고 싶다면 다음 사이트를 방문하시면 됩니다.

정규표현식

위에서 말한 특수한 문자열을 정규표현식(Regular expression; RegEx)이라고 부릅니다. 정규표현식이란 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 사용되는 형식 언어입니다. 이는 문자열을 검색, 대체, 추출하는데 사용되는 강력한 도구로, 특수한 문법을 통해 문자열 내에서 원하는 패턴을 찾을 수 있습니다. 프로그래밍 언어나 텍스트 편집기에서 주로 적용되어 데이터 검증, 웹 스크래핑 및 텍스트 처리와 같은 다양한 분야에 활용됩니다.

정규표현식에 대해서 구체적인 예시로 이해하고 싶으신 분을 위해서 MDN에서도 추천하는 단계적으로 정규표현식을 학습할 수 있는 사이트를 소개해드립니다.

본 글은 코드를 수정하는 과정에서 정규표현식을 이용해 단순 노동을 줄이기 위한 방법을 소개합니다.

Visual Studio Code에서 정규표현식을 이용해 치환하기

Visual Studio Code(VSC)는 널리 사용되는 소스코드 편집기로 제가 VSC를 사용하기 때문에 본 문서에서는 VSC를 기준으로 설명하게 되었습니다.

하지만 다음 문서에서 설명하듯 다른 편집기에서도 정규표현식을 이용한 치환이 가능하니 참고하시길 바랍니다. 소개되지 않은 편집기를 사용하신다면 추가적으로 검색해보시길 추천드립니다.

기초적인 사용법

Find panel for VSC

Ctrl+F를 누르면 문자열을 검색할 수 있는 창이 나옵니다. 검색 창 왼쪽에 있는 오른쪽 화살표 버튼을 누르면 문자열을 치환할 수 있는 창이 나옵니다. 이때는 단순히 한 문자열을 다른 문자열로 바꾸는 역할을 하기 때문에 바꾸는 대상을 정규표현식으로 표현하기 위해서는 다음과 같이 .*과 유사하게 생긴 아이콘을 눌러야 합니다. 그러면 VSC가 Find에 적힌 문자열을 정규표현식으로 인식합니다.

Find-Replace panel with regular expression option for VSC

정규표현식 치환을 사용하기 위해서는 Find에 정규표현식을 입력하고 Replace에 치환할 문자열을 입력합니다. 그 후 Replace 또는 Replace All을 누르면 모든 문자열이 치환됩니다. 정규표현식을 이용한 치환도 Ctrl+z 등을 통한 방법으로 되돌릴 수 있으므로 편하게 시도해보셔도 됩니다.

다만 정규표현식을 이용한 치환은 문서 전체에 영향을 미치기에 특히 문서가 긴 경우 주의하시길 바랍니다. Find에 정규표현식을 입력한 후 에디터 상에서 정규표현식에 의해 강조된 문자열이 모두 원하는 문자열인지 확인하시길 바랍니다.

이러한 문제를 줄이기 위해 선택된 영역만 치환하는 기능을 찾아보았으나 이에 대한 버그를 신고하는 Github 이슈가 있다는 점을 사용하실 때 참고해주시기 바랍니다.

Visual Studio Code를 위한 정규표현식 문법 배우기

Visual Studio Code를 위한 정규표현식 문법을 배우기 위해서는 설명이 빈약한 Visual Studio 관련 문서보다는 다음의 Visual Studio 관련 문서를 추천드립니다. 비록 Visual Studio 관련 문서이지만 Visual Studio Code의 정규표현식과 유사한 점이 많기에 초반 학습에 참고하시길 바랍니다. Visual Studio와 Visual Studio Code의 정규표현식 엔진 차이에 대한 StackOverflow 질문도 첨부해드리니 차이점을 이해하여 올바르게 이용하시면 좋겠습니다.

예시

예시 상황과 이에 적용할 수 있는 정규표현식 예시를 제공하였습니다.

이를 위해 다음과 같은 정규표현식 치환을 사용한다. 문장 다음에 나오는 정규표현식은 다음과 같은 형식입니다.

Find에 입력해야 할 정규표현식
Replace에 입력해야 할 정규표현식

일부분 남기기

도겸은 학교 교과목을 분류하는 프로그램을 리팩토링하였다. 리팩토링 과정에서 코드 일부를 짧게 만들 수 있게 되어 다음과 같이 수정하고 싶다.

기존 코드는 다음과 같다.

{
all: new LecsExpr(this.lectures),
dept: new LecsExpr(union(this.lectures, this.lecture.all)),
기필: new LecsExpr(union(this.lectures, this.lecture.기필)),
기선: new LecsExpr(union(this.lectures, this.lecture.기선)),
전필: new LecsExpr(union(this.lectures, this.lecture.전필)),
전선: new LecsExpr(union(this.lectures, this.lecture.전선)),
심전필: new LecsExpr(union(this.lectures, this.lecture.심전필)),
연구: new LecsExpr(union(this.lectures, this.lecture.연구)),
}

이를 다음과 같이 바꾸고 싶다.

{
all: this.lectures,
dept: union(this.lectures, this.lecture.all),
기필: union(this.lectures, this.lecture.기필),
기선: union(this.lectures, this.lecture.기선),
전필: union(this.lectures, this.lecture.전필),
전선: union(this.lectures, this.lecture.전선),
심전필: union(this.lectures, this.lecture.심전필),
연구: union(this.lectures, this.lecture.연구),
}

이를 위해 다음과 같은 정규표현식 치환을 사용한다.

new LecsExpr\((.+)\)
$1

일부분 남기기2

도겸은 학교 교과목을 분류하는 프로그램을 다시 한 번 리팩토링하였다. 이를 위해 코드 일부를 다음과 같이 수정하고 싶다.

기존 코드는 다음과 같다.

{
all: this.lectures,
dept: union(this.lectures, this.lecture.all),
기필: union(this.lectures, this.lecture.기필),
기선: union(this.lectures, this.lecture.기선),
전필: union(this.lectures, this.lecture.전필),
전선: union(this.lectures, this.lecture.전선),
심전필: union(this.lectures, this.lecture.심전필),
연구: union(this.lectures, this.lecture.연구),
}

이를 다음과 같이 바꾸고 싶다.

{
all: this.lectures,
dept: this.lectures + this.lecture.all,
기필: this.lectures + this.lecture.기필,
기선: this.lectures + this.lecture.기선,
전필: this.lectures + this.lecture.전필,
전선: this.lectures + this.lecture.전선,
심전필: this.lectures + this.lecture.심전필,
연구: this.lectures + this.lecture.연구,
}

이를 위해 다음과 같은 정규표현식 치환을 사용한다.

union\((.+), (.+)\)
$1 + $2

포맷 맞추기

승관이는 시스템 프로그래밍을 공부하다가 공부가 하기 싫어져 모든 함수 사이 줄 간격을 일정하게 바꾸려고 한다.

기존 코드는 다음과 같다.

pid_t Fork(void);
pid_t Waitpid(pid_t pid, int *iptr, int options);
void Kill(pid_t pid, int signum);


unsigned int Sleep(unsigned int secs);


void Setpgid(pid_t pid, pid_t pgid);

void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);



void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);




void Sigaddset(sigset_t *set, int signum);

ssize_t Sio_puts(char s[]);

이를 다음과 같이 바꾸고 싶다.

pid_t Fork(void);
pid_t Waitpid(pid_t pid, int *iptr, int options);
void Kill(pid_t pid, int signum);
unsigned int Sleep(unsigned int secs);
void Setpgid(pid_t pid, pid_t pgid);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
ssize_t Sio_puts(char s[]);

이를 위해 다음과 같은 정규표현식 치환을 사용한다.

\n+
\n

특정 부분 삭제하기

정한이는 사용자 정보 예시를 바탕으로 interface를 만들려고 한다. 어떻게 해야 할까?

사용자 정보는 다음과 같다.

const user = {
id: "ALSKDJ",
name: "정한이",
age: 23,
email: "wow@wow.com",
password: "strong ",
point: 5000,
};

이를 바탕으로 다음과 같은 interface를 만드려고 한다.

export interface UserScheme {
id: string;
name: string;
age: number;
email: string;
password: string;
point: number;
}

이를 위해 다음과 같은 정규표현식 치환을 사용한다. 그 후 첫 줄을 const user = {에서 export interface UserScheme {로 직접 바꾸어준다.

: [^"]+,
: number;
: .+,
: string;

이름 바꾸기 / 치환하기

백준을 풀던 중 이전 문제에서 코드를 복붙한 민규는 실수로 graph[x][0]의 형태 대신 smaller[x]를 써버렸다. 이를 바꾸기 위해 정규표현식을 사용해보자.

현재 코드는 다음과 같다.

import sys

I = sys.stdin.readline
N, M = map(int, I().split())
graph = [[] for _ in range(N + 1)]
for _ in range(M):
A, B = map(int, I().split())
graph[A].append(B)
smaller[B] += 1

stack = [i for i in range(1, N + 1) if smaller[i] == 0]
L = []
while stack:
i = stack.pop()
for v in graph[i]:
smaller[v] -= 1
if smaller[v] == 0:
stack.append(v)
L.append(i)
print(*L)

이를 다음과 같이 바꾸고 싶다.

import sys

I = sys.stdin.readline
N, M = map(int, I().split())
graph = [[] for _ in range(N + 1)]
for _ in range(M):
A, B = map(int, I().split())
graph[A].append(B)
graph[B][0] += 1

stack = [i for i in range(1, N + 1) if graph[i][0] == 0]
L = []
while stack:
i = stack.pop()
for v in graph[i]:
graph[v][0] -= 1
if graph[v][0] == 0:
stack.append(v)
L.append(i)
print(*L)

이를 위해 다음과 같은 정규표현식 치환을 사용한다.

smaller\[([Bvi])\]
graph[$1][0]

시대의 변화

정규표현식에 익숙해지신다면 다양한 단순 노동을 순식간에 처리해버릴 수 있습니다. 하지만 기술의 발전은 새로운 도구를 가져다 주었습니다.

Copilot recommend well

이 글을 작성하는 과정에서도 GitHub Copilot이 제안하는 문장이 제가 원하는 문장과 매우 유사했습니다.

Copilot recommend very well

주석과 함께 원하는 변화의 예시를 적절히 적어준다면 정규표현식 없이도 GitHub Copilot을 이용해 단순 노동을 크게 줄일 수 있을 것입니다. 하지만 내가 쓰고 싶은 코드를 예측해서 적어주는 것이기에 결과를 확신하기는 어렵습니다.

ChatGPT also recommend well

ChatGPT 또한 코드를 작성하는 과정에서 생기는 단순 노동을 줄이기 위해 사용될 수 있습니다. 우선 위 사진에서 보여지듯이 자연어를 이용해 코드를 수정할 수 있습니다. 그러나 이 또한 Copilot과 마찬가지로 내가 원하는 코드를 예측해서 적어주는 것이기에 결과를 확신하기는 어렵습니다.

만약 정확성이 중요한 일을 처리하신다면 ChatGPT를 이용해 정규표현식을 생성하도록 만들면 됩니다. 실제로 ChatGPT를 이용한 정규표현식 생성기에 대한 유튜브 영상도 존재합니다. 다만 regex for vsc와 같은 표현을 통해 Visual Studio Code에서 돌아가는 정규표현식을 생성하도록 만들어야 합니다.

맺음말

정규표현식은 강력하지만 잘못 사용하기 쉬운 도구입니다. 정규표현식에 대한 글을 오래전부터 염두에 두고 준비해왔으나 ChatGPT와 같은 신기술의 발전으로 인해 단순 노동을 줄이기 위한 도구로써의 정규표현식의 장점이 적어진다고 생각합니다. 이에 따라 예시 위주로 효용을 입증하되 구체적인 문법에 대해서는 설명하지 않았습니다.

비록 이 글은 정규표현식을 중심으로 설명하였지만 정규표현식에 대한 관심을 넘어 코드를 수정하는 과정에서 단순 노동을 줄이는 방법에 대해 고민해보면 어떨까 싶습니다.

읽어주셔서 감사합니다.

주석

1: 이메일을 검증하기 위한 완전한 JavaScript 표현식에 대해서는 약간의 의견 차이가 있을 수 있습니다. 다음의 StackOverflow 질문들을 참고하시길 바랍니다.

ChatGPT와 Prompt Engineering

· 약 14분
황현빈 (nunu)
NLP 재밌어요

혹시 ChatGPT... 사용해보신 적 있으신가요? 😲

ChatGPT(챗지피티)란 OpenAI가 개발한 대화형 🤖인공지능🤖 으로, 최근 엄청난 성능으로 많은 화제가 되고 있습니다. 이에 따라 챗지피티를 활용한 플러그인이나 서비스도 많이 출시가 되고 있는데요, ChatGPT는 웹사이트를 방문하는 거 외에도, API를 발급받아 사용해 볼 수 있습니다.

[중요] ChatGPT는 웹사이트에서 사용하는 경우 무료이지만, API 사용의 경우 1,000 토큰 당 0.002$이 부과됩니다. 보통 한 단어가 1-2개의 토큰이라고 보시면 됩니다.

OpenAI API 발급 방법

  • OpenAI 웹사이트 방문 후 회원가입을 한다.
  • 프로필 클릭 후, Biiling - Payment methods에서 결제수단을 등록한다.
  • 이후, User - API keys 클릭 - Create new secret key 클릭
  • 화면에 출력되는 key를 다른 어딘가에 잘 저장해두자!

코드에서 사용하는 방법 (Python)

먼저 shell을 열고 openai package를 설치해줍니다.

pip install openai

이후 python 파일을 만들어볼까요? 저는 call_chatgpt.py라고 이름을 지어보겠습니다.

import openai # openai package를 사용합니다.

openai.api_key="" # 여기 아까 발급받은 key를 넣어줍니다.

messages = [
{"role": "user", "content": "스팍스로 삼행시 지어줘."
]

response = openai.ChatCompletion.create(
model= "gpt-3.5-turbo", # ChatGPT의 또 다른 이름입니다.
messages = messages,
temperature = 0,
max_tokens= 128,
top_p= 1,
stop= ["\n\n"]
)

result = response['choices'][0]['message']['content']
print("ChatGPT Output:", result)

그럼 결과가 어떻게 나오는지 봅시다! 파일을 저장한 뒤, shell에서 call_chatgpt.py를 실행해봅시다.

python call_gpt.py

저는 다음과 같이 나왔습니다.

ChatGPT Output: 스팍스는 빛나는 별, 끝없이 펼쳐진 우주의 대장, 데이터 분석의 대명사

앗! 아직 챗지피티가 삼행시는 잘 못하나 보네요.. 하하.

코드 설명

1. messages

그럼 좀 더 자세히 코드 설명을 해보겠습니다.

messages = [
{"role": "user", "content": "스팍스로 삼행시 지어줘."
]

messages의 타입은 list[Dict]이여야 합니다. 각 Dictionary는 "role"과 "content"의 key를 가지는데, role은 "system", "user", "assistant" 셋 중 하나가 들어갈 수 있습니다.

a) role이 "system"일 때,

content에 보통 "You are an (something) that ... " 의 형태를 넣어줍니다. 채팅을 시작하기 전 첫번째 element로 많이 넣어주는데요, ChatGPT의 역할이 무엇인지 specify 해주기 위해 쓰입니다.

b) role이 "user"일 때,

content에 user가 ChatGPT에게 말하고 싶은 것을 넣어주면 됩니다.

c) role이 "assistant"일 때,

content에 ChatGPT가 대답한 것을 넣어주면 됩니다.

즉, 먼저 message에 이렇게 넣어 API를 호출했다고 가정해봅시다.

messages = [
{"role": "system", "content": "You are a CAT that always adds 'meow' at the end."},
{"role": "user", "content": "Hello! What's your name and how are you today?"},
]

그럼 이런 대답을 얻게 됩니다.

Hello! My name is CAT and I'm doing well, meow. How about you?

대화를 이어 나가고 싶으면 어떡하면 될까요? messages를 다음과 같이 수정할 수 있습니다.

messages = [
{"role": "system", "content": "You are a CAT that always adds 'meow' at the end."},
{"role": "user", "content": "Hello! What's your name and how are you today?"},
{"role": "assistant", "content": "Hello! My name is CAT and I'm doing well, meow. How about you?"}, # 방금 생성된 대답 추가해주기.
{"role": "user", "content": "What? I thought you were an AI model."} # 대화 이어나가기
]

자, 이제 다시 호출해봅시다.

I am an AI language model, but I am programmed to respond as if I were a cat. So, meow! How can I assist you today?.

대화를 이어서 잘 생성하는 것을 볼 수 있습니다!


2. Hyperparameters

API를 호출하는 코드를 보면 함수 파라미터들이 굉장히 많은 걸 볼 수 있습니다. 각각의 의미를 한번 알아봅시다.

  • model: 호출하고자 하는 모델의 이름입니다. ChatGPT의 다른 이름인 "gpt-3.5-turbo"외에도, "davinci", "text-davinci-003", "gpt4"(단, GPT4는 현재 API 신청 Waitlist가 있음)등의 다양한 모델들이 있습니다.
  • messages: 위에서 말한 List[Dict] 형태의 대화기록입니다.
  • temperature: 생성된 text의 다양성을 조절하는 수치입니다. 동일한 text input에 대해서, temperature가 0에 가까울수록 일관된 생성 결과가 나오고, 숫자가 커질수록 다양한 텍스트가 생성됩니다. 보통은 0.5 ~ 1.0 사이의 값을 사용합니다.
  • max_tokens: 생성할 텍스트의 최대 길이(토큰 개수)를 지정합니다.
  • top_p: top_p의 값이 p일 때 모든 단어들에 대한 확률분포를 생성한 뒤, 가장 확률이 높은 단어들의 확률 합이 p보다 작을 때까지 고른 뒤, 각 단어의 확률을 고려해 그 중 하나 random sampling을 진행합니다.
  • stop: '\n\n', 즉 newline character가 두 개 연속으로 나올 시에, 생성을 멈추도록 해두었습니다.

3. response

API가 반환하는 결과는 dictionary. 어떻게 생겼는지 한번 볼까요?

{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "I am an AI language model, but I am programmed to respond as if I were a cat. So, meow! How can I assist you today?",
"role": "assistant"
}
}
],
"created": 1683468678,
"id": "chatcmpl-7DZOYOe97FuYCbqj9LLfGWU1RZ6OB",
"model": "gpt-3.5-turbo-0301",
"object": "chat.completion",
"usage": {
"completion_tokens": 32,
"prompt_tokens": 79,
"total_tokens": 111
}
}

다양한 정보들이 들어있지만, 생성된 텍스트만 확인하고 싶을 경우, 반환한 dictionary D

D['choices'][0]['message']['content']

를 통해 이를 추출할 수 있습니다.

Prompt Engineering

챗지피티와 같은 거대 언어모델의 개발과 함께 어떤 Input을 넣어줘야 AI가 대답을 잘 할 수 있는지에 대한 연구도 활발하게 이루어지고 있는데요, 이를 Prompt Engineering이라고 합니다. 대표적으로, "질문을 어떻게 잘 하는지"가 그 중 하나입니다. 이에 대한 가이드를 여기서 찾아볼 수 있습니다. 이 외에도, 유명한 기법 2가지에 대해 소개드리려고 합니다.

Few-Shot Learning

첫번째는 Few-Shot 러닝입니다. 퓨샷이란 동일한 Task의 예제를 n개 보여준 뒤, 그 후 타겟 문제를 풀 게 하는 방식입니다. 예를 들어, 주어진 문장이 긍정적인지 부정적인지 판단하는 Task가 있다고 가정해봅시다. 일반적인 제로샷(Zero-Shot)의 경우, 모델에 들어가는 input은 다음과 같습니다.

다음 문장을 읽고 문장이 긍정적인지 부정적인지 판단하시오.

문장: 사과를 먹자마자 쓴 맛이 입안에 퍼지는 것 같았다.
답:

반대로 Few-Shot(n=3)의 경우, input은 다음과 같습니다.

다음 문장들을 읽고 문장이 긍정적인지 부정적인지 판단하시오.

문장: 오늘 공원에 갔는데 정말 재밌었어.
답: 긍정적

문장: 정말 최악의 영화군요. 다시는 안 볼 것 같습니다.
답: 부정적

문장: 개발을 하느라 밤을 새웠더니 정말 보람찬걸?
답: 긍정적

문장: 사과를 먹자마자 쓴 맛이 입안에 퍼지는 것 같았다.
답:

실제로 많은 Task들에서 Few-Shot을 사용할 시 비약적으로 성능이 올라가는 경우가 많습니다.

Chain-of-Thoughts

Chain of Thoughts은 사고 사슬이란 뜻을 가지고 있습니다. 쉽게 말해 step-by-step 사고를 통해 복잡한 reasoning을 필요로하는 문제를 풀어나갈 때 많이 사용되는 Prompting 기법입니다. 실제로 MultiArith라는 문장제 사칙연산 데이터셋에서 GPT3에 이 기법을 사용할 경우 Accuracy가 17.7%에서 78.7%까지 증가한다고 합니다. 사용하는 방법은 간단합니다. 묻고자 하는 질문 뒤에 "Let's think step by step"을 붙여주면 됩니다.. (Kojima, 2022)

ChatGPT에 Chain of Thoughts를 적용해봅시다. 다음의 논문을 참고했습니다.
When do you need Chain-of-Thought Prompting for ChatGPT?

Q: Alice, Bob, and Claire are playing a game. At the start of the game, they are each holding a ball: Alice
has a brown ball, Bob has a red ball, and Claire has a purple ball. As the game progresses, pairs of
players trade balls. First, Bob and Claire swap balls. Then, Alice and Claire swap balls. Finally, Alice and
Bob swap balls. At the end of the game, Bob has the
Which choice is true ? Answer Choices: (A) brown ball. (B) red ball. (C) purple ball.

A:

기존의 언어 모델들은 A, B, 또는 C중에 하나를 답으로 바로 출력하게 됩니다. 그러다보니, 왜 이렇게 나왔는지에 대한 이유도 알 수 없을 뿐더러, 정확도가 떨어지는 경우가 많습니다. Chain of Thoughts를 통해 모델이 어떻게 이 답을 유추했는지도 볼 수 있고, 정확도도 올라간다면, 일석이조라고 할 수 있죠! 물론, ChatGPT는 Chain of Thoguhts를 굳이 안 쓰더라도 답이 왜 이렇게 나왔는지까지 잘 설명해줍니다!

At the end of the game, Bob has the purple ball. To see why, let's trace the swaps that occurred during the game. After the first swap, Bob has the purple ball and Claire has the red ball. After the second swap, Alice has the red ball and Claire has the brown ball. Finally, after the third swap, Bob has the purple ball and Alice has the brown ball. Therefore, Bob ended up with the purple ball at the end of the game.

So the correct choice is (C) purple ball.

엇 그러면, Chain of Thoughts 기법을 굳이 쓸 필요가 없는 거 아닌가 싶을 수도 있습니다. 다만, 자세히보면 ChatGPT의 답이 틀렸습니다. 정답은 (B) red ball이거든요! 자 이제, Chain of Thoughts를 Prompting 기법을 적용해볼까요?

Let's track the movement of each ball throughout the game to determine the final holder of the red ball:

Bob and Claire swap balls:
Bob gives his red ball to Claire and takes her purple ball. Now, Bob has the purple ball and Claire has the red ball.

Alice and Claire swap balls:
Alice gives her brown ball to Claire and takes Claire's red ball. Now, Alice has the red ball and Claire has the brown ball.

Alice and Bob swap balls:
Alice gives her red ball to Bob and takes his purple ball. Now, Bob has the red ball and Alice has the purple ball.

Therefore, at the end of the game, Bob has the red ball. So, the correct choice is (B) red ball.

단순히 A: 뒤에 Let's think step by step을 더해줬을 뿐인데, 답이 정확하게 나오는 것을 볼 수 있습니다!

마무리

최근 AutoGPT가 Github에서 굉장한 인기를 얻고 있더라구요! AI 분야가 정말 빠르게 변하고 있는 만큼 사람들의 관심도 정말 높아지고 있는 것 같습니다 ㅎㅎㅎ 저도 개발할 때 ChatGPT의 도움을 많이 받는 사람으로써, 앞으로 AI 분야가 더 다양한 방면으로 성장했으면 좋겠습니다🤗

Kubernetes 란 무엇일까

· 약 15분
주예준 (triangle)
여러분 휠 세미나 꼭 들으세요

서론

백엔드 개발자, 혹은 DevOps에 관심이 있는 개발자들이라면 한번쯤 Kubernetes에 대해서 들어본 적이 있을 것이다. 이름만 들어본 사람도 있을 것이고, 그냥 Docker랑 비슷한 컨테이너 관리 툴이라고 아는 분들도 있을 것이다. 이 기술이 뭐길래 이렇게 인기가 많은 걸까? 그리고 SPARCS 에서 Kubernetes를 사용해도 좋을까?

“이중화”

Kubernetes를 적용하기 가장 좋은 예시가 바로 작년에 있었었다. 바로 데이터센터 화재로 인한 카카오 서비스 먹통 사태이다.

kakaotalk-error

해당 사건은 전 국민이 간접적인 피해를 느꼈을 정도로 대한민국 전반에 큰 혼란을 가져다 준 사고였다. 이때 일반인들도 쉽게 접했을 법한 키워드가 있는데, 바로 이중화다. 카카오톡 측에서는 데이터는 이중화되어 있었으나, 막대한 트래픽으로 이러한 백업 장치가 잘 작동이 되지 않았다고 해명한다. 카카오 측이 어떻게 이중화 조치를 했는진 모르겠지만, 하나의 데이터 리전이 다운됨에 따라서 모든 서비스가 먹통된 것으로 보아, 이런 비상상황을 대응한 제대로된 이중화 대응책이 마련이 되지 않았었다고 보여진다.

카카오 서비스만큼 거대한 규모가 아니더라도 SPARCS 서비스들이 위치해있는 서버가 비슷한 상황에 대응하고 싶으면 어떨까? 물론 이런 경우를 대비해서 대부분의 프로젝트들이 AWS 인프라를 사용하고 있지 않지만, 한번 클라우드 서비스를 사용하지 않는다고 가정해보자. 우선, 다중 서버를 사용해야 할 것이다. 서버 한대가 예기치 않게 고장이 나더라도 다른 서버가 대응할 수 있어야 하기 때문이다. 그리고 SPARCS 서버실 전체에 정전 등 문제가 생기더라도 대응할 수 있는 독립된 또다른 서버실이 필요하다. 교분 SPARCS실 외에 사용할 수 있는 또 다른 공간이 없으니… 법적인 SPARCS 단체 위치라고 할 수 있는 회장님 집에 서버실이 있다고 가정해 보자.

여분의 서버와 서버실이 있다고 하면 이들을 어떻게 네트워크로 연결해야 할까? Docker (w/o swarm) 나 nginx로만 이를 구축하기에는 많이 복잡하고 어려워 보인다. 뭔가 LXC 컨테이너들을 다중 노드 환경에서 띄울 수 있는 기술이 없을까? Kubernetes가 이런 상황에서 사용될 수 있다.

Kubernetes 소개

쿠버네티스는 ‘컨테이너 오케스트레이션 툴’ 이다. 이런 오케스트레이션 엔진을 통해 컨테이너의 생성과 소멸, 시작 / 중단 시점 제어, 스케줄링, 로드 밸런싱, 클러스터링들을 수행할 수 있게 된다.

쿠버네티스는 다음과 같은 특징을 가지고 있다.

  • 자동화된 복구 (self-healing)
    • 컨테이너 중 하나라도 죽으면 바로 재시작
  • 로드 밸런싱 (load-balancing)
    • 한 노드나 엔드포인트로 들어온 요청을 여러 서버에 위치해 있는 노드로 고르게 분산해줌
  • 오토 스케일링 (auto-scaling)
    • 사이트에 요청이 급격하게 늘어나는 경우, 자동으로 새로운 컨테이너들을 생성하거나, 자원의 크기를 증가시킨다.
  • 무중단 서비스 (Fault tolerance-FT)
    • 서비스를 업그레이드할 때, 일시중지할 필요 없이 업데이트를 할 수 있다.
  • 호환성 (Vendor lock in 문제 해결)
    • 컨테이너를 기반으로 하는 오픈소스
    • A 사의 클라우드 서비스에서 N 사의 클라우드 서비스로 이전 간단히 해결

Kubernetes가 필요한 좀 더 쉬운 예시를 생각해보자

server-example

위 그림은 A 서비스, 점심에는 B 서비스, 저녁에는 C 서비스로 트래픽이 몰리는 상황이 있다고 가정했다. 음.. SPARCS 서비스에 맞게 위와 거의 비슷한 상황(하지만 실제로 같은 문제를 겪고 있는)를 만들어보자.

수강 신청 기간에는 OTL 서비스, 목요일~일요일에는 Taxi 서비스, SPARCS 지원 기간에는 Apply 서비스, 신입생 심사 기간에는 Biseo 서비스에 트래픽이 몰린다. 이는 실제로 우리가 겪는 문제이다! 현재는 각 서비스별로 ec2 (혹은 물리서버) 를 부여해 그 위의 도커에서 서비스를 띄우고 있다. 하지만 해당 트래픽이 몰리지 않는 상황에서는 해당 자원들이 사용되지 않고 낭비되게 되고 실제로도 그렇다. 이는 불필요한 비용 지출 및 가용 물리 서버의 부족으로 이어지게 된다. 하지만 To-Be 이미지를 보면 오직 4대의 서버 만으로 기존의 9대의 서버가 필요로 했던 자원을 감당하고 있는 것을 확인할 수 있다. 그리고 Autoscaler가 동적으로 서비스의 자원 사용량을 감지해 필요할 때마다 스스로 새로운 서버를 부여하고, 필요하지 않으면 제거하게 된다. EKS는 AWS에 만든 완성된 kubernetes 서비스이므로 당장 EC2 대신 EKS로 옮기기만 해도 비용을 절감할 수 있는 것이다. (물론 과정이 순탄치 않겠지만..)

만약 마이크로 서비스와 같이 여러개의 컨테이너가 아닌 전형적인 프론트 - 백엔드 - DB 정도로 적은 컨테이너만 사용한다면 Kubernetes를 사용할 필요는 없다. 하지만, 단순히 docker-compose.yaml 을 작성하는 것보다 쿠버네티스 설정 파일 (이것도 yaml 형식을 사용한다.) 에서 지정할 수 있는 옵션들이 많기 때문에 일반적인 서비스들도 쿠버네티스를 사용해도 좋겠다는 생각이 든다.

Kubernetes는 Docker의 지원이 중단되었다. 정확히 말하면 기존 컨테이너 런타임으로 dockershim 대신 cri-containerd 를 사용하게 된 것이다. 따라서 Kubernetes 를 이용해 컨테이너를 띄우면 더이상 docker ps -a 로 컨테이너들을 확인할 수 없고 crictl ps -a 로 확인해야 한다. 그렇다고 해서 Docker가 쓸모 없어진 것은 아니다. 도커에서 만든 컨테이너 이미지를 Kubernetes에서 등록하고 실행하는 것은 가능하기 때문이다. 이미지는 표준 기준인 OCI(Open container initiative)를 따르기 때문이다. Dockerfile 을 이용해 이미지를 빌드 후 [docker.io](http://docker.io) 같은 레지스트리에 올린 뒤, 이를 사용할 수 있는 것이다.

Docker swarm도 컨테이너 오케스트레이션 툴이다. Kubernetes와 다르게 진입 장벽이 낮고, 중소 규모의 클러스터에서 애플리케이션 구등을 제어하기엔 충분하다. 또한 docker만 설치해도 swarm 모드만 활성화 할수있다는 장점이 있다. 하지만 사실상 2022년 기준 유지보수단계에 들어갔고, 장래성이 그리 밝지 않다는 단점이 있다. 사실상 Kubernetes가 컨테이너 오케스트레이션에 관한 표준 기술로 자리 잡았기 때문에 SPARCS의 대부분의 서비스가 Docker를 통해 띄워졌음에도 불구하고, 개인적으로 Docker swarm 대신 Kubernetes를 사용했으면 한다.

Bare-metal server간 Kubernetes Cluster 구축

  • Kubernetes는 설치하고 서버간의 연결하는 과정까지 docker보단 다소 복잡하다. 이번에 서버실 정리가 어느정도 되어 이용 가능한 자원이 늘어났기 때문에 이 서버들을 이용해 클러스터를 구성해 보려고 한다. 이는 현재 휠 프로젝트에서 진행 중에 있으므로, 관심 있거나 조언을 해 줄 수 있는 분들께서는 언제든지 연락 바란다.
  • AWS EKS와 묶는 것이 최종 목표이다. 현재 동방 내 SPARCS 물리 서버들은 1년에 한번 점검을 위한 정전으로 인해 필연적으로 down 될 수 밖에 없다. 만약 kubernetes를 도입했다면 동방의 모든 컨테이너가 죽게 되면 EKS로 노드가 이전하면서 거의 무중단으로 서비스 제공이 가능해진다.

컴포넌트 맛보기

배포를 하기 위해서는 .yaml 파일을 작성하면 된다. 이름은 상관 없으며, 실행할 때 kubectl apply -f config.yml 식으로 실행하면 된다. docker-compose.yaml 과 비슷해 보이지만 훨씬 다양한 설정이 가능하며, 진입 장벽도 높다. 따라서 kubernetes로 서비스를 배포하기 위해서는 충분한 연습이 필요하다.

이 config 파일 내에서는 Pod Replicaset Deployment Service Ingress Statefulset SecretMap ConfigMap PersistentVolume(Claim) VPA HPA 등 수많은 요소들이 들어갈 수 있다.

하지만 너무 글이 길어지기 때문에 여기서 설명하지는 않겠다. 다만 개인적으로 잘 설명되어 있던 블로그를 소개하려고 한다. 누구나 쉽게 따라할 수 있게 설명이 되어 있고 간단한 연습 문제도 있으니 한번 확인해 보길 바란다.

시작하기

여러 키워드들

kubectl : k8s 제어를 위한 툴. docker 명령어와 비슷하고, 90%정도는 이 명령어와 사용하게 될 정도로 자주 사용한다.

kubeadm : k8s 클러스터 셋업을 위한 툴. control plane과 worker node를 생성하고 연결하는데 사용한다.

kubelet : 각각의 worker node를 관리하기 위한 agent. kubelet 이 control plain과 소통을 하게 되며 journalctl 로 로그를 확인해 troubleshooting을 할 수 있다.

k8s : 그냥 k(ubernete)s → k8s 로 줄인 약자인다.

k0s, microk8s, k3s : lightweight 한 k8s

k9s : kubernetes 리소스 관리 툴. kubectl 명령어 대신 사용 가능

helm : Kubernetes를 위한 패키지 매니저. Chart라는 템필릿화 된 yaml 형의 파일

kustomize : kubernetes yaml 설정을 다양한 환경(dev, prod)에서 관리하기 위한 툴

calico , flannel : 네트워크 인터페이스. 여러 노드 간 HA, load balancing을 하는데 필요한 network meshing을 구현하는데 필요하다.

우선 생각나는 것은 이정도인데, 더 있을 수 있다.

마무리

원래 본 기술 블로그은 조금 더 구성 요소 및 디테일한 예시 위주로 서술하려고 했으나, 추후 진행될 Orchestration tool 세미나와의 중복 및 필자의 시간관계상 여기서 마무리하려고 한다. 지금까지 간단히 왜 Kubernetes를 써야 하는지, 그리고 어떤 상황에서 사용될 수 있는 지를 확인해보았는데 장단점들을 고려해 본인의 프로젝트에 Kubernetes를 붙히는 것이 좋은지, 그렇지 않은 지 판단해 보자.

💥찐막 홍보💥

구체적인 Docker swarm, Kubernetes 사용법, 더 나아가 Terraform과 같은 IaC에 대한 설명은 23년도 여름 휠 세미나에서 진행할 예정입니다! 관심 있으신 분들은 꼭 청강하시기 바래요 🧡

Flutter vs React Native

· 약 12분
김현수 (default)
언제 졸업하지...

서론

안녕하세요! NewAra 팀에서 앱 개발을 진행하고 있는 김현수입니다. 현재 Flutter로 iOS, Android 크로스 플랫폼 앱 개발을 진행하고 있습니다. Flutter로 앱 개발을 진행하는 김에 Flutter 와 React Native 두 앱 프레임워크를 비교하는 간단한 글을 써봤습니다.

vs

본론

앱 프레임워크 종류

앱 프레임워크는 모바일 앱 개발을 위한 소프트웨어 개발 도구입니다. 크로스 플랫폼 앱은 다양한 플랫폼에서 실행될 수 있는 앱입니다. 크로스 플랫폼 앱은 네이티브 앱과 유사한 성능을 제공하면서, 하나의 코드베이스로 다양한 플랫폼에서 앱을 개발할 수 있어 개발 비용을 절감할 수 있습니다. 크로스 플랫폼 앱은 다양한 플랫폼에서 실행될 수 있으므로, 앱을 다양한 사용자에게 제공할 수 있습니다. 하지만, 네이티브 앱보다는 성능이 아주~ 살짝~ 떨어질 수 있습니다.

밑에는 크로스 플랫폼 앱 프레임워크입니다.

  • Flutter: 구글에서 개발한 모바일 앱 프레임워크로, 다양한 위젯과 레이아웃을 제공하여 UI 개발을 쉽게 할 수 있습니다. Dart 언어를 사용하며, 자체 렌더링 엔진을 가지고 있어 네이티브 앱과 비슷한 성능을 제공합니다.

  • React Native: 페이스북에서 개발한 모바일 앱 프레임워크로, JavaScript 언어를 사용합니다. 네이티브 앱과 비슷한 UI를 제공할 수 있으며, 다양한 플랫폼을 지원합니다. 또한, 다른 JavaScript 기반의 프레임워크와 호환성이 높습니다.

  • Xamarin: C# 언어를 사용하는 Microsoft의 모바일 앱 프레임워크로, 다양한 플랫폼을 지원합니다. Visual Studio를 사용하여 개발할 수 있으며, 네이티브 앱과 비슷한 성능을 제공합니다.

  • Ionic: AngularJS 및 JavaScript를 사용하는 모바일 앱 프레임워크로, 다양한 플랫폼을 지원합니다. Cordova 플러그인을 사용하여 하이브리드 앱을 개발할 수 있으며, 개발 생산성이 높습니다.

  • PhoneGap: Apache Cordova를 기반으로 한 모바일 앱 프레임워크로, HTML, CSS, JavaScript를 사용하여 하이브리드 앱을 개발할 수 있습니다. 다양한 플랫폼을 지원하며, Adobe에서 개발하고 있습니다.

  • Corona: Lua 스크립트 언어를 사용하는 모바일 앱 프레임워크로, 게임 개발에 적합합니다. 다양한 플랫폼을 지원하며, 개발 생산성이 높습니다.

  • NativeScript: Angular, Vue.js 등과 함께 사용할 수 있는 자바스크립트 프레임워크로, 네이티브 앱과 비슷한 성능을 제공합니다. 다양한 플랫폼을 지원하며, 개발 생산성이 높습니다.

이 중 가장 유명한 것은 Flutter와 React Native일 것입니다.

앱 프레임워크 이용 경향

Alt text

Flutter vs React Native 장단점 비교

Flutter와 React Native의 장단점을 비교해 보면 다음과 같습니다.

  1. 성능

Flutter는 자체 렌더링 엔진을 가지고 있기 때문에, 네이티브 앱과 비슷한 성능을 제공합니다.

React Native는 네이티브 앱과 비교하면 성능이 다소 떨어질 수 있습니다.

  1. 개발 생산성

Flutter는 Hot Reload 기능을 제공하여 빠른 개발을 가능하게 하며, 다양한 위젯과 레이아웃을 제공하여 개발 생산성을 높일 수 있습니다.

React Native는 Hot Reload 기능을 제공하여 빠른 개발이 가능하지만, Flutter만큼 다양한 위젯과 레이아웃을 제공하지는 않습니다.

  1. 사용자 인터페이스(UI)

Flutter는 다양한 위젯과 레이아웃을 제공하여 사용자 인터페이스(UI) 개발이 쉽습니다.

React Native는 Flutter보다 다양한 위젯과 레이아웃을 제공하지는 않지만, 네이티브 앱과 비슷한 UI를 제공할 수 있습니다.

  1. 커뮤니티

Flutter는 구글에서 개발하고 있기 때문에, 안정성과 커뮤니티 지원이 잘 되어 있습니다.

React Native는 페이스북에서 개발하고 있기 때문에, 안정성과 커뮤니티 지원이 잘 되어 있습니다.

  1. 플랫폼 지원

Flutter는 안드로이드, iOS, 웹, 데스크톱 등 다양한 플랫폼을 지원합니다.

React Native는 안드로이드, iOS 등 다양한 플랫폼을 지원합니다.

  1. 학습 곡선

Flutter는 Dart 언어를 사용하기 때문에, 처음 사용하는 개발자들에게는 학습 곡선이 높을 수 있습니다.

React Native는 JavaScript 언어를 사용하기 때문에, 다른 JavaScript 기반의 프레임워크와 비교하면 학습 곡선이 낮습니다.

어떤 경우에 어떤 프레임워크를 선택해야 할까?

Flutter와 React Native는 모두 크로스 플랫폼 앱 개발에 적합한 프레임워크입니다. 하지만, 개발자는 개발 목적과 앱 특성을 고려하여 선택해야 합니다.

  • 성능이 중요한 앱: Flutter

    Flutter는 자체 렌더링 엔진을 가지고 있기 때문에, 네이티브 앱과 비슷한 성능을 제공합니다. 또한, JIT(Just-in-Time) 컴파일러와 AOT(Ahead-of-Time) 컴파일러를 모두 지원하기 때문에, 빠른 개발과 성능 최적화를 모두 할 수 있습니다. 따라서, 성능이 중요한 앱(예: 게임, 미디어 앱)을 개발할 때는 Flutter를 선택하는 것이 좋습니다.

  • 개발 생산성이 중요한 앱: React Native

    React Native는 JavaScript 언어를 사용하기 때문에, 웹 개발 경험이 있는 개발자들이 빠르게 학습할 수 있습니다. 또한, 다양한 컴포넌트와 라이브러리를 제공하여 개발 생산성을 높일 수 있습니다. 따라서, 개발 생산성이 중요한 앱(예: SNS, 쇼핑몰 앱)을 개발할 때는 React Native를 선택하는 것이 좋습니다.

  • 다양한 플랫폼 지원이 필요한 앱: Flutter

    Flutter는 iOS, Android, Web, Windows, macOS 등 다양한 플랫폼을 지원합니다. 또한, 네이티브 앱과 같은 UI를 제공하기 때문에, 다양한 플랫폼에서 일관된 UI를 제공할 수 있습니다. 따라서, 다양한 플랫폼을 지원해야 하는 앱(예: 멀티 플랫폼 게임)을 개발할 때는 Flutter를 선택하는 것이 좋습니다.

  • 이미 JavaScript를 사용하는 개발 환경: React Native

    React Native는 JavaScript를 사용하기 때문에, 이미 JavaScript 기반의 프론트엔드 개발 환경을 갖춘 개발자들이 쉽게 학습할 수 있습니다. 또한, 다른 JavaScript 기반의 라이브러리와 호환성이 높기 때문에, React Native를 사용하는 것이 유리할 수 있습니다. 따라서, 이미 JavaScript를 사용하는 개발 환경에서 앱을 개발해야 하는 경우에는 React Native를 선택하는 것이 좋습니다.

  • 다양한 UI 컴포넌트를 제공하는 앱: Flutter

    Flutter는 다양한 위젯과 레이아웃을 제공하여, 자유롭게 UI를 구성할 수 있습니다. 또한, 커스텀 위젯을 만들어 사용할 수 있으며, 다양한 애니메이션을 쉽게 구현할 수 있습니다. 따라서, 다양한 UI를 제공하는 앱(예: 새로운 디자인 컨셉의 앱)을 개발할 때는 Flutter를 선택하는 것이 좋습니다.

결론

사실 둘의 성능은 좋기에 둘 간의 성능을 비교하는 것은 거의 무의미합니다. 따라서 본인이 익숙한 언어로 되어 있는 프레임워크를 선택하는 것이 좋습니다. 만약 개발자가 이미 JavaScript를 사용하는 개발 환경에서 앱을 개발하려는 경우, React Native를 선택하는 것이 좋습니다. 반면, Flutter는 성능이 중요한 앱을 개발하거나, Dart 언어를 사용할 수 있는 개발자라면 선택할 수 있습니다.

참고

일본 1위 배달 앱, 바닥부터 다시 짠다 - Recode 프로젝트

멀쩡한 앱을 Flutter 앱으로 다시 짠 이유 - 일본 1위 배달 앱, 두 번째 Recode

모바일 개발 프레임워크 선택

IaC To Terraform

· 약 6분
황민성 (minseong)
잠을 잘 자야합니다.

Definition

Infrastructure as Code (IaC) is the managing and provisioning of infrastructure through code instead of through manual processes.

  • by ⛑️(Redhat)
  • 인프라를 관리하고 Provision 하는 과정을 수동(인간이 Step By Step으로 명령어를 날리는 것 등)이 아닌 코드를 통해서 수행하는 것
  • Provision하려는(한) 인프라에 대해서 Spec을 담고있는 Configuration에 해당하는 파일이 생성되며, 이 파일이 코드로 작성된다.
    • 매번 같은(비슷한) 인프라를 구성하는데 있어서 빠뜨리는 것 없이 동일하게 Provision 가능하다.
    • 이러한 Config 파일들이 일종의 문서화의 역할을 한다.
    • Ad-hoc으로 불어나는 자잘한 변화들을 피할 수 있다.
  • 코드로 작성되기 때문에, 수정사항에 대해서 VCS(Version Control System)을 사용하여 인프라 변화에 대하여 추적,관리, 운영할 수 있다.

Types

  • Imperative

    수행해야하는 명령들과, 명령들의 수행 순서를 정의하는 방식

  • Declarative

    생성 결과물에 대한 상태를 정의하고, 이를 적용하면 IaC Tool이 자동으로 해당 결과물을 생성해주는 방식

Imperative/Declarative

Tools

Terraform

Features

  • Cloud, On-prem resource 지원
  • Human Readable한 HCL(Hashcorp Language)로 Declarative한 IaC를 제공
  • Community Based 모듈(Provider) 지원으로 Low level의 하드웨어 리소스부터, High level의 DNS, SaaS 기능들까지 Code로 관리할 수 있게 제공 [Terraform Registry]

Workflow Stages

  • Write
    • Resource들을 .tf파일로 정의하는 단계(ex: AWS VPC with SG, ELB)
  • Plan
    • Terraform이 tfstate(현재 인프라의 상태를 담고있는 파일)과 정의된 Resource를 비교하여 Desired State of Infra를 달성하기 위해서 어떠한 수정을 거칠 것인지 설명해주는 Execution Plan을 반환
  • Apply
    • 사용자의 Confirm후에 Terraform이 Plan단계에 명시된 Operation을 알아서 각 Resource에 필요한 작업들을 맞는 순서로 수행함

TerraformWorkTypes

HCL(Terraform Language)

  • Terraform의 Resource들을 선언하기 위해서 사용되는 언어
resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}

# Way to Write a Block
<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
# Block body
<IDENTIFIER> = <EXPRESSION> # Argument
}
  • Block은 type을 꼭 가지고 있고 Label은 없거나 여러개를 가질 수 있음
  • Block Body에는 Argument나 Nested Block이 있음
  • Argument는 Name(Key)에 해당하는 Value를 할당
  • Expression은 값 자체나 다른 값에 대한 참조가 될 수 있음

Caveats For Team Using Terraform

  • Terraform을 통해서 생성, 수정, 삭제된 인프라의 정보들은 tfstate 의 포맷의 파일로 테라폼 레포지토리 Root에 기록
    • 팀단위로 테라폼을 운영할 경우에는 이 상태파일이 팀내에서 공유될 수 있도록, 상태파일 공유를 위한 스토리지 운영이 필수적으로 필요하다. (AWS를 사용하는 팀의경우 AWS S3활용이 범용적)
    • 또한, 여러명이 동시에 Terraform을 통해서 Apply하여 인프라에 수정상황이 발생할 경우에 예기치않은 많은 문제들이 발생할 수 있다.
      • 이를 막기 위해서, Race Condition을 방지하기 위한 잠금 장치가 필요하다. (AWS를 사용하는 팀의 경우 주로, DynamoDB를 통해서 terraform_lock을 구현한다.
        • 참고

          resource "aws_dynamodb_table" "terraform_locks" {
          name = "terraform-up-and-running-locks"
          billing_mode = "PAY_PER_REQUEST"
          hash_key = "LockID"

          attribute {
          name = "LockID"
          type = "S"
          }
          }
    • 마지막으로, 환경별(Production, Staging, Develop 등) 상태파일의 격리가 필요하다.
      • Human Error의 전형이 이 환경구분을 실수하여 개발환경에 필요한 리소스 변경을 Production에 한다던지 하면서 발생하기 쉽다.

Simple Example

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 1.0.4"
}
}
}

variable "aws_region" {}

variable "base_cidr_block" {
description = "A /16 CIDR range definition, such as 10.1.0.0/16, that the VPC will use"
default = "10.1.0.0/16"
}

variable "availability_zones" {
description = "A list of availability zones in which to create subnets"
type = list(string)
}

provider "aws" {
region = var.aws_region
}

resource "aws_vpc" "main" {
# Referencing the base_cidr_block variable allows the network address
# to be changed without modifying the configuration.
cidr_block = var.base_cidr_block
}

resource "aws_subnet" "az" {
# Create one subnet for each given availability zone.
count = length(var.availability_zones)

# For each subnet, use one of the specified availability zones.
availability_zone = var.availability_zones[count.index]

# By referencing the aws_vpc.main object, Terraform knows that the subnet
# must be created only after the VPC is created.
vpc_id = aws_vpc.main.id

# Built-in functions and operators can be used for simple transformations of
# values, such as computing a subnet address. Here we create a /20 prefix for
# each subnet, using consecutive addresses for each availability zone,
# such as 10.1.16.0/20 .
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index+1)
}

CI/CD

· 약 8분
김건 (suwon)
곧 카이스트 졸업할 몸

안녕하세요. SPARCS에서 2021년에는 Taxi팀의 front 개발자로, 2022년에는 PM으로 활동하고 있는 김건(suwon)입니다.

CI/CD란 여러 DevOps 단계를 아우르는 포괄적인 용어로 CI/CD의 개념에 대해 살펴보는 시간을 가지도록 하겠습니다.

image-20221204221254056

CI (Continuous Integration)

CIContinuous Integration, 지속적 통합이라는 뜻을 가지고 있습니다. 여기서 지속적 통합이란 여러 명이 하나의 코드에 대해서 수정을 진행해도 코드의 품질을 관리하면서 지속적으로 통합할 수 있음을 의미합니다.

프로젝트 그룹에서 여러 개발자들이 코드 수정 작업을 진행을 하고 main 브랜치에 통합을 할 때, 작업이 끝나면 통합된 코드가 잘 빌드가 되는지, 올바르게 동작을 하는지, 버그는 없는지 테스트하는 과정을 거쳐야합니다. 하지만 매번 개발자가 직접 코드를 병합할 때 마다 테스트를 진행하면서 코드를 검증하는 것은 프로젝트의 크기가 커질 수록 더욱더 많은 시간이 소요됩니다. 최악의 경우에는 작업 시간보다 통합 및 테스트 과정이 더 많은 시간이 필요하게 될 수 있습니다. 이러한 상황을 통합의 지옥이라고 합니다.

하지만 테스트를 자동화를 한다면 개발자가 빌드와 테스트를 직접 하지 않아도 코드를 검증할 수 있습니다. CI는 테스트를 자동화하여 통합의 지옥을 피하게 도와주며, 개발자들의 기존 코드와의 Conflict 해결 및 버그 수정을 더 빠르게 할 수 있도록 도와줍니다.

CD (Continuous Delivery, Continuous Deployment)

CD는 Continuous Delivery 지속적 제공이라는 뜻과 Continuous Deployment, 지속적 배포라는 두 가지의 뜻이 혼용되어 사용됩니다.

지속적 제공(Continuous Delivery)은 CI를 통해서 새로운 코드의 빌드와 테스트를 검증하였다면, Github과 같은 저장소에 업로드하는 것을 의미합니다. CI/CD에서 많은 프로세스를 자동화하였고, 프로젝트 그룹에서 코드를 점진적으로 계속 변경할 수 있는 자율성을 보장하였다면 지속적 제공의 마지막 단계인 배포에 대한 승인은 자동화되지 않습니다. 이 승인은 수동적으로 검토와 승인을 받아야 합니다.

지속적 배포(Continuous Deployment)는 위 단계까지 성공적으로 병합된 코드를 저장소 뿐만 아니라 사용자가 사용할 수 있는 배포환경(프로덕션)까지 릴리즈하는 자동화된 작업을 의미합니다. 이를 통해 사용자 피드백을 지속적으로 받고 통합하는 일이 훨씬 수월해질 수 있습니다. 자동화된 배포 방식은 수동적인 배포의 위험성을 줄여줍니다. 효율적인 지속적 배포 파이프라인에는 서비스에서 발생하는 문제에 신속하고 효율적으로 대응할 수 있어야 하며, 또한 복구 (롤백)이 가능해야 합니다. 지속적 배포로 서비스의 새로운 기능과 수정 사항을 사용자들에게 빠르게 제공할 수 있으며 서비스 출시 기간을 단축시켜줍니다.

image-20221204221254057

CI/CD Tools

대표적인 CI/CD 툴로는 Jenkins와 Travis가 있습니다.

Jenkins는 많이 사용 되고 있는 오픈 소스 소프트웨어로 문서화가 잘 되어 있습니다. 빌드/배포 이외에도 스케쥴링을 이용한 배치 작업에도 활용될 수 있습니다.

Travis CI는 Travis에서 만든 CI툴 입니다. Travis CI 웹페이지에서 우리가 사용 중인 GitHub Repository와 연동을 하고 난 뒤에, 해당 레포지토리에 푸쉬를 하게되면 자동 빌드 및 테스트를 진행 해줍니다.

Taxi의 CI/CD

SPARCS의 Taxi Project 역시 CI/CD 파이프라인을 가지고 있습니다. (2022-12-04 작성 당시의) 이 구조에 대해 간단히 설명드리고자 합니다.

image-20221204221254055

  • main으로 머지되는 PR이 열리면 Front(react)의 코드에 대해서는 자동으로 Cypress를 사용한 E2E Test를 진행하며, Back(node express)의 코드에 대해서는 Mocha를 사용하여 Unit Test를 진행합니다. 만약 이 단계에서 빌드나 테스트가 실패한다면 main 브랜치로 merge 할 수 없습니다. (참고: test_ci.yml)

  • main으로 코드가 merge가 되면 main 브랜치에 대해서 자동으로 Github release를 만들며 이 때 사용하는 tag를 이전 버전보다 높게 만들어 사용합니다. (참고: create_release_tag.yml)

  • main의 코드를 서비스에 사용할 수 있는 Docker 이미지로 자동으로 빌드합니다. 빌드된 Docker 이미지에는 위 release에 사용한 tag와 latest tag가 붙여집니다. 그리고 빌드된 이미지를 자동으로 Taxi의 AWS ECR repository에 push합니다. (참고: push_image_ecr.yml)

  • 채널톡 서버에서 실행 중인 도커 컨테이너인 taxi-watchtower는 이미지의 업데이트를 자동으로 감지합니다. 업데이트된 이미지를 pull 받아오며 이 이미지를 사용하여 taxi-front와 taxi-back 컨테이너를 업데이트 합니다.

아직까지는 무중단 배포가 아니기에 taxi 서비스는 새벽 4시에만 업데이트가 진행되도록 설정되어 있습니다. 보안점을 조언해주시거나 궁금한 점이 있으시다면 SPARCS 슬랙 DM으로 편하게 연락주세요.

긴 글 읽어주셔서 감사합니다.!!