📚소개





프로젝트 동기
🔥프론트엔드 공부라 말할 수 있는 공부를 하자🔥
- 프론트엔드를 공부하는 것인지, 웹 프레임워크(리액트, 뷰 등)을 공부하는 것인지 모르겠음
- 어느 순간 실무에서는 퍼블리싱만 하고 있는 느낌이 들었음
- 프론트엔드 공부 뭐할꺼야 라는 질문에,
그래프QL 사용해보기
등의 답변을 하는 내가 마음에 들지 않았음
- 내부 로직을 이론으로는 들었다 한들, 남에게 설명할 수 있는 수준이 아니었음
- 리액트만 다루다보니 시야가 너무 좁은 것 같다는 느낌을 받았음
프로젝트 목표
- 📌 리액트, 뷰 등의 웹 프레임워크(라이브러리)에 의존성 낮은 공부하기
- 📌 여러 프레임워크를 사용해보고, 각각의 장단점을 경험해보기
- 📌 프로젝트 시작시에, 근거를 기반으로 프론트 기술 스택을 선택할 수 있어야 함
프로젝트 방법
📈 함께 성장
- 개발 커뮤니티를 통해 함께 성장하기
- 참여자들 의견을 모두 공유하며, 타 참여자들에게 새로운 관점을 제안해보기
- 격주 참여자들이 담당한 부분에 대해서
코드잼-세미나
진행하기
🧑🏻💻 결과 주도 프로젝트(스터디)
- 결과물이 없는 프로젝트는 의미가 없다..!
- 프레임워크 별
투두앱
을 제작해보며, 각 프레임워크의 특징을 이해하자
- 기록을 남기며 부족한 점을 채우고, 나중에 다시 돌아와 내가 어떤 부분을 잘(혹은 잘 못)이해하고 있었는 지 파악하자
결과물
2023-FE-with-no-framework
gdsc-ssu • Updated Aug 30, 2023
todo-on-any-front-framework
gdsc-ssu • Updated Feb 3, 2023
fe-package-mono
itjustbong • Updated Apr 20, 2023
스택별 Todo
하기 글들은 Velog에도 동일하게 작성되어 있습니다.
Velog에서 보기
Angular
안녕하세요,
언제부터인가 프론트엔드 공부를 어떻게 해야 깊이있게 공부했다고 말할 수 있을까에 대한 고민이 많았습니다.
깊이있는 프론트엔드 개발을 위해 인프런의 멘토링 기능을 통해서 시니어 개발자 분들에게 조언도 들어보고,현직 실무에 나가있는 선배들을 통해서도 이런저런 조언을 많이 들어봤던 것 같습니다.
결국에는 조언들을 한 문장으로 요약해보면 "라이브러리나 프레임워크에 의존적인 공부를 하지 말아라" 였습니다.이유는 모두 다들 아실 것이라고 생각합니다.
그러면 어떻게 해당 조언을 통해 깊이 있는 공부를 할 수 있을까요,
저는 선배 개발자가 추천해준 "프레임워크 없는 프론트엔드 개발"이라는 책과지난 GDG 행사에서 얻은 정보를 토대로, 다양한 프레임워크를 접해보고 그 차이점과 장단점을 느껴보고,
최종목표를 프레임워크 없는 프론트엔드 개발을 해보고자 하였습니다.
이에 대한 첫번째 단계로 TODO-ON-ANY-FRAMEWORK 라는 프로젝트를 진행해보고자 계획을 세웠습니다.
우선 가장 궁금했고 리액트와 가장 다를 것 같은 Angular로 시작해보고자 하였습니다.
TODO-ON-ANY-FRAMEWORK의 깃허브 링크
해당 주제는 다음과 같은 순서로 진행해보고자 합니다.
1. 앵귤러의 특징을 알아봅니다.
2. 앵귤러를 기초적인 활용법을 알아봅니다.
3. 앵귤러를 활용하여 TODO 앱을 제작해봅니다.
4. 타 프레임워크와 차이점을 비교해봅니다.
(현재는 리액트만 비교하며, 프로젝트를 진행하면서 채워나갈 예정입니다.)
앵귤러의 특징

앵귤러는 "Angular는 단일 페이지 애플리케이션을 효율적이고 체계적으로 만들기 위해 개발된 프레임워크이자 개발 플랫폼입니다."이라고 하며, 구글에서 만들었습니다.
앵귤러는 구글이 만든 프레임워크 답게 (다행히 버려지진 않았지만) 큰 이벤트가 있었습니다.
그건 AngularJS 에서 Angular로 버전 업이 되면서,
다른 웹 프레임워크라고 해도 될 정도로 호환성 없는 브레이킹 체인지를 가지고 와서 많은 개발자에게 혼란을 주었던 이력입니다.
큰 변동 사항으로는 CBD로 전환, 데코레이터 지원, CLI 지원 등의 내용이며, 분명 더 좋은 프레임워크를 제공하기 위해 수정이 되었지 않았나 싶습니다.
물론 현재는 안정화가 되어 있다고 보며, 앞으로는 전과 같은 대규모 업데이트는 없을 것이라 믿습니다.
또한 최신 버전의 리액트를 접하신 분이라면 앵귤러는 낯설게 느껴질 수 있을 것 같습니다.
그 이유는 타 프레임워크와는 달리 OOP 내용을 적극적으로 활용했고,JS/TS를 기반으로 하는 프레임워크임에도 클래스로 시작해서 클래스로 끝나는 프레임워크이기 때문입니다.
이러한 특성때문에 기존에 제가 알던 웹 프레임워크와는 전혀 다른 개발 경험을 받았고,자바를 전문으로 하는 개발자(혹은 OOP에 진심인 개발자)가 프론트엔드 프레임워크를 만든 것 같다 라는 생각이 들었습니다.
앵귤러 기초 내용
Angular에는 몇 가지 중요 구성 요소들이 있습니다.
1. 컴포넌트: 애플리케이션의 뷰를 생성하고 관리
2. 디렉티브: 디렉티브가 사용된 요소에게 어떤 기능적인 것을 지시
3. 서비스: 컴포넌트 관심사 이외의 로직과 관련된 부분
4. 모듈: 컴포넌트, 디렉티브, 서비스 등의 Angular의 구성요소로, 하나의 서비스의 단위
위와 같은 구성 요소들(+@)이 하나로 합쳐져 하나의 앱을 구성할 수 있습니다.
또한 앵귤러는 그 진입장벽을 낮춰주기 위해 Angular CLI를 제공하며,CLI를 통해서 위의 구성 요소들을 빠르게, 그리고 Angular가 제안하는 패턴으로 서비스를 개발할 수 있는 환경을 제공합니다.
컴포넌트
컴포넌트는 사용자에게 보여지는 뷰 영역입니다.

사진과 같이 앵귤러는 컴포넌트의 로직과 템플릿이 명확하게 나뉘어져 있고,
앵귤러는 상태의 변화에 맞춰 모델과 뷰를 동기화 시킵니다.
상태변화의 캐치는 DOM 이벤트 캐치와 몇몇 비동기 이벤트를 감지하여 작동합니다.
서비스
서비스는 뷰를 제외한 어떤 로직 영역입니다.
데이터를 다루거나 하는 역할을 할 수 있습니다.

기타
이외에도 앵귤러로 개발을 시작하기 위해서는 클래스 개념, OOP에 대한 내용, 데코레이터 등 앵귤러 이외의 내용도 많이 등장합니다.중간에 포기하지되지 않으려면 위 내용도 충분히 접한 상태로 접근하면 좋을 것 같습니다!
ANG-TODO 코드 잼!
앵귤러를 처음 접해보고 진행한 프로젝트이다 보니 부족한 점이 있을 수 있으며 앵귤러의 철학과는 거리가 있을 수 있습니다.그렇지만 언제나 훈수는 환영입니다!
제일 먼저, 완성된 ANG-TODO 앱은 사진과 같습니다.

위의 사진과 같은 서비스를 만들고자 합니다.
요구사항은 크게 하기와 같습니다.
1. 투두를 추가할 수 있어야 합니다.
2. 투두의 상태를 보여주어야 합니다. (미완료, 완료)
3. 투두를 삭제할 수 있어야 합니다.
4. 투두를 로컬 스토리지에 저장하지만, 확장성이 있어야 합니다.
제가 신경을 쓰고자 했던 부분은 서비스와 관련된 부분입니다.
그 이유는 해당 부분을 GDSC 스터디에서 코드잼 형태로 진행할 예정이었으며,원래는 파이어베이스 혹은 서버와 연동되는 투두앱을 개발하고자 하였습니다.
하지만 제한된 스터디 시간을 고려하여 코드잼을 진행하기 위해서는 로컬스토리지를 활용하는 것이 맞다고 생각하였습니다.그렇지만, 누군가는 분명 파이어베이스 혹은 서버와의 연동까지 경험해보고자 할 수 있겠다라는 생각이 들었고, TODO 데이터를 저장하는 서비스를 쉽게 변경할 수 있어야겠다라는 생각을 하게 되었습니다.
그래서 저는 가장 많은 고민을 했던, 서비스 부분을 가장 먼저 구현해보았습니다.
앵귤러 프로젝트 생성
앵귤러 CLI를 통해 개발을 진행해야하므로, 전역에 관련 툴을 설치합니다.자세한 내용은 https://angular.io/cli 서 더 확인 할 수 있습니다.
npm install -g @angular/cli
이후 설치된 CLI를 통해서 프로젝트를 생성합니다.
ng new my-first-project
해당 서비스를 실행하고자 한다면,
ng serve
이렇게 첫 번째 앵귤러 앱이 만들어졌습니다.

서비스
데이터 모델
ANG-TODO 앱에서는 데이터를 관리할 서비스 하나가 필요합니다.
해당 서비스를 통해서 TODO 데이터를 관리할 것이기 때문에,그 데이터가 어떤 것인지에 대해 먼저 명시하는 것이 필요하다 생각하였습니다.
CLI에서 제공하는 인터페이스 생성 도구를 통해 TODO 의 데이터 타입을 지정합니다.
ng g i models/todo
데이터 구조는 달라질 수 있지만,저는 로컬스토리지가 아닌 백엔드DB에 실제로 데이터가 저장되어 활용할 수 있어야 한다는 점,상태를 변경할 수 있어야 한다는 점을 고려하였습니다.
또한 JS에서 enum을 사용하면 안좋다라는 의견도 있지만,JS 성능이나 관련 내용에 대한 스터디는 아니므로 개발 편의상 enum을 활용하였습니다.
또한 type이 아닌 interface를 사용한 이유는,개발자마다 의견은 다르지만,저는 객체는 interface로 타입을 지정하는 것이 맞다고 생각하여 하기와 같이 모델의 타입을 지정하였습니다.
export enum TodoState { DELETED = 'Deleted', DONE = 'Done', NORMAL = 'Normal', } export interface TodoType { id: number; todo: string; state: string; }
내/외부 서비스 인터페이스
이런 타입을 가지는 데이터를 이제 관리할 녀석이 필요합니다.해당 부분에서도 고민이 있었습니다.
1. 외부 서비스는 어떻게 구조를 가져야 할까.
2. 서비스와 외부 서비스를 역할은 어떻게 나눠야 할까.
3. 어떻게 확장가능한 서비스를 만들 수 있을까
위 고민에 대한 저의 생각은 "서비스는 외부 서비스 변경에 영향을 받지 않는 것이 제일 중요하다"였습니다.그래서 실제로 개발할때에는 로컬스토리지를 외부 서비스로 사용하지만 서버가 연결되거나 타 외부 서비스로 변경될 수 있다라는 것을 가장 중요하게 생각하며 개발을 진행하였습니다.
저희가 자체적으로 정한 투두 앱은 C,R,D 기능을 제공해야합니다.
그래서 우선 저는 외부 서비스가 어떤 역할을 제공해주어야 하는지 인터페이스로 명시하였습니다.
*인터페이스를 먼저 만든이유는 클래스를 구현할때 이런 멤버들을 만들어야 한다라고 먼저 명시하기 위해서 입니다.
(혹은 추후에 다른 클래스를 통한 기능을 제공하고자 한다면 인터페이스에 맞춰서 제작하면 된다는 것을 명시하는 것이 목적이었습니다.)
해당 고민을 통해서 하기와 같은 구조로 플젝을 구성해보고자 합니다.

외부 서비스를 위한 폴더는 따로 두지 않고,db-saver 라는 폴더에 관련 코드를 작성하였습니다.
// src/app/db-saver/db-saver.interface.ts import { TodoState, TodoType } from '../models/todos'; export interface iTodoDataSaver { add(data: string): void; allTodos: TodoType[]; updateTodoState(id: number, toState: TodoState): void; delete(id: number): void; }
위와 같이 외부 서비스가 지원해야하는 기능을 명시하였습니다.해당 내용은 투두 추가, 모든 투두 조회, 투두 상태 업데이트, 삭제 기능을 담고 있습니다.
그리고 인터페이스를 구현한 추상 클래스를 하나 생성하여,여러 서비스를 생성하거나 서비스를 변경할때 해당 추상 클래스를 구현하도록 합니다.
추상 클래스
추상 클래스는 단순하게 인턴페이스의 내용이 포함되고,다만 todo를 추가정보를 담아 객체로 변환해주는
todoObjBuilder
만 구현되어 있습니다.todoObjBuilder
는 일반적인 백엔드와 DB를 사용한다면 내부 서비스 측에서 외부 서비스를 이용할때 string 형태로 값이 올 것임을 고려했습니다.해당 경우에 클라이언트 -> 서버로 투두를 string 형태로 값을 보낼 것 이고 (POST {"todo" : "밥 먹기"})백엔드에서는 DB튜플과 관련한 추가적인 정보인 id와 기본적으로 미완료 상태로 데이터를 추가할 것입니다.
그래서 로컬 스토리지에 저장되는 데이터도 해당 형식과 같이 저장하는 것이 맞다고 생각하여, (또 필요하기도 하고)
todoObjBuilder
를 DBSaver의 메소드로 구현하였습니다.// src/app/db-saver/data-saver.ts import { TodoState, TodoType } from '../models/todos'; import { iTodoDataSaver } from './data-saver.interface'; export abstract class TodoDBSaver implements iTodoDataSaver { todoObjBuilder(todo: string) { return { todo: todo, state: TodoState.NORMAL, id: Date.now(), }; } abstract add(data: string): void; abstract allTodos: TodoType[]; abstract updateTodoState(id: number, toState: TodoState): void; abstract delete(id: number): void; }
그리고 이 추상 클래스를 구현한 클래스가 외부 서비스가 됩니다.동일한 폴더에 클래스를 구현합니다.
외부 서비스 구현
// src/app/db-saver/db-saver.localdb.ts export class LocalDBSaver implements TodoDBSaver {}
위 클래스의 멤버들을 인터페이스에 맞춰 하나씩 구현해보고자 합니다.
먼저, 로컬 스토리지에 값을 저장하는 메소드를 따로 빼고 CRUD를 구현할때 재활용하고자 하였습니다.
localDBSaver(todosObjData: TodoType[]) { try { localStorage.setItem(this.localDBKey, JSON.stringify(todosObjData)); } catch (e) { console.log('LocalDBSaver save 에러'); } }
- 참고로 localStorage 에는 String 형태의 타입만 저장할 수 있습니다.

위 두 메소드는 인터페이스를 구현하면서 생성될 메소드들로 부터 사용될 가장 기본적인 역할을 담당하게 될 것입니다.
add 구현
이제, 인터페이스의 첫번째 요구사항인
add(data: string): void;
를 구현해보고자 합니다.
일반적인 REST API에서는 클라이언트가 String 형태의 값을 주면 그 값을 중간 로직을 거쳐 DB에 저장한다는 것을 고려하여 구현하였습니다.add(todo: string) { const todoObj = this.todoObjBuilder(todo); const prevTodos = this.allTodos || []; let todoObjData; if (prevTodos.length === 0) { todoObjData = new Array(todoObj); } else { todoObjData = prevTodos.concat(todoObj); } this.localDBSaver(todoObjData); }
위에서 언급했듯이 localStorage 는 문자열 형태의 데이터만 저장되다 보니, 객체 형태로 저장된 데이터를 곧바로 업데이트 할 수는 없습니다.
그래서 기존에 저장된 데이터를 가져와서 추가 로직을 수행한 이후에 동일한 키 값으로 데이터를 덮어씁니다.
getter allTodos 구현
두번째 요구사항인
allTodos: TodoType[];
를 구현해보자 합니다.getter 를 활용하여 데이터에 접근하고 객체에는 직접 접근하지 못하도록 할 것입니다.get allTodos() { const localData = JSON.parse( localStorage.getItem(this.localDBKey) || '[]' ) as TodoType[]; return localData; }
이 메소드에서는 추가 작업 없이 그냥 데이터를 리턴해줍니다.다만, 추후에 state에 따라서 정렬해주는 로직을 추가한 getter를 구현하는 등의 추가 요구사항에 대응할 수 있을 것입니다.
updateTodoState 구현
그리고 세번째 요구사항인
updateTodoState(id: number, toState: TodoState): void;
입니다.해당 메소드는 todo의 상태를 업데이트 하는 녀석으로 "완료 -> 미완료", "미완료 -> 완료" 로 상태를 업데이트 해주는 기능을 제공합니다.updateTodoState(id: number, toState: TodoState) { const allTodos = this.allTodos; const todoIdx = allTodos.findIndex((todo) => todo.id === id); allTodos[todoIdx].state = toState; this.localDBSaver(allTodos); }
위와 마찬가지 이유(로컬스토리지 관련)로, 우선 기존의 저장된 데이터를 가져옵니다.
그리고 클라이언트(편의상 뷰에서 넘어오는 데이터를 클라이언트라고 하겠습니당)에서 전달 받은 id(
todoObjBuilder
을 통해 생성된 값)와 변경되어야 할 상태 정보를 받아와서 그 값을 업데이트 합니다.delete 구현
마지막 요구사항인
delete(id: number): void;
을 구현합니다.이름에서도 알 수 있듯이 사용자가 삭제하라고 요청한 id 값을 받아 그 객체를 삭제합니다.
delete(id: number) { const allTodos = this.allTodos; const filteredTodos = allTodos.filter((todo) => todo.id !== id); this.localDBSaver(filteredTodos); }
이렇게 외부 서비스에 대한 구현을 마무리했습니다.
이제 컴포넌트에서 직접 접근하여 사용할 내부 서비스를 구현합니다.
내부 서비스 구현
해당 서비스는 사실 큰 역할은 없습니다.컴포넌트와 외부 서비스를 이어주는 역할만 담당합니다.
다만, 외부 서비스를 개발자의 입맛에 맞춰 변경할 수 있도록 하였습니다.
import { Injectable } from '@angular/core'; import { DBSaver } from '../data-saver/data-saver'; import { TodoState } from '../models/todos'; @Injectable({ providedIn: 'root', }) export class TodosService { constructor(private dataSaver: DBSaver) {} get allTodos() { return this.dataSaver.allTodos; } add(todo: string) { this.dataSaver.add(todo); } delete(id: number) { this.dataSaver.delete(id); } updateState(id: number, state: TodoState) { this.dataSaver.updateTodoState(id, state); } }
@Injectable 및 모듈
위의 데코레이터는 앵귤러를 사용하기 때문에 추가된 녀석인데,앵귤러라고 하는 프레임워크에 이 클래스는 root라고 하는 모듈에 추가시켜줘 라고 하는 내용을 담고 있습니다.
앵귤러에서는 모듈은 관련된 역할을 담당하는 구성요소(컴포넌트, 디렉티브, 파이프, 서비스 등)를 하나의 단위로 묶는 매커니즘(?)입니다.
모듈의 구조(정확히는 인젝터 트리)를 살펴보면 다음과 같습니다.

사진과 같은 구조가 나오게 된 이유는 싱글톤 인스턴스 범위 때문이라고 생각합니다.
앵귤러는 싱글톤 패턴을 활용하는데, 모듈은 여기서 하위 컴포넌트가 요청한 인스턴스를 어떤 scope를 나누어주는 역할도 할 수 있습니다.
모듈을 통해 싱글톤 패턴이 가지는 특성(동일한 클래스에 대해서는 하나의 인스턴스)을 활용합니다.컴포넌트가 요청한 인스턴스가 모듈내에 있는지 파악(컴포넌트에서는 인젝터가 프로바이더 토큰 값을 토대로 기존 인스턴스가 있는지 파악)하고,없다면 useClass 프로퍼티(일반적으로는 providers의 값과 동일)의 인스턴스를 생성하고 의존성을 주입합니다.
참고로, 하기 두 코드는 동일합니다.
providers: [{ // 의존성 인스턴스의 타입(토큰, Token) provide: GreetingService, // 의존성 인스턴스를 생성할 클래스 useClass: GreetingService }]
@Component({ ... providers: [GreetingService] /* @Component 프로바이더 */ })
이것을 토대로 모듈을 하나의 독립된(정확히는 재사용 가능한...?) 요소들의 묶음으로 활용할 수 있습니다.
그래서 제가 구성한 코드에서는 그냥 root라고 하는 모듈 하나에 todo와 관련된 컴포넌트와 해당 서비스가 모듈의 구성요소로 포함될 것입니다. (해당 모듈의 구성요소에서 동일한 서비스를 여러번 호출한다하더라도, 동일한 인스턴스가 주입될 것 입니다.)
느슨한 결합
앞서 서비스를 구현하면서 가장 중요하게 생각했다고 한 부분이 "외부 서비스에 흔들리지 않는 서비스를 구현하는 것" 이었습니다. 그래서 저는 해당 서비스의 인스턴스를 선택할때, 사용자가 원하는 외부 서비스(DBSaver)를 선택할 수 있도록 하고자 하였습니다.
그래서 모듈이 해당 인스턴스를 생성할때는 생성자와 함께 외부 서비스를 넘겨 받을 수 있도록 하였습니다.
이렇게 내부 서비스 구현까지 완료했습니다.
컴포넌트 구현
가장 먼저 모듈을 살펴보면 지금까지 구현한 요소들이 해당 모듈에 포함되어 있어야 합니다.
@NgModule({ declarations: [AppComponent, HeaderComponent, TodoListComponent], imports: [ BrowserModule, ReactiveFormsModule, RouterModule.forRoot([{ path: '', component: TodoListComponent }]), ], providers: [], bootstrap: [AppComponent], }) export class AppModule {}
- 컴포넌트와 관련한 부분은 없는 것이 맞습니다!
해당 모듈을 살펴보면 declarations 에는 생성한 컴포넌트(뷰)가 포함되어있고,
imports
에는 각종 모듈들이 포함됩니다.bootstrap
은 모듈의 첫 진입 컴포넌트입니다.AppComponent
첫 진입 컴포넌트인
AppComponent
을 살펴보면@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'ang-todo'; }
로 구성이 되어 있습니다.
위에서 간랸하게 소개했던 컴포넌트와 템플릿의 구조를 가지며,뷰와 로직이 완벽하게 분리된 모습입니다.
템플릿은 하기와 같이 구성되어 있습니다.
<app-header></app-header><div class="container"><router-outlet></router-outlet></div>
앵귤러에서는
seletor
라는 녀석을 통해서 컴포넌트의 뷰를 마크업으로 표현할때 사용할 수 있습니다.그래서 <app-header></app-header>
는 실제로 app-header
라는 컴포넌트를 가져와 보여줍니다.참고로, seletor 말 그대로 seletor의 역할을 합니다.즉, 해당 가 있는 곳을 쿼리셀렉터 마냥 찾고, 해당 DOM에 템플릿을 가져다 붙입니다.또한
router-outlet
는 RouterModule을 사용하면서 라우팅과 관련된 기능을 사용하기 위해서 작성되었습니다.app-header
app-header는 말그래도 앱의 페이지에서 헤더의 역할을 합니다.
해당 컴포넌트를 생성하기 위해서 하기와 같은 CLI 명령을 사용할 수 있습니다.
ng g c app-header
다만, 해당 컴포넌트는 딱히 설명이 필요한 부분이 없기도 하고 필요없이 포스팅 길이만 길어지는 것 같아 깃허브 링크를 남기도록 하겠습니다!https://github.com/itjustbong/todo-on-any-framework
todo-list
todo-list도 마찬가지로 설명이 많이 필요하지는 않을 것 같지만,필요한 부분에 대해서만 글을 작성하려고 합니다.
// todo-list.component.ts @Component(~~) export class TodoListComponent { db = new TodosService(new LocalDBSaver()); newTodo = new FormControl(''); addTodo(e: any) { e.preventDefault(); if (!this.newTodo.value) return; this.db.add(this.newTodo.value); this.newTodo.setValue(''); } delTodo(id: number) { this.db.delete(id); } updateState(id: number, nowState: string) { if (nowState === TodoState.NORMAL) this.db.updateState(id, TodoState.DONE); else this.db.updateState(id, TodoState.NORMAL); } }
위의 컴포넌트에서는 여러 로직이 포함되어 있습니다.
가장 먼저, 앞서 말했던 "느슨한 결합"에 대해서는 하기와 같이 사용할 수 있습니다.
db = new TodosService(new LocalDBSaver());
만약 이것을 로컬스토리지가 아니라 서버나 파베로 이전하고 싶다면, 해당 외부 서비스를 인터페이스에 맞춰 구현한 이후,db = new TodosService(new FirestoreSaver());
와 같이 사용할 수 있을 것입니다.
그리고 이 서비스는 인스턴스로 한번 생성되면 같은 모듈안에서는 계속해서 재활용 될 것 입니다.그리고 컴포넌트는 해당 인스턴스를 받아,컴포넌트에서 필요한 기능 CRUD 기능을 제공합니다.
또한 해당 컴포넌트의 템플릿을 살펴보면
디렉티브
를 활용합니다.디렉티브는 요소안에 작성되어 해당 요소가 어떤 역할(기능)을 할 수 있도록 지시(?)를 해줍니다.대표적으로 반복 돔 생성, if 조건 등의 지시가 있습니다.
<li *ngFor="let item of db.allTodos"> <></> </li>
우선
*ngFor
는 구조 디렉티브이며, 돔의 반복 생성을 제공합니다.참고로 *
이 붙은 디렉티브는 문법적 설탕이며 실제로는 하기 코드로 변환됩니다.<ng-template ngFor let-item [ngForOf]="db.allTodos"><element>...</element></ng-template><ng-template ngFor let-item [ngForOf]="db.allTodos" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"><element>...</element></ng-template>
그래서 결과적으로는
db.allTodos
의 값들이 순환되어 뿌려질 수 있습니다.또한
*ngIf
도 마찬가지의 메커니즘으로 동작합니다.<div *ngIf="item.state === 'Done'; then done; else none"></div><ng-template #done>~</ng-template><ng-template #none>~</ng-template>
해당 코드는 itme.state가 Done일 경우에는
<ng-template #done>~</ng-template>
를 렌더링하고 그외의 경우에는 none 컴포넌트를 렌더링합니다.그리고 데이터 바인딩과 관련하여 작성된 코드가 포함되어 있습니다.
<input type="text"name="newTodo"required="true"[formControl]="newTodo"/>
앵귤러JS는 양방향 데이터 바인딩을 지원합니다.
하지만 앵귤러는 프로젝트의 데이터 흐름을 단방향으로만 제어하고 있습니다.
그럼에도 불구하고 앵귤러는 DX를 고려하였는지 단방향임에도 Form을 양방향데이터 바인딩처럼 활용할 수 있도록 하는 디렉티브를 모듈형태로 제공하고 있습니다.(단, 모듈에
FormsModule
을 등록해야합니다.)해당 디렉티브는 이벤트 바인딩과 프로퍼티 바인딩을 함께 등록하고, 실제 동작시에도 이벤트 바인딩과 프로퍼티 바인딩의 조합으로 작동합니다.
만약 위 코드를 이벤트 바인딩과 프로퍼티 바인딩을 나눈다면 하기와 같을 것 입니다.
<input type="text" [value]="name" (input)="name=$event.target.value">
완성
컴포넌트들의 코드에 대해서는 벨로그에 전부 작성하지는 못하여,궁금하신 분들은 깃허브에서 모든 코드를 확인해주시면 감사하겠습니다. https://github.com/itjustbong/todo-on-any-framework
저 또한 앵귤러를 접한지 일주일도 지나지 않아 진행한 플젝이다보니 부족하거나 이상한 점이 많이 있을 수 있습니다.어떤 의견이든 좋으니 댓글이나 이슈 달아주시면 매우 감사드리겠습니다🧑🏻💻
타 프레임워크와의 차이 및 DX
우선 현재 제가 todo-on-any-framework를 진행하는 초기라서 모든 프레임워크와의 비교는 어렵습니다.(추후 채워나갈 예정입니다)
‼️ 리액트에 지쳐버린 개발자의 생각이라 일부 편협적인 내용이 있을 수 있습니다ㅎ해당 플젝을 진행하기 전부터 가장 많이 사용했던 리액트와 비교해보자면,
VS 리액트
OOP 그리고 구조
처음에 앵귤러를 접했을 때, 굉장히 프론트엔드 프레임워크 같지 않았습니다.여태 프론트 개발 방법과는 상당히 달랐고,이것은... 새롭게 접해야하는 내용이 많았다라는 것과 동일합니다ㅎ
그래도 안드로이드 프로젝트를 진행하면서 프론트+OOP에 대해 나름 어느정도 적응이 있었기에,그나마 그 문턱을 낮춰준 것 같습니다.그럼에도 불구하고 처음에 앵귤러를 접하며, "싱글톤, DI, 디렉티브, OOP 그 자체, 데코레이터 등"의 키워드 등을 보고 세상을 넓고 공부할 건 언제나 넘치구나 라는 걸 느끼며, 내용을 이해하는데 많은 시간을 투자했습니다.
그치만 해당 내용을 이해하고 앵귤러 투두 프로젝트를 시작하면서 부터는 굉장히 마음에 들었습니다.왜냐하면 구조에 대해서 명확하게 프레임워크가 가이드를 제안하고 있었고,뷰와 로직이 완벽하게 분리되어 개발을 진행할 수 있었습니다.
또한 리액트에는 쉽게 떠올리지 못했던 인터페이스와 서비스를 개념을 활용하여 각각의 결합이 강하지 않게 결합될 수 있도록 개발을 진행할 수 있었습니다.
다만, 웹을 처음 접하는 분들에게는 엄청난 학습 곡선이 필요할 수 있겠다라는 생각도 듭니다.
Form과 관련된 개발 경험 + 디렉티브
Form 개발에 대한 경험이 리액트에서보다 앵귤러에서 너무나 좋았습니다.
리액트에서는 뭐하나 하려고 하면 그것에 대응되는 코드를 구성하고 하는 과정이 꽤 복잡하고 귀찮은 작업이라고 여겨졌는데,앵귤러에서는 프레임워크 자체에서 디렉티브를 통해 "단방향 바인딩임에도 불구하고" 양방향 데이터 바인딩과 같이 개발 편의를 올려주고 있다는 점이 매우 마음에 들었습니다.
또한 프레임워크 답게 앵귤러는 개발자가 필요로 할 것 기능들을 이미 디렉티브 형태로 제공하고 있었습니다.다만 리액트의 JSX가 오히려 가독성과 개발 경험이 좋을 수도 있겠다라는 영역도 있었습니다.예를 들어 반복문이나 조건문은 제 기준, 디렉티브 보다는 JSX로 구현하는게 더 편하게 느껴졌습니다.아무래도 디렉티브도 새롭게 접하는 내용이다보니 이렇게 느끼는 것 일 수도 있습니다..ㅎ
앵귤러의 개발 경험
너무 좋다! 근데 왜 앵귤러는 묻혀버린걸까
그냥 좋았습니다.위에서 언급했듯 제가 너무 리액트에 지쳐서 이렇게 느끼는 것일 수도 있지만,구조가 명확하게 나뉘고, OOP 개념을 통해서 서비스를 확장성있게 고민해볼 수 있었다는 점 등 재미있는 부분이 많이 있었습니다.
누군가 앵귤러를 다음 프로젝트의 기술 스택으로 선택할 것인지 묻는다면,저는 form 사용이 많고, 외부 서비스에 대한 교체가 빈번할 것으로 예상된다면 앵귤러를 선택해 볼 것 같습니다.
긴 글 읽어주셔서 감사드리며, 위에서 언급한 내용이 100% 맞는 내용이라는 보장은 없습니다.그래서 다른 의견이나 틀린 내용이 있다면 지적해주시면 감사드리겠습니다!
VueJS
해당 포스팅도 앵귤러와 동일한 목차로 정리해보고자 합니다.
- Vuejs의 특징을 알아봅니다.
- Vuejs의 기초적인 활용법을 알아봅니다.
- Vuejs를 활용하여 TODO 앱을 제작해봅니다.
- 타 프레임워크와 차이점을 비교해봅니다.
📌 Vuejs의 특징
간략 소개
vuejs는 본인들 스스로 "웹 사용자 인터페이스를 만들기 위한 쉽고 강력하며 다재다능한 프레임워크" 라고 소개하고 있습니다.
리액트와는 달리 프레임워크라고 소개하는 부분이 인상적인데,
어떤 차이점으로 이렇게 소개하는지 알아보면 좋을 것 같습니다.
그리고 vuejs의 개발 목적은 다음과 같다고 합니다.
- 개발자에게 더 쉽고 가볍고 누구나 빨리 배울수 있는 프레임워크
위와 같은 방향성을 토대로 만들어졌기 때문인지,
전에 살펴봤던 앵귤러와는 다르게 기존 웹 개발자들의 DX가 크게 달라지는 것 같지는 않았습니다.
위와 같이 느낀 이유는 하기와 같습니다.
- 일반 스크립트 태그로 CDN을 통해 사용 가능
- 기존 HTML 마크업 템플릿을 거의 그대로 사용
- css를 작성하는 스타일 문법 동일
또한 vuejs는 컴포넌트 기반 프로그래밍 모델을 제공하는데,
이는 앵귤러 리액트등 모던 프론트엔드들과 크게 다르지 않습니다.
더욱이, 선언적 렌더링과 반응성 이라는 키워드도 vuejs에서 빼먹을 수 없는데,
해당 개념도 다른 도구들과 크게 다르지 않은 모습입니다.
Vuejs 3.0에서의 변화
얼핏 듣기로 vue3.0이 세상에 나오기까지 예정된 일정보다 많이 늦어져서,
"수 많은 vuejs 개발자 분들이 react로 이민을 갔었다" 라는 이야기를 들었습니다.
vue3 이전의 버전에는 어떤 문제가 있었길래 위와 같은 말들이 나왔을까요?
우선 컴포넌트 코드의 재사용이 어려웠습니다.
주로 로직을 재사용하기 위해 믹스인 방식을 많이 활용했는데,
하나의 컴포넌트에서 믹스인을 2개 이상 활용하면,
로직의 흐름을 따라가기 어렵습니다.
또한 더 심각했던 문제는 타입스크립트에 대한 지원이라고 생각합니다.
이전 버전에서도 ts를 활용할 수는 있었지만,
vuejs의 철학과도 맞지 않게,
굉장히 앵귤러 스럽게 vuejs 프로젝트를 구성해야 합니다.
이런 문제를 가지고 있던 vuejs는 하기와 내용에 대해 업데이트를 제공합니다.
- 로직 재활용성 개선 위에서 기존 버전의 문제로 언급했던, 로직의 재활용성을 개선했습니다. 대체로 리액트에서 함수형 컴포넌트를 사용하면서 경험할 수 있는 재사용성과 비슷한 개념이라 보입니다.
- 타입스크립트 지원 기존 버전은 객체 방식으로 컴포넌트를 구성했습니다. 그렇기 때문에 타입스크립트를 100% 활용하는데 어려움이 있었지만, 업데이트를 통해서 타입 추론등의 기능을 더 잘 활용할 수 있게 되었습니다.
객체 구조 방식에서 타입스크립트를 사용하기 어려운 이유 TS의 타입 추론은 명시적으로 타입을 지정하지 않아도, TS가 타입을 추론해준다 라는 것인데, 객체 구조에서는 개발자가 타입을 정의해야하는 경우가 많다고 합니다. 이와 관련된 내용에 대해 더 잘 아시는 분이 있다면, 피드백 부탁드립니다...🙏
- 가상 DOM 비교 개선 가상 돔과 관련되어 있으며, 기존의 전체 트리에 대해 diff를 확인하는 것이 아니라, 이제는 정적 요소와 동적요소를 구분하여, 동적요소만 diff를 비교 한다고 합니다.
- 중복 객체 생성 억제 리렌더링 등의 이유로, 한 객체가 여러 번 생성되는 것을 방지하고자, 컴파일러가 관련 내용을 탐지하여, 렌더링 함수 밖으로 호이스팅을 제공합니다.
- 트리쉐이킹 해당 개념은 번들링시에 필요한 코드만 가져온다는 개념으로 파일의 크기를 크게 줄일 수 있습니다.
- 컴포지션 API 기존의 옵션 API와는 달리 함수 기반의 API를 제공합니다. 리액트가 클래스형 컴포넌트에서 함수형 컴포넌트로 넘어가던 과정과 굉장히 유사하네요. 해당 API에서는 모든 코드를 독립적으로 정의할 수 있고, 각각의 기능을 함수로 묶어서 처리할 수 있어 유지보수에 장점이 있습니다.
- 기타 변경 사항
텔레포트 (React의 Portals)
프래그먼트 (React의 <></>)
서스펜스 (React의
<Suspense fallback={<></>}/>
) 리액티비티API (React의 useState)
📌 Vuejs 활용 기초
기본적으로 Vuejs는 React와 상당히 유사한 부분이 많이 있는 것 같습니다.
코드를 구성하는 방법이나 API를 활용하는 세세한 부분에서는 차이가 있겠지만,,,,
그래서 간략하게 Vuejs에서 많이 사용되는 문법을 기록해보고자 합니다.
디렉티브
해당 개념은 리액트에서는 생소할 수 있지만,
전에 다루었던 앵귤러에서 이미 한번 확인했던 문법입니다.
<a v-bind:href="imageUrl">
과 같이 동적 인자 등의 값을 세팅하거나,
해당 컴포넌트 혹은 DOM에게 어떤 작업을 지시하는데 사용할 수 있습니다.v-if
, v-bind
, v-on
등 다양한 기본 디렉티브가 제공되고 있습니다.해당 디렉티브들은 다음과 같은 구조를 가집니다.

당연하게도 앵귤러와 마찬가지로, 커스텀 디렉티브도 생성할 수 있습니다.
이벤트 핸들러
vue에서는 디렉티브를 적극적으로 활용하기 때문인지,
리액트와 사용법이 살짝 다른 모습을 확인할 수 있었습니다.
v-on:이벤트 명
혹은 @이벤트 명
과 같은 방식으로 사용할 수 있습니다.<button v-on:click="클릭했어요">클릭</button><button @click="클릭했어요($event)">클릭</button>
여기에 더해, 이벤트 관련 디렉티브에서는 수식어를 활용하여,
리액트에 비해 더 간결한 코드를 작성할 수 있습니다.
<input @keydown.enter="엔터가_눌렸어요">
위 코드에서 보듯이,
enter라는 키가 눌리면 실행될 함수를 쉽게 붙일 수 있습니다.컴포넌트간 이벤트 전달
리액트에서는 컴포넌트에서 함수를 props로 내려서 그냥 이벤트가 발생하는 곳에 연결하였습니다.
vue도 크게 다르지는 않지만,
Evnet emit에 대한 추가적인 문법을 제공합니다.
그래서 함수를 내리는게 아니라,
이벤트를 상위 컴포넌트로 던져서 로직을 실행시키는 구조로 구성이 가능합니다.
// 최하위 컴포넌트 (이벤트 발생) <button @click="$emit('delete', 10)">삭제</button> // 중간 컴포넌트 (이벤트 전달) <todo-item @delete="$emit('remove', $event)"></todo-item> // 최상위 컴포넌트 (이벤트 소비) <todo-list @delete="deleteTodo"></todo-list> ~ deleteTodo(num) { axios.delete('/todo/' + num); } ~
컴포넌트간 데이터 전달
해당 부분도 리액트와 유사한 방법을 활용합니다.
다만 차이점이라면, 문자열은 그냥 내리면 되지만,
그 외의 값은 디렉티브를 활용해야합니다.
문자열을 전달하는 경우,
<velog title="VUE 투두"></velog>
그 외의 값을 전달하는 경우,
<template> <velog :data="데이터"></velog> </template> <script> export default { data() { return { 데이터: '벨로그 안녕!' } } } </script>
데이터 바인딩
해당 내용은 앵귤러에서도 다루었던 내용입니다.
앵귤러와 마찬가지로 input에 입력되는 값과 데이터를 동기화 시킬 수 있습니다.
<input v-model="text">
위의 코드를 풀어서 쓰면 다음과 같이 작성할 수 있습니다.
<input :value="text" @input="event => text = event.target.value">
📌 Vue Todo 코드잼
먼저, 완성된 Todo 앱의 화면입니다.

옵션 API를 활용하여 프로젝트를 진행하였습니다.
초기 세팅
전체적인 프로젝트 구조는 하기 사진과 같이
App.vue
에서 todoList에 대한 데이터와 관련 로직을 전부 담당하도록 할 예정입니다.이후 필요에 따라서,
해당 데이터와 로직을 props로 통해서 내려주는 과정을 거칠 것입니다.
이렇게 진행한 이유는,
해당 프로젝트의 특성상 많은 데이터를 다루는 것이 아니라 todoList 하나의 데이터에 대해서만 다루고 있으며,
props로 모든 값을 내린다고 하여도,
단계가 3단계 뿐이기 때문에 props drilling으로 발생하는 문제는 크게 없어 보였습니다.
그 대신 데이터와 관련된 로직을 최상단에서 관리하면서 얻게될 유지보수성 및 가독성이 더 크다고 판단하였습니다.
모델
우선 관련
App.vue
에 포함될 todoList의 로직에 대해서 모두 구현해야합니다.추가로 고려해야하는 사항은
localStorage
에 값이 저장되어야 한다는 것입니다.ang-todo에서 진행한 바와 같이
localStorage
에 값이 저장될 때에는,
문자열 값만 들어갈 수 있기 때문에,
데이터를 저장하거나 읽어올 때 추가적인 작업이 항상 필요합니다.그래서 해당 작업을 대신할 수 있도록 유틸함수를 만들었습니다.
(localStorage의 인스턴스를 만들고 데이터 입출력 함수를 수정하여 내보내는 것도 가능했겠지만, 오히려 가독성과 블랙박스를 키울 수 있을 것 같다는 생각이 들었습니다.)
// utils/storage export const getData = (key: string) => { const savedData = localStorage.getItem(key); return savedData && JSON.parse(savedData); }; export const saveData = (value: unknown, key: string) => { const toJson = JSON.stringify(value); localStorage.setItem(key, toJson); };
위의
localStorage
의 util 함수를 이용하여 다음과 같은 함수를 구성하였습니다.methods: { addTodoItem(todo: string) { if (todo) { const newTodoData = { id: this.todoList[this.todoList.length - 1]?.id + 1 || 1, // 마지막 요소의 id값 + 1 completed: false, todo, }; this.todoList.push(newTodoData); // LocalStorage 업데이트 saveData(this.todoList, TODO_STORAGE_KEY); } }, toggleTodoItem(targetId: number) { this.todoList.forEach((todoItem) => { if (todoItem.id === targetId) { todoItem.completed = !todoItem.completed; // toggle } }); // LocalStorage 업데이트 saveData(this.todoList, TODO_STORAGE_KEY); }, removeTodoItem(targetId: number) { this.todoList = this.todoList.filter( (todoItem) => todoItem.id !== targetId ); // LocalStorage 업데이트 saveData(this.todoList, TODO_STORAGE_KEY); }, resetTodoList() { this.todoList = []; // LocalStorage 업데이트 saveData(this.todoList, TODO_STORAGE_KEY); }, },
그리고 해당 todoList는 앱이 실행되면 항상 불러져와야 합니다.
컴포넌트가 생성되고 DOM에 마운트되기 전에 불려지는 훅인
created
에 관련 로직을 작성해주면 됩니다.라이프사이클에 대한 자세한 내용은 재그지그님의 블로그에 확인할 수 있습니다.
created() { const storageData = getData(TODO_STORAGE_KEY); this.todoList = storageData; },
template와 모델 연결하기
우선 해당 과정에서 필요한 내용은 크게 다음과 같았습니다.
- props로 데이터 / 함수 전달하기
- 데이터 / 함수를 사용하기
만들어진 App구조에 따르면,
App.vue
의 데이터와 함수를 props로 전달해야합니다.단순히 데이터를 전달하는 경우에는 정적인 데이터와, 동적인 데이터를 내릴 수 있습니다.
공식문서-props
todoList는 CRUD가 가능한 모델이기에 동적 데이터로 취급하였으며,
다음과 같이 데이터를 전달할 수 있습니다.
<MainWrapper :todoList="todoList" />
하위 컴포넌트에서는 위 데이터를 다음과 같이 받을 수 있습니다.
<script lang="ts"> // ... export default { // todoList 라고 하는 props를 받아온다. props: ["todoList"], } </script> <template> <TodoList :todoList="todoList" /> </teamplate>
자식 컴포넌트에서 발생한 이벤트를 받아서,
부모 컴포넌트에서 실행시키는 방법은 다음과 같습니다.
공식문서-이벤트
내장 메서드 $emit을 사용하여 템플릿 표현식에서 직접 사용자 정의 이벤트를 발신할 수 있습니다
methods: { addTodoItem(todo: string) { this.$emit("addTodoItem", todo); }, }
$emit에서 첫 번째 인자는 사용자 정의 이벤트를,
두 번째 이후부터는 해당 이벤트의 인자 값들을 전달할 수 있습니다.
부모 컴포넌트에서는 해당 이벤트를 다음과 같이 전달받을 수 있습니다.
// 화살표 함수 <MyButton @increase-by="(n) => count += n" /> // 혹은 이벤트 핸들러 <MyButton @increase-by="increaseCount" /> <MyButton v-on:increase-by="increaseCount" />
이렇게 기본적으로 데이터와 이벤트 함수를 다룰 수 있습니다.
해당 Todo App프로젝트 또한 위와 같이 방식으로 구성되어 있습니다.
또한 Vue에서는 양방향 데이터 바인딩을 지원하여,
리액트에 비해서 편리하게 form을 다룰 수 있습니다.
양방향 데이터 바인딩이란 다음과 같습니다.
데이터의 변경사항이 발생할 때 자동으로 화면에 반영되는 기능입니다. 일반적인 데이터 바인딩 방식에서는, 데이터의 변경사항이 발생하면 해당 데이터를 사용하는 화면 요소를 갱신하는 일반적인 방식을 따릅니다. 즉, 화면의 변경사항은 데이터를 업데이트할 수 있지만, 데이터의 변경사항은 화면을 갱신할 수는 없습니다. 이러한 상황에서 사용하는 것이 양방향 데이터 바인딩입니다. 양방향 데이터 바인딩은 데이터의 변경사항이 발생하면 해당 데이터를 사용하는 화면 요소를 업데이트하고, 화면 요소의 변경사항도 데이터를 업데이트하는 방식으로 작동합니다. 이를 위해 프레임워크에서는 일반적으로 v-model이라는 디렉티브를 제공합니다.
즉, 일반적으로는 "데이터 변경 -> 화면 요소 갱신" 이며,
양방향 데이터 바인딩에서는 **"데이터 변경 <-> 화면 요소 갱신" ** 으로 작용할 수 있습니다.
그래서 실제로 폼을 다룰때에도 단방향 바인딩만을 지원하는 리액트와는 달리,
더 간결하게 form데이터를 다룰 수 있습니다.
// vueJS <input type="text" v-model="newTodoItem" @keyup.enter="addTodo" placeholder="add what you have to do" /> // 리액트 <input type="text" value={todoItem} onChange={(e) => { if(엔터){ addTodo() } setTodoItem(e.target.value) }} placeholder="add what you have to do" />
위와 같이 기본적인 Vue 활용을 기반으로 TODO 앱을 구현할 수 있습니다.
모든 코드는 깃허브 링크에 있습니다.
📌 타 프레임워크와의 차이 및 DX
vs 리액트 및 특징
직전에 앵귤러를 보고와서 그런지,
체감상으로 리액트와는 유사한 점이 굉장히 많았다고 느껴졌습니다.
우선
template
문법이 기존의 html, css, js 와 크게 다르지 않아서, 리액트의 JSX
문법보다는 쉽게 접근해볼 수 있겠다고 느꼈습니다.
(하지만, 리액트에 익숙한 상태에서 vue를 보니 다양한 디렉티브가 오히려 거추장스럽다라는 생각도 듭니다.)한 동안 "리액트를 왜 써요?" 의 질문에,
"리액트가 ~ 가상 돔 ~ 빨라요"라고 답했던 시기가 있었던 것 같은데,
사실 뷰도 가상 돔을 지원하기 때문에 적절한 답변은 아닌었던 것 같습니다.
(뷰도 가상돔을 품고 있다는 뜻입니다.)
또한 리액트에서는 본인들을
UI 라이브러리
라고 칭하며,
뷰는 프로그레시브 프레임워크
라고 칭합니다.프로그레시브 프레임워크
는 일반적인 프레임워크에 비해 자유도가 높으며,
시스템과 개발자에 의해 프레임워크의 활용도를 결정할 수 있다고 합니다.그 만큼 vue는 개발자의 선택에 따라 프레임워크 적인 특성을 가질 수도 있으며, 혹은 반대로 라이브러리 적인 특성을 가질 수 있습니다.
그리고 이 둘의 차이는 코드의 형태에서 많이 드러납니다.
vue를 활용할 때에는 html, css, js 영역을 분리하여 작성하는 반면, 리액트는 JSX(JavaScript XML)를 활용하여 모든 영역을 자바스크립트로 구현합니다.
양방향 데이터 바인딩
양방향 데이터 바인딩의 지원으로,
간단한 form을 다룰 때에는 리액트보다는 공수가 덜 든다는 느낌을 받았습니다.
물론 양방향 데이터 바인딩에 대해서는 단점도 존재한다고 합니다.
우선 복잡성, 성능 저하, 디버깅 어려움 등이 존재한다고 알려져 있습니다.
하지만 해당 프로젝트를 진행하면서 저의 생각은 다음과 같습니다.
(양방향 바인딩을 폼에서 활용활 때)
- 복잡성: 복잡성은 전혀 느끼지 못했고, 오히려 간편했습니다. 양방향 바인딩으로 인해 onChange 이벤트를 신경쓰지 않아도 되어서 편했습니다.
- 성능저하: 일반적으로 onChange 이벤트에 setState를 붙이기 때문에, 리렌더링이 발생하는 것을 동일할 것으로 보입니다. 물론 나름의 최적화 방법이 있다고는 하지만, 이것이 양방향 데이터 바인딩의 큰 단점일지는 모르겠습니다.
- 디버깅 어려움: 위 두가지 단점보다는 가장 설득력있는 단점으로 생각됩니다. 리액트의 useEffect과 유사하게, 어쩌면 뷰의 변화가 사이드 effect을 불러와서 값이 변하는 과정이 디버깅의 어려움을 야기할 수 있다고는 느껴집니다.
📌 Vuejs의 개발 경험
앞서 언급한 바와 같이 많은 부분이 리액트와 유사하다고 느꼈습니다.
다만 일부 form이 다수 존재하는 프로젝트나,
혹은 단순하게 CDN과 SFC를 활용해서 가볍고 빠른 앱을 구현해야하는 상황에서는 vue를 고민해볼 수 있겠다라는 생각이 들었습니다.
물론 개발 규모에 대해 확장 가능성을 고려해야하고,
복잡성이 가늠이 안될때에는 참고자료가 많으며 트렌드에 뒤쳐지지 않는 리액트를 선택할 것 같습니다.
📌 참고자료들
Svelte kit
해당 포스팅도 vueJS와 동일한 목차로 정리해보고자 합니다.
- Svelt의 특징을 알아봅니다.
- Svelt의 기초적인 활용법을 알아봅니다.
- Svelt를 활용하여 TODO 앱을 제작해봅니다.
- 타 프레임워크와 차이점을 비교해봅니다.
📌 Svelt의 특징
Svelt는 약 1~2년 전부터 주목을 받았던 프레임워크 컴파일러입니다.
공식 홈페이지

주요 특징
- React와 Vue.js와 같은 선언적인 프레임워크와는 달리, HTML, CSS, JavaScript를 합쳐 런타임에 필요한 코드만 생성하여 작동.
- 작은 번들 사이즈를 제공하고, 이에따라 초기 렌더링 속도를 비롯한 애니메이션 및 인터랙션 처리가 용이함.
- Svelte는 React 및 Vue.js와 비교하여 문법이 단순 (React처럼 JSX 문법 등을 익히지 않아도 됩니다.)
- 런타임 중 생성되는 가상 DOM을 사용하지 않으면서도 렌더링 성능을 향상시킴
빠름
- 런타임이 아닌 빌드 타임에 애플리케이션 코드를 해석함.
- 타 프레임워크는 자체 코드도 클라이언트로 내려야 함. 하지만, 스벨트는 필요한 순수 자바스크립트만 내림.
가상 돔 X
- 가상 돔은 변경된 돔을 메모리에 유지하고 이를 실제 돔과 비교하는 것
- 리액트는 변경사항만을 반영하는 diffing 과정이 있었지만, 리액트는 skip
반응형
- 리액트는 선언적 언어이며, 100% 반응형이라고 볼 수는 없음. 변경된 값을 DOM에 반영하기 위해서는 Hook을 사용해야 함.
- 스벨트는 컴포넌트 안계가 변경되면 DOM 업데이트 진행. 단, 전에 발생한 변경 사항을 기억하고, 업데이트 명령을 받았을 때 그 변경을 한번에 반영
- 또한 반응형 선언문과 변수로 인해, 로직과 반응형 변수를 사용할 수 있음.
상태관리
- 컨택스트 API: React의 Context API와 비슷
- 스벨트 스토어: 단일 데이터 소스를 추적하고 변경사항을 감지하는데 사용
- Writable Store: 스토어와 유사, 일부 컴포넌트만 상태값을 변경가능
- Readable Store: 읽기 전용 상태값을 저장, 스토어에서 상태값을 읽기만 가능
단점
- 아직 React와 Vue.js와 같은 높은 수준의 커뮤니티가 없음.
- 일부 프레임워크에서 제공하는 기능과 라이브러리를 지원하지 않음.
📌 Svelte/kit 기초
Svelte 프로젝트는 주로 다양한 기능을 제공하는 Svelte/kit를 활용하여 진행됩니다.
Svelte/kit가 제공하는 추가적인 기능은 다음과 같습니다.
- SSR, SSG 등 지원
- 다양한 배포 옵션
라우팅
+page.svelte
- 초기 요청을 위한 SSR과 이후 네비게이션을 위한 CSR 지원
+page.js
- js, ts 파일 -> 렌더링 되기 전에 로드해야하는 데이터를 fetching
+page.svelte
와 함께 SSR 혹은 CSR에서 실행.
+page.server.js
- 데이터베이스등의 접속이 필요하여 서버에서만 작동해야만 할 때.
+error.svelte
- 가장 가까운 에러바운더리로 올라감.
+layout.svelte
- 일반적으로 네비게이트시에, 스벨트는 파일 하나를 컴포넌트 하나로 다룸.
- 페이지 이동 시 기존의 스벨트 페이지가 완전히 사라지고 새로 대체 됨.
- 푸터나 헤더같은 녀석을 유지할 수 있음.
+layout.js
, +layout.server.js
+page.svelte
와+page.js
관계와 유사.
데이터 로딩
+page.js
에서+page.svelte
가 필요한 데이터를 로드할 수 있음.
// +page.js /** @type {import('./$types').PageLoad} */ export function load({ params }) { return { post: { title: `Title for ${params.slug} goes here`, content: `Content for ${params.slug} goes here` } }; } // +page.svelte <script> /** @type {import('./$types').PageData} */ export let data; </script> <h1>{data.post.title}</h1> <div>{@html data.post.content}</div>
렌더링
- svelte/kit는 서버에서 렌더링(pre렌더링)하고 클라이언트에 HTML 형태로 내림
- 내려받은 컴포는트를 인터렉티브 하게 브라우저가 변경함 (하이드레이션)
- 그래서 컴포넌트가 서버, 클라이언트 둘 다에서 작동해야 함.
- 페이지마다 해당 세팅을 설정할 수 있음.
prerender
- 빌드 시에 HTML 파일 형태로 생성될 수 있는 페이지들.
- js, ts에서
export const prerender = true;
ssr
- SvelteKit는 하이드레이트된 클라이언트에 렌더링이 된 HTML을 보낼 수 있음.
- js, ts 에서
export const ssr = false;
csr
- 보통, SvelteKit는 server-rendered HTML을 interactive client-side-rendered (CSR) page으로 하이드레이트 함.
- 몇몇 페이지는 JS를 전혀 필요로 하지 않기도 하는데, 이 경우 CSR 옵션을 꺼버려도 돼
export const csr = false;
Props
- 기본적인 값 전달 방법
<script> import Item from './item.svelte' </script> <Item carryValue = { 12 } > <Item {carryValue} > <script> export let id </script> <p> { id } </p>
- 함수 전달
// App.svelte <script> import Item from './components/item.svelte' function alertSum(x, y) { alert(x + y) } </script> <Item {alertSum} /> // child.svelte <script> export let alertSum </script> <button on:click={(_) => alertSum(5,10)} >alert</button>
스벨트 스토어(Svelte Store)
- Writable, Readable, derived 등의 함수로 생성될 수 있음.
Writable Store
<script> import { writable } from 'svelte/store'; const name = writable('John'); function changeName() { name.set('Jane'); } </script> <button on:click={changeName}>Change Name to Jane</button>
Readable Store
<script> import { readable } from 'svelte/store'; const users = readable([ { name: 'John', age: 30 }, { name: 'Jane', age: 25 } ]); </script> {#each $users as user} <p>{user.name} ({user.age})</p> {/each}
Context
// 값을 기록하는 부분 <script> import {setContext} from 'svelte' const setValues = { a: 1, b: 2 } setContext('setKey', setValue) ... // 값을 받아쓰는 부분 <script> import { getContext } from 'svelte' const getValue = getContext('setKey') </script> a: { getValue.a } b: { getValue.b }
📌 Svelte Todo
- 완성된 Svelte Todo

Store 세팅
- 투두 타입 및 스벨트 스토어 활용
src/store/index.ts import { writable } from 'svelte/store'; export interface TodoType { id: number; text: string; completed: boolean; } // 스토어 생성 관련 // 구독 관련 작업도 다 해줌 export const todoData = writable<TodoType[]>([]);
route 세팅
- 단일 페이지이기에 파일은 하나
// src/routes/+page.svelte <script> import TodoInput from "../component/TodoInput.svelte"; import TodoList from "../component/TodoList.svelte"; import Total from "../component/Total.svelte"; </script> <section> <header>TODO</header> <TodoInput /> <TodoList /> <Total /> </section>
Store 값 활용
- $: Reactive Declaration 문법 활용
- $를 사용하지 않으면, 일반적인 자바스크립트 표현식으로 값 변화 반영 X
<script lang="ts"> import {todoData} from "../store" import Barcode from "./Barcode.svelte"; $: completedTodo = $todoData.filter(todo => todo.completed).length; $: ratio = $todoData.length > 0 ? Number(completedTodo/$todoData.length*100).toFixed(2) : 0; </script>
each 블록
<div> {#each $todoData as todo (todo.id)} <TodoItem {todo} /> {/each} </div>
- $todoData는 writable store.
- (todo.id)는 Svelte의 keyed each 블록 구문 (React JSX의 Key)
bind
- state값과 form 값이 서로 유기적으로 반영
<label> <input type="checkbox" bind:checked={completed} on:click={(e) => handleCompleteTodo(id)}/> {text} </label>
전체코드: 깃허브링크
📌 타 프레임워크와의 차이 및 DX
간편, 쉬움, 빠름
리액트에서 가장 학습이 많이 필요한 곳인 전역상태 관리가
스벨트에서는 학습할 것(?)이 없습니다.
그냥 Svelte Store와 Context만 활용하면 뚝딱입니다.
리덕스의 복잡한 보일플레이트도 신경쓸 필요가 없습니다.
앵귤려, 뷰에서 언급했던 폼 관리도 매우 쉽습니다.
bind를 활용하여 폼을 매우 쉽게 관리할 수 있습니다.
폼에 붙는 이벤트도 on: 을 활용하면 다양한 처리가 가능합니다.
그리고 요즘에는 어쩌면 필수라고 여겨지는 SSR 관련 지원까지
Svelte(Kit)가 많은 부분을 제공해주고 있습니다.
또한 성능에 관해서는 단순히 투두앱을 만들어 보았기 때문에,
획기적인 성능 개선을 경험해 봤다라고는 못하지만,
적어도 사라지는 프레임워크로 작용하며,
굉장히 적은 번들사이즈를 제공해준 것은 분명합니다.
새로운 프론트엔드 프레임워크를 접할 수록 항상 더 나은 경험을 하고 있는데,
아마도 간결함에 있어서는 최고이지 않을까 싶습니다.
그럼에도 불구하고, 스벨트가 여전히 높은 시장 점유율을 가지지 못하는 것은,
타 프레임워크에 비해 작은 커뮤니티,
아직 보장되지 않은 배포 환경에서의 안정성(?),
FE시장의 리액트 고인물...
정도가 있을 거 같습니다.
스벨트 만세...? 🥹
📌 참고자료들
SolidJS
패키지매니저
npm, yarn, pnpm
📌 서론
📚 하는 것들
- 메타데이터 처리 및 쓰기 (package.json)
- 의존성 관리 (node_modules)
- 스크립트 관리 (사용자 지정 스크립트)
- 보안 검사
- 의존성 지옥 관리
- NPM에 올라온 노드 모듈들(패키지)들이 있으며, 해당 패키지들 서로서로 의존관계를 가질 수 있음. 만약 A패키지와 B패키지 둘 다, C패키지에 의존하고 있다면 어떻게 관리해야할까
- 앱은 A와 C 패키지가 필요하며, 두 패키지는 각각 다른 버전의 B 패키지가 필요해

⇒ 앱은 어떻게 B 패키지를 설치해야할까?
📌 NPM
- 가장 기본적인 Node Package Module
- NPM V2 와 V3 가 패키지의 의존성 문제를 해결하는 방법은 달라
- NPM2 에서는 의존 관계에 있는 모듈을 해당 모듈 아래에 둠
- 이 경우, 동일한 패키지에 대한 여러 중복 폴더가 생겨버릴 수 있음
- NPM3 에서는 모든 모듈을 Flat 하게 두지만 (호이스팅), 특정 모듈에서 이미 설치된 모듈과 다른 버전이 필요하면, 그 모듈 아래에 둠 ⇒ 해당 방식에서는 “내컴되, 니컴안 “ 현상이 발생할 수 있음 (에러 찾기 어려움) ⇒ 모듈을 설치하는 순서에 따라, 의존성 구조가 달라지는 문제가 생김

📚 특징
- 패키지들을 순차적으로 설치하여 느림
- 의존성 구조에 따라 에러가 발생할 수 있음
- 디스크에 효율적이지 못한 의존성 관리 / 설치 방법
- NPM은 패키지를 찾기 위해서 계속 상위 디렉토리의 파일을 찾음
- 보안에 취약
📌 YARN Classic
공식 홈페이지
- Yet Another Resource Negotiator
- 페북 구글 등이 공동 작업
📚 특징
- NPM3의 Flat한 의존성 구조
- 오프라인 캐싱
- 심링크로 연결하는 PNPM과 달리 캐싱 파일에서 파일을 복사하여 활용
- 동일한 버전의 패키지는 한번만 설치됨
- yarn.lock에 저장된 체크섬으로 각 패키지의 무결성을 검증
- 악성 패키지 설치를 방지할 수 있음
- 네이티브 모노레포 지원
- 캐시 설치
- lock 파일
- yarn.lock 파일을 자동 생성하고 의존 모듈을 순서대로 적어둠 ⇒ 잠금 파일의 장점으로 특정 버저만 설치가 가능
- 패키지 병렬적으로 설치
📚 단점
- Flat한 의존성 트리
- 한 의존성 트리를 만드는 알고리즘 비용이 큼
- 유령 의존성 문제가 있음
*유령 의존성: 의존성과 관련하여 사용하겠다고 명시한 패키지 뿐아니라, 의존 관계에 있는 패키지도 가져다가 사용할 수 있게 되버림 ⇒ 만약에 해당 패키지가 삭제되버리면, 에러가 발생할 수 있음
📌 PNPM
- performant npm
- 공식 홈페이지
📚 특징
- yarn과 npm 보다 빠르고 디스크 효율성이 뛰어남
- npm과 yarn 의 문제는 프로젝트 전체에서 사용되는 의존성에 의해 중복되는 파일 다수 존재
- 패키지를 복사해서 사용하지 않고, .pnpm_store라는 글로벌 폴더에 패키지를 한번만 설치하고, hard link(심링크)를 사용하기 때문
- 패키지를 호이스팅하지 않아 유령 의존성 문제도 해결
- 저장 공간 덜 차지
- Content-addressable Storage
- 홈 폴더(/.pnpm-storage)의 전역 저장소에 패키지를 저장하는 중첩된 node_modules 폴더가 생성됨
- 패키지 버전 하나는 한 개의 복사본만 존재
- 해당 파일의 링크를 프로젝트의 node_modules 폴더에 추가함
- npm의 Drop-in Replacement * Drop-in Replacement: 별다른 설정없이 새로운 구성요소로 변경 가능 한 것
- NPM2와 같이 플랫한 구조가 아닌 중첩된 구조를 가짐.
- 의존성 구조에서 패키지 버전을 탐색하는 계산 과정이 생략될 수 있음.
- 패키지 설치 순서에 따른 호이스팅 문제가 발생하지 않음
- 모든 패키지의 무결성 체크를 위한 체크섬 검사
- npm2 의 의존성 구조를 가지기 때문에, 중첩된 node_modules 폴더를 생성하고, 이는 의존성이 package.json에 명시적으로 선언된 패키지만 사용할 수 있음
hardlinks and symlinks 차이?
📌 YARN BERRY
- 공식 홈페이지
📚 특징
- Plug’n’Play (PnP)
- 패키지 설치 시 발생할 수 있는 큰 IO작업을 방지할 수 있음
- package.json에 명시된 의존성 패키지만 설치할 수 있음.
- node_modules 대신, 의존성 조회 테이블 .pnp.cjs 파일 생성
- 중첩된 폴더에서 탐색하지 않고, 테이블에서 바로 조회
- Zero Install
- 모든 패키지는 .yarn/cahce/ 에 zip 파일로 저장됨
- 해당 파일에는 관련된 패키지 이름과 버전, 위치, 의존성 리스트 등이 적혀 있음
- Yarn은 Node에게 pnp.cjs를 통해 “너가 찾는 패키지 여기 있음! 이라고 알려줄 수 있음”