Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"semi": false,
"printWidth": 140
}
Binary file modified .yarn/install-state.gz
Binary file not shown.
20 changes: 20 additions & 0 deletions src/components/todo-Input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Component from "../core"

export default class TodoInput extends Component<HTMLDivElement> {
template(): string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 타입을 지정했군요?? 근데 string 타입인가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

template은 템플릿 리터럴을 사용하는 것이기 때문에 반환 값을 string 타입으로 한거였어요. 사실 일부러 작성한 것 보다는 기존 Component에 타입이 정의되어 있어서 자동완성할 때 저렇게 되네요!

return `
<form>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프리티어 파일 설정할 때 정렬 관련 속성을 넣으면 읽기 편할 것 같아요ㅠㅠ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다!

<input type="text" />
<button>Submit</button>
</form>
`
}

setEvent(): void {
this.addEvent("submit", "form", (event) => {
event.preventDefault()
const inputElem = this.target.querySelector("input") as HTMLInputElement
this.props.addTodo(inputElem.value)
})
}
}
46 changes: 46 additions & 0 deletions src/components/todo-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from "../core"
import { Todo } from "../types"

interface TodoListProps {
todos: Todo[]
deleteTodo: (todoId: number) => void
toggleActive: (todoId: number) => void
}

export default class TodoList extends Component<HTMLUListElement, TodoListProps> {
template(): string {
const { todos } = this.props!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! 뭔가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 Non-null 단언 연산자 라고 해서 해당 값이 undefinednull이 아니라는 뜻입니다

return `
${todos
.map(
(todo) => `
<li data-id=${todo.id}>
${todo.title}
<button type="button" id="toggle-button" style="color: ${todo.isActive ? "#09F" : "#F09"}">
${todo.isActive ? "비활성화" : "활성화"}
</button>
<button type="button" id="delete-button">삭제</button>
</li>
`
)
.join("")}
`
}

getTodoId(element: HTMLElement): number {
const todoId = Number((element.closest("[data-id]") as HTMLLIElement)!.dataset.id)
return todoId
}

setEvent(): void {
this.addEvent("click", "#toggle-button", (event) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아티클엔 클래스로 지정해썼는데 아이디로 바꾼 이유가 있을까요?

Copy link
Contributor Author

@D5ng D5ng Dec 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스는 어디서든 사용할 수 있도록 하는 목표가 있지만, 여기에선 굳이? 싶어서 아이디로 사용했습니다. 그래도 확장성을 생각한다면 클래스로 가는것이 더 맞겠네요

const todoId = this.getTodoId(event.target as HTMLElement)
this.props!.toggleActive(todoId)
})

this.addEvent("click", "#delete-button", (event) => {
const todoId = this.getTodoId(event.target as HTMLElement)
this.props!.deleteTodo(todoId)
})
}
}
67 changes: 67 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
type ComponentInternalRecord = Record<string, any>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 아래 interface에 바로 선언하지 않고 타입을 선언해서 사용한 이유가 뭔가요?

동현님의 인터페이스와 타입을 사용하는 기준이 궁금합니다

Copy link
Contributor Author

@D5ng D5ng Dec 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인터페이스를 사용하지 않은 이유는 확장을 할 필요가 없기 때문이였어요. 만약 인터페이스를 사용한다면 아래처럼 작성하게 되는데요.

interface ComponentInternalRecord extends Record<string,any>{}

이미 Record<string, any>이기 때문에 확장을 하는게 의미가 없다고 생각했어요. 따라서 type을 사용하는게 더 명확하지 않을까? 라는 생각을 했습니다


interface ComponentOptions<TElement = HTMLElement, Props = any, State extends ComponentInternalRecord = ComponentInternalRecord> {
target: TElement
props?: Props
state?: State
}

export default abstract class Component<
TElement extends HTMLElement,
Props = any,
State extends ComponentInternalRecord = ComponentInternalRecord
> {
target: TElement
props?: Props
private _state?: State
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불변성을 신경쓴 부분인거죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불변성이라기 보다, 외부에서 값을 접근할 수 없도록 하기 위함입니다!


constructor({ target, props, state = {} as State }: ComponentOptions<TElement, Props, State>) {
this.target = target
this.props = props
this._state = state

this.setup()
this.render()
this.setEvent()
}

setup() {}

get state() {
return { ...this._state! }
}

set state(newState: State) {
if (this._state) {
this._state = { ...this._state, ...newState }
this.render()
}
}

setState(newState: State) {
if (this._state) {
this._state = { ...this._state, ...newState }
this.render()
}
}

abstract template(): string

render() {
this.target.innerHTML = this.template()
this.componentDidMount()
}

componentDidMount() {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메서드 첨 알았어요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의미에 맞는진 아직 잘 모르겠네요,, 더 공부를 해봐야 정확하게 판단이 내려질것 같습니다


addEvent(eventType: keyof HTMLElementEventMap, selector: keyof HTMLElementTagNameMap | string, callback: EventListener) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿

this.target.addEventListener(eventType, (event) => {
const target = event.target as HTMLElement
if (target.closest(selector)) {
callback(event)
}
})
}

setEvent() {}
}
73 changes: 72 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
// your Code
import Component from "./core"

import TodoInput from "./components/todo-Input"
import TodoList from "./components/todo-list"
import { Todo } from "./types"

interface TodoState {
todos: Todo[]
}

class App extends Component<HTMLDivElement, {}, TodoState> {
setup() {
this.state = {
todos: [],
}
}

componentDidMount(): void {
const todoInputElem = this.target.querySelector("#todo-input") as HTMLDivElement
const todoListElem = this.target.querySelector("#todo-list") as HTMLUListElement

new TodoInput({
target: todoInputElem,
props: {
addTodo: this.addTodo.bind(this),
},
})

new TodoList({
target: todoListElem,
props: {
todos: this.state!.todos,
deleteTodo: this.deleteTodo.bind(this),
toggleActive: this.toggleTodoActive.bind(this),
},
})
}

template(): string {
return `
<main>
<div id="todo-input"></div>
<ul id="todo-list"></ul>
</main>
`
}

addTodo(title: string) {
const newTodo = {
id: Date.now(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 신박합니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 이거는 굉장히 안좋습니다 ㅋㅋㅋㅋㅋㅋ 왜냐하면 id는 유일해야하는데 겹칠 수 있어요. 이런 것도 더 고민을 해봐야겠네요

title,
isActive: false,
} as Todo
this.setState({ ...this.state, todos: [...this.state!.todos, { ...newTodo }] })
}

deleteTodo(todoId: number) {
const filteredTodo = this.state!.todos.filter((todo) => todo.id !== todoId)
this.setState({ todos: filteredTodo })
}

toggleTodoActive(todoId: number) {
const updateTodo = this.state!.todos.map((todo) => (todo.id === todoId ? { ...todo, isActive: !todo.isActive } : todo))
this.setState({ todos: updateTodo })
}
}

const rootElement = document.getElementById("app") as HTMLDivElement

if (rootElement) {
new App({ target: rootElement })
}
5 changes: 5 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Todo {
id: number
title: string
isActive: boolean
}
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Loading