본문으로 건너뛰기

디자인 패턴 - 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 패턴을 처음 접하는 분들께 도움이 되었길 바라며 마치겠습니다.

Prisma를 사용해보세요

· 약 11분
양지웅 (tom)
너무 어렵다...

SPARCS 서비스 중 하나인 OTL plus 팀에 백엔드 신입으로 참여하여 Nest.js를 이용한 여러 기능 구현 과제를 수행하게 되었습니다. 이 과정에서 MySQL과 Prisma ORM을 사용하게 되었고, 공식 문서에 잘 나와 있긴 하지만 직접 사용해 본 경험을 바탕으로 Prisma에 대한 간단한 소개를 해보겠습니다.(초보자의 경험을 바탕으로 적은 글이므로 더 쉬운 방법이나 오류가 있을 수 있습니다.. 양해 부탁드립니다😭)

Prisma가 뭐죠?

간단하게 설명하자면 Prisma ORM은 TypeScript와 JavaScript를 위한 데이터베이스 ORM입니다. Prisma를 사용하면 개발자가 SQL 쿼리를 직접 작성할 필요 없이, 모델과 필드를 정의하고, Prisma CLI를 사용하여 데이터베이스 스키마를 자동으로 생성할 수 있습니다. 이를 통해 데이터베이스 스키마 변경 시, TypeScript/JavaScript 코드가 자동으로 업데이트되기 때문에 개발 생산성을 높일 수 있습니다.

스키마 파일 작성

Prisma를 사용하기 위해서 진행해야 하는 가장 첫 단계라 할 수 있는 prsma schema 파일 작성입니다. 연결하는 DB에 대한 정보와 생성할 Model들에 대해 작성해줍니다. prisma schema파일은 Data sources, Generator, Data model definition의 세 부분으로 나눌 수 있습니다.

datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
anonymous Boolean @default(true)
auth Auth @default(USER)
}

enum Auth{
USER
ADMIN
}

Data sources는 prisma가 어떤 데이터 베이스에 어떻게 연결할지에 대한 정보를 적는 부분입니다. 연결하고자 하는 데이터 베이스의 종류(mysql, postgresql 등)와 데이터베이스 URL을 적어주면 됩니다. 여기서 URL의 경우 dotenv를 이용하여 env(DB URL입력)의 방식으로 환경파일을 사용함으로써 개발 데이터베이스와 프로덕션 데이터베이스를 분리할 수 있습니다. Generators는 prisma generate명렁어를 사용할 때 어떤 클라이언트를 생성할 것인지에 대해 설정해주는 부분입니다. Data model definition 테이블간의 관계, 각 field의 타입등 application model(Prisma model)들을 정의하는 부분입니다. TypeScript를 사용할 경우 index.ts파일에 prisma schema파일에서 지정해준 type에 맞게 다음과 같이 prisma client code를 자동생성해줍니다.

export type User = {
id: number @default(autoincrement()) @id
email: string @unique()
name: string | null
anonymous: boolean
auth: Auth
}

export const Auth: {
USER: 'USER',
ADMIN: 'ADMIN'
};

export type Auth = (typeof Auth)[keyof typeof Auth]

enum, type 을 사용해 새로운 타입을 지정할 수도 있고, @id, @unique, @default같은 attribute, autoincrement(), now()같은 function을 사용할수도 있습니다. 각 model은 record를 식별하기 위해서 @id, @@id, @unique, @@unique 중 최소한 하나를 지정해야 합니다. 여기서 @@는 두 field를 하나로 묶는다는 의미입니다. 다음과 같은 방식으로 지정하여 뒤에 설명할 updatefindunique에서 unique input으로 사용할 수 있습니다.

@@unique(fields: [id, isDeleted], name:"validChat")

schema파일에서 @relation을 이용하여 data model사이의 1:1, 1:N, N:M, 자기 참조 관계들을 생성할 수 있습니다.

model User {
id Int @id @default(autoincrement())
email String @unique
name String
test Test?
}

model Test{
id Int @id @default(autoincrement())
content String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
tests Test[]
}

model Test{
id Int @id @default(autoincrement())
content String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
anonymous Boolean @default(true)
spaces UsersInSpaces[]
}

model Space{
id Int @default(autoincrement()) @id
name String @unique
users UsersInSpaces[]
}

model UsersInSpaces{
user User @relation(fields: [userId], references:[id], onDelete: Cascade)
userId Int
space Space @relation(fields: [spaceId], references:[id], onDelete: Cascade)
spaceId Int
@@unique(fields: [userId, spaceId], name:"userspace")
@@id([userId, spaceId,roleId])
}
model Chat{
id Int @default(autoincrement()) @id
content String
chat Chat[] @relation("chat")
parent Chat? @relation("chat", fields: [parentId], references: [id])
parentId Int? @map("chatid")
level Int @default(0)
order Int @default(0)
@@unique(fields: [id, isDeleted], name:"validchat")
}

CRUD 간단 설명

  • create, createMany 새로운 DB record를 만들어줍니다. 앞서 설명했듯이 이전에 생성한 index.ts에 생성된 data model의 type에 맞춰 데이터를 넣어 생성할 수 있습니다.
    //this는 Nest.js에서 DI한 PrismaService를 가리킵니다.
    await this.prisma.user.create({
    data: { email: "foo@bar.com", name: "foo" },
    });

    await this.prisma.user.createMany({
    data: [
    { email: "first@foo.com", name: "first" },
    { email: "second@foo.com", name: "second" },
    { email: "third@foo.com", name: "third" },
    ],
    skipDuplicates: true, //생성되는 record중에서 unique field값이 중복되는 경우 생성하지 않습니다.
    });
  • findUnique, findFirst, findMany findUnique의 경우 where에 unique한 값만 넣을 수 있습니다. soft delete처럼 만약 두 field의 값을 묶어서 사용해야 하는 경우가 필요하다면 앞선 @@unique 의 예시처럼 사용하는 모델에 있는 isDeleted field와 다른 unique 또는 id field를 묶어주면 됩니다.
    @@unique(fields: [id, isDeleted], name:"validChat") //prisma.schema 파일 model 내부에 작성
    await this.prisma.order.fidnUnique({
    where: { validChat:{id: Id, isDeleted: false} },
    });
    findFirst, findMany의 경우 orderBy와 take를 사용해서 column기준 최대값, 최솟값 혹은 정렬된 몇개의 record를 얻을 수 있습니다. where내부에 OR, AND를 추가하는 것도 가능합니다.
    await this.prisma.order.findFirst({
    where: { id: Id, isDeleted: false },
    orderBy: { level: "desc" },
    take: 1,
    });
    await this.prisma.order.findMany({
    where: { name: "foo", isDeleted: false },
    orderBy: { level: "asc" },
    take: 5,
    });
  • update, upsert, updateMany update, upsert는 where에 unique input을 넣어 원하는 record를 찾아 data에 넣어주는 값으로 업데이트 해줍니다. upsert의 경우 찾는 record가 없다면 새로운 record를 생성합니다. update 종류들은 increment, decrement, multiply, divide, set등을 통해서 number 종류의 값들을 변경할 수 있습니다.
    await this.prisma.user.update({
    where: { id: id },
    data: { name: "bar" },
    });
    await this.prisma.user.upsert({
    where: { email: "first@bar.com" },
    update: { email: "second@bar.com" },
    create: { name: "foo", email: "second@bar.cpm" },
    });
    await this.prisma.post.updateMany({
    where: { name: "foo" },
    data: { likes: { increment: 1 } },
    });
  • delete, deletemany
    • record를 삭제하기 위해서는 delete혹은 deleteMMany를 사용하면 됩니다. delete, deleteMany 내부 where에는 unique input을 넣어줍니다.
    await this.prisma.user.delete({
    where: { email: "foo@bar.com" },
    });
    await this.prisma.user.deleteMany({
    where: { name: "foo" },
    });
  • select, include 앞서 설명한 CRUD에 넣어주고 데이터를 가져오기를 원하는 field에 true설정만 해주면 됩니다.
    await this.prisma.user.create({
    data: { email: "foo@bar.com", name: "foo" },
    select: { name: true, email: true },
    });

    await this.prisma.user.create({
    data: { email: "foo@bar.com", name: "foo" },
    include: { post: true },
    }); //user model에 post model이 @relation으로 묶여있는 경우
    select는 scalar field, include는 relation으로 연결되어 있는 다른 data model을 불어와 지정된 변수에 넣어줄 수 있습니다. 하지만 select를 통해서도 relation으로 연결되어 있는 data model을 불러올 수 있고, include와 select를 같이 사용하지 못하기 때문에 특별한 경우가 아니라면 select를 통해서 원하는 field와 연결된 data model을 불러오면 됩니다. Prisma에서는 select나 include에서 true로 설정한 field들로 변수를 구성하기 때문에 type검사에 있어 런타임 전에 문제가 되는 부분을 수정할 수 있습니다. 여기서 주의해야 할 점은 select를 통해 다른 model을 불러올 때 해당 모델은 빈 모델일 수 없다는 점입니다. 예를 들어 다음과 같은 상황에서 author model의 하위의 모든 field를 false로 둘 경우 런타임 전 type검사에서 문제가 없다 하더라도 런타임에서 ‘최소한 하나의 field는 불러와야 한다’는문제가 발생합니다.
    select: {
    author: {
    select:{
    email: false,
    name: false,
    img: false
    },
    },
    .....
  • Transaction Prisma에서는 pessimistic lock을 간단하게 설정할 수 있습니다. 기본적인 사용법에 대해서 하나만 소개하자면 다음과 같습니다.(Interactive transactions)
    await this.prisma.$transaction(async(tx) => {
    await tx.user.update ....
    await tx.author.create ....
    })

마무리

Prisma는 홈페이지에 있는 Documentation이 상당히 자세히 나와있고 저도 아직 사용해보지 못한 더 많은 기능들이 많습니다. 이 글에서 설명한 기능들 중에서도 설명하지 못한 더 다양한 옵션들도 있습니다. 사용자 또한 많기 때문에 처음 ORM을 사용해보더라도 쉽게 접근할 수 있습니다. 그렇기에 만약 누군가 처음 ORM을 사용해야 하는 상황이라면 저는 Prisma를 추천해주고 싶습니다. 이상 부족한점이 많은 Prisma간단 소개를 마치겠습니다. 읽어주셔서 감사합니다.

출처: https://www.prisma.io/docs

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으로 편하게 연락주세요.

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