관리 메뉴

bright jazz music

[프레임워크 없는 프론트엔드 개발] 1.1. 순수함수를 이용한 렌더링 구현 본문

Books/프레임워크 없는 프론트엔드 개발

[프레임워크 없는 프론트엔드 개발] 1.1. 순수함수를 이용한 렌더링 구현

bright jazz music 2025. 5. 24. 00:04

https://github.com/hojuncha997/frameworkless-front-end-dev

 

GitHub - hojuncha997/frameworkless-front-end-dev: 프레임워크 없는 프론트엔드 서적 학습 리포지토리

프레임워크 없는 프론트엔드 서적 학습 리포지토리. Contribute to hojuncha997/frameworkless-front-end-dev development by creating an account on GitHub.

github.com

* css, 라이브러리 등의 파일들은 여기에 기록하지 않았다. 위 리포지토리에는 남아 있으므로 참고할 것

* 브라우저에서 file://경로/index.html 로 접근하면 CORS 정책 오류 때문에 자바스크립트 파일을 가져오지 못한다. 

  따라서 npx http-server를 사용하여 노드 서버를 구동하거나  VS Code 의 Live Server등의 확장프로그램을 사용할 것.

 

1. 개요

리액트와 같은, 프레임워크에 가까운 라이브러리의 내부 방식에 대한 이해가 부족하여 학습을 시작하였다. 이 책에서는 프레임워크를 사용하지 않으면서 프레임워크에 사용되는 방식을 구현하는 내용이 있다. 

 

2. 파일 설명

 

이 포스팅에서는 2장 '렌더링'의 일부를 기록하였다.

 

- index.html은 화면에 직접적으로 보여지는 파일이다. css 파일이 임포트 돼 있으며, index.js파일을 임포트하고 있다.

- index.js 파일은 컨트롤러 역할을 하는 파일이다. getTodos.js와 view.js 파일을 임포트하고 있다.

- getTodos.js은 fake.js 라이브러리를 사용하여 임의의 todo 리스트 배열을 만들어 반환하는 모듈이다.

- view.js은 HTML 요소와 상태를 인자로 받는다. 인자로 받은 HTML 요소를 복제한 요소를 반환한다. 이 때 복제된 요소를 가공하기 위해 상태값을 사용한다.

 

즉, index.js에서 getTodos.js 파일를 사용해 임의의 todos 배열을 만들어내고, 해당 배열을 상태 역할을 하는 객체에 포함시켜 view.js 파일 의 view 함수에 인자로 넘긴다. view 함수는 상태 객체 뿐만 아니라 HTML 요소 또한 인자로 받는데, 이는 view 함수가 인자로 받은 요소의 복사된 HTML 요소를 반환하기 때문이다. 상태값은 이 복사된 요소를 가공하는 데 사용된다.

 

3. 스키마

위에서 열거한 파일들로 생성한 스키마는 아래와 같다.

 

[브라우저 렌더링] -> [다음 렌더링 대기] -> [새 가상 노드] -> [DOM 조작] -> [브라우저 렌더링]

 

 

4. 코드

4.1. /index.html

 

<html>

<head>
    <link rel="shortcut icon" href="../favicon.ico" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.1.2/index.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.js"></script>
    
    <!-- <link rel="stylesheet" href="./base.css">
    <link rel="stylesheet" href="./index.css">
    <script src="./faker.js"></script> -->
    <title>
        Frameworkless Frontend Development: Rendering
    </title>
</head>

<body>
    <section class="todoapp">
        <header class="header">
            <h1>todos</h1>
            <input class="new-todo" placeholder="What needs to be done?" autofocus>
        </header>
        <section class="main">
            <input id="toggle-all" class="toggle-all" type="checkbox">
            <label for="toggle-all">Mark all as complete</label>
            <ul class="todo-list">
            </ul>
        </section>
        <footer class="footer">
            <span class="todo-count">1 Item Left</span>
            <ul class="filters">
                <li>
                    <a href="#/">All</a>
                </li>
                <li>
                    <a href="#/active">Active</a>
                </li>
                <li>
                    <a href="#/completed">Completed</a>
                </li>
            </ul>
            <button class="clear-completed">Clear completed</button>
        </footer>
    </section>
    <footer class="info">
        <p>Double-click to edit a todo</p>
        <p>Created by <a href="http://twitter.com/thestrazz86">Francesco Strazzullo</a></p>
        <p>Thanks to <a href="http://todomvc.com">TodoMVC</a></p>
    </footer>
    <script type="module" src="index.js"></script>
</body>

</html>

 

4.2. /index.js

// 컨트롤러 역할을 하는 파일

import getTodos from './getTodos.js'
import view from './view.js'

const state = {
  todos: getTodos(),
  currentFilter: 'All'
}

const main = document.querySelector('.todoapp')

window.requestAnimationFrame(() => {
    // 뷰 함수를 호출하여 투두 앱 요소를 반환한다.
  const newMain = view(main, state)
  // DOM에서 기존 요소를 제거하고 새 요소를 삽입한다.
  main.replaceWith(newMain)
})

/*

# innerHTML 대신 replaceWith()를 사용하는 주요 이유:

1. 성능 이점: innerHTML은 문자열을 HTML로 파싱해야 하므로 더 많은 리소스를 사용한다. replaceWith()는 이미 생성된 DOM 노드를 직접 교체하므로 더 효율적이다.
2. 이벤트 리스너 보존: innerHTML을 사용하면 모든 이벤트 리스너가 제거된다. replaceWith()는 새로운 요소에 등록된 이벤트 리스너를 그대로 유지한다.
3. 깔끔한 코드 구조: 복잡한 요소 구조를 문자열로 조합하지 않고 DOM API를 일관되게 사용할 수 있다.
4. 참조 관리: 노드 참조를 직접 다루기 때문에 코드가 더 명확하고 관리하기 쉽다.


# main = view(main, state) 처럼 할당하지 않는 이유:

1. JavaScript 변수 vs DOM: 변수 main에 새 값을 할당하는 것은 JavaScript 메모리 내의 참조만 변경하고, 실제 DOM에는 아무 영향이 없다.
2. 실제 DOM 조작 필요: DOM 요소를 실제로 교체하려면 DOM API 메서드를 사용해야 한다.
3. 불변성 원칙: 이 코드는 원본 요소를 직접 수정하지 않고 새 요소를 만들어 교체하는 패턴을 사용한다. 이는 프레임워크 없는 환경에서도 리액트와 같은 최신 프레임워크의 작동 방식을 모방하고 있다.
4. 명확한 의도: 새 변수를 생성하고 교체하는 방식은 코드의 의도를 더 명확하게 보여준다.


# window.requestAnimationFrame

1. 기본 기능
- 브라우저의 Window 인터페이스에서 제공하는 메서드이다.
- 다음 리페인트(화면 갱신) 직전에 지정된 콜백 함수를 실행. 즉, 화면 갱신 직전에 실행된다.
- 보통 초당 60회(60fps) 실행되지만, 기기 성능과 화면 주사율에 따라 달라질 수 있다.

2. 장점
- 성능 최적화: 브라우저가 적절한 타이밍에 실행하도록 스케줄링
- 배터리 효율: 백그라운드 탭이나 화면 밖의 요소는 실행 빈도가 줄어든다.
- 부드러운 애니메이션: 모니터 주사율에 맞춰 실행되어 부드러운 시각 효과를 제공한다.

3. 구분
- 자바스크립트 엔진의 메서드가 아니다.
- 브라우저가 제공하는 Web API의 일부이다.
- 브라우저의 렌더링 엔진과 연결되어 있다.

4. 작동 과정
- 1. 콜백이 특별한 큐에 등록된다.
- 2. 브라우저는 다음 리페인트 직전에 이 콜백을 실행하도록 스케줄링한다.
- 3. 자바스크립트 메인 스레드의 다른 작업이 완료된 후 실행된다.
- 4. 브라우저의 화면 주사율에 맞춰 실행된다.

5. 일반적인 사용 사례
- 애니메이션 구현
- DOM 업데이트 최적화
- 복잡한 렌더링 작업의 효율적 처리


*/

 

 

4.3. /getTodos.js

const { faker } = window

const createElement = () => ({
  text: faker.random.words(2), // 2개의 랜덤 단어를 반환한다.
  completed: faker.random.boolean() // 랜덤 불리언 값을 반환한다.
})

// 함수와 숫자를 받아서 해당 함수를 숫자만큼 반복하여 반환된 값을 배열에 추가하고 배열을 반환
const repeat = (elementFactory, number) => {
  const array = []
  for (let index = 0; index < number; index++) {
    array.push(elementFactory())
  }
  return array
}

// 익명 함수로 export. index.js 에서 임의의 이름으로 import하여 사용할 수 있다.
export default () => {
  const howMany = faker.random.number(10) // 10개 이하의 랜덤 숫자를 반환한다.
  return repeat(createElement, howMany) // 결국 {text: '랜덤 단어', completed: 랜덤 불리언} 형태의 객체를 10개 이하로 생성하여 배열로 반환한다.
}

 

4.4. view.js

// 뷰 역할을 하는 함수


// 투두 아이템 요소 생성을 위한 함수. 투두 아이템 객체를 받아서 투두 아이템 요소를 반환한다.
const getTodoElement = todo => {
    const {
      text,
      completed
    } = todo
  
    return `
    <li ${completed ? 'class="completed"' : ''}>
      <div class="view">
        <input 
          ${completed ? 'checked' : ''}
          class="toggle" 
          type="checkbox">
        <label>${text}</label>
        <button class="destroy"></button>
      </div>
      <input class="edit" value="${text}">
    </li>`
  }
  
  // 투두 아이템 개수를 반환하는 함수. 투두 아이템 배열을 받아서 투두 아이템 개수를 반환한다.
  const getTodoCount = todos => {
    const notCompleted = todos
      .filter(todo => !todo.completed)
  
    const { length } = notCompleted
    if (length === 1) {
      return '1 Item left'
    }
  
    return `${length} Items left`
  }

  
  
  // 투두 앱 요소를 반환하는 함수. 투두 앱 요소와 상태를 받아서 투두 앱 요소를 반환한다.
  // 익명함수로 export 되기 때문에 사용하는 곳에서 임의의 이름으로 사용될 수 있다. e.g. import view from './view.js'
  export default (targetElement, state) => {

    console.log('targetElement: ', targetElement)
    console.log('state:', state)
    const {
      currentFilter,
      todos // 투두 아이템 배열
    } = state
  
    // 타겟 DOM 요소를 복사한다. JS의 DOM API가 가진 메서드이다. true 인자를 넘기면 깊은 복사로, 자식까지 전부 복제한다. false는 자식 없이 해당 요소만 복제한다.
    const element = targetElement.cloneNode(true) 
    console.log('cloned element: ', element)
    // 복제된 DOM 요소의 하위 요소 중에서 클래스 이름으로 조회하여 가져온다. 텍스트만 가져오는 것이 아니라 요소 자체를 가져온다.
    const list = element.querySelector('.todo-list')
    const counter = element.querySelector('.todo-count')
    const filters = element.querySelector('.filters')
  
    // 
    list.innerHTML = todos.map(getTodoElement).join('') // 투두 아이템 배열을 순회하며 각 투두 아이템 요소를 생성하고 이를 문자열로 변환하여 결합한다. <li>...</li> 여러 개가 한 줄로 만들어진 문자열이다.
    counter.textContent = getTodoCount(todos) // (예: "1 Item left" 또는 "5 Items left"). 이 생성된 문자열이 counter 요소의 textContent 속성에 할당된다. 태그로 감싸진 내부의 문자열에만 적용된다.
  
    Array
      .from(filters.querySelectorAll('li a')) // 타겟 DOM 요소의 하위 요소 중에서 li 요소의 하위 요소 중에서 a 요소를 모두 선택한다.
      .forEach(a => { // 그리고 해당 요소이 가진 텍스트가 현재 인자로 넘어온 필터와 같은 것들에만 css 클래스를 추가한다.
        if (a.textContent === currentFilter) {
          a.classList.add('selected') // add 역시 DOM API가 가진 메서드이다. HTML요소에 css 클래스를 추가한다. 결과적으로 HTML에서 <a class="selected"> 형태로 변경된다.
        } else {
          a.classList.remove('selected') // HTML에서 <a> 형태로 변경된다.
        }
      })
  
    return element
  }

  /*
  여기서 export 하는 view 함수는 기본으로 사용되는 타겟 DOM 요소를 받는다. state의 형태는 

1. 파라미터:
- targetElement: 타겟 DOM 요소
- state: 상태 객체. 컨트롤러 역할을 하는 index.js 파일에서 전달받은 상태 객체이며 형태는 아래와 같다.

  const state = {
  todos: getTodos(),
  currentFilter: 'All'
}
다시 todos속성을 확인해보면, 이는 {text: '랜덤 단어', completed: 랜덤 불리언}의 객체형태의 요소가 10개 이하로 채워진 배열이다. 

- 이 함수에서는 인자로 받은 타겟 DOM 요소를 복사하고, 
- 인자로 넘어온 배열을 <li> 문자열로 만들어서 복사한 요소의 하위 요소에 입력한다.
- 그리고 현재 필터 상태에 따라서 필터 요소에 css 클래스를 추가하거나 제거한다.

즉, 이 함수는 실제 HTML요소와 JS로 만들어진 객체인 상태를 인자로 받아서,
기존 요소를 복제하고, 상태 값을 가공하여 HTML 요소에 입력하고 css 스타일링까지 완료하여 
실제 HTML 요소를 반환한다. 
그리고 이 함수를 호출한 곳에서 replaceWith 메서드를 사용하여 기존 요소를 반환된 요소로 교체한다.

**** 주의. ****
반환 값은 '<section class="todoapp">...</section>' 이런 형태의 문자열이 아니다.
아래와 같은 DOM 객체이다.

// 콘솔에 출력했을 때의 표현
const 실제반환값 = {
  tagName: "SECTION",
  className: "todoapp",
  childNodes: [...], // DOM 노드 배열
  innerHTML: "...",
  outerHTML: "<section class=\"todoapp\">...</section>",
  querySelector: function() { ... },
  replaceWith: function() { ... },
  // 기타 수많은 DOM 메서드와 속성들...
}

*/

 

이렇게 보이지만 실제로는 브라우저가 제공하는 특별한 객체로 JS형태를 가지며 JS로 조작이 가능하다. 여기서는 브라우저가 보기 쉽게 태그로 풀어주는 것뿐이다.

 

 

 

* 저자가 관리하는 리포지토리

https://github.com/Apress/frameworkless-front-end-development/tree/master

 

GitHub - Apress/frameworkless-front-end-development: Source code for 'Frameworkless Front-End Development' by Francesco Strazzul

Source code for 'Frameworkless Front-End Development' by Francesco Strazzullo - Apress/frameworkless-front-end-development

github.com

 

Comments