본문 바로가기

개발/JavaScript

[Javascript] The Basic Vanilla JavaScript Project Setup - 기본 Vanilla JS 프로젝트 세팅 방법 (번역)

다음 Vanila JS 프로젝트를 시작하는 방법

지난 기사에서는 10년 이상 Javascript frameworks를 사용해오던 내가, 왜 vanilla JS를 완전히 다시 사용하는 이유에 대해 공유를 했습니다. 오늘은 기본 프로젝트 세팅에 대해 알아볼것입니다. 그 과정에서, 저는 다양한 테크닉을 사용할것이고 그것에 대한 근거들을 논의할 것입니다. 그러나 모든 것에대해 디테일하게 논의하지는 않을 것입니다. 몇몇의 것들은 다음 기사에서 설명할 것입니다.

 

여기에서 논의된 내용 중 많은 부분은 예를 제공한 존경하는 동료  Vladimir Spirin 덕분입니다 이 기사는 그의 기술에 대한 나의 해석과 내가 추가 및 제거한 일부 내용을 혼합한 것입니다.

 

Vlad가 코드를 보여주지 않고 접근 방식을 처음 설명했을 때를 기억합니다. 저는 말할 것도 없이 회의적이었습니다. 저는 그것이 가능하다는 것을 믿지 않았습니다. 그리고 이 기사를 읽으면서 당신도 회의적일 것이라고 확신합니다. 1-2회 이것을 시도했을 때와 같은 회의적인 시선들이 사라지기를 바랍니다. 그리고 모든 것이 얼마나 간단할 수 있는지 깨닫고 나면 여러분도 저처럼 행복하기를 바랍니다. 이 모든 것이 실제로 동작합니다. 약속합니다. 일부 프로덕션 코드에 이미 성공적으로 적용되었습니다. (나머지는 시간이 나는 대로 바꿀 예정입니다).

 

시작하기 앞서

vanilla JS를 효과적으로 사용하기 위해 알아야 할 가장 중요한 한 가지를 꼽으라면 다음과 같습니다.

 

플랫폼의 이점을 활용하는 것

 

이것은 우리가 Coin Metrics 의 프론트엔드 엔지니어링 팀에서 (시도하는) Vanilla Redux Manifesto 에서 나온 것 입니다. (이것은 다른 것이 아니라 redux 입니다.) 간단히 말해서, 플랫폼이 당신을 위해 할 수 있는 것이 있다면, 그것이 하는 일을 이해하고 그것을 최대한 활용하십시오. 플랫폼이 작동하는 방식이나 모양  마음에 들지 않는다고 해서 플랫폼을 재발명하지 마십시오 .

 

파일들

이 설정을 위해 빌드 도구가 필요하지 않습니다. index.html 파일을 열고 작업을 시작할 수 있습니다. 이것은 상당히 쉬워 보이지만, vanilla JS를 이제 막 시작하는 사람들(또는 저처럼 과거에 몇 년 동안 이 작업을 수행했지만 완전히 손을 놓은 사람들)에게는 분명하지 않은 몇 가지 세부 사항을 여기에서 다룰 것입니다.

필요한 최소한의 파일은 index.html, index.js및 index.css 입니다. (CSS 및 JavaScript 파일의 이름은 원하는 대로 지정할 수 있지만 일반적으로 HTML 파일의 이름은 index.html로 지정합니다, 왜냐하면 당신이 앱 URL의 루트 경로로 이동할 때,  HTTP 서버가 찾는 것이 index.html이기 때문입니다.)

 

일부 개발자는 코드에서 변경된 사항을 기반으로 애플리케이션의 일부를 자동으로 다시 로드하는 웹 서버를 갖고 싶어합니다. 저는 이것이 신뢰할 수 없었고 이러한 도구를 사용하는 비용이 비교적 높다는 것을 알게 되었습니다. (예를 들어, 브라우저에서 파일을 여는 데 추가 도구가 필요하지 않습니다). 많은 코드 에디터들은 바닐라 HTML/JavaScript/CSS로 작업하는 경우, 유사한 기능들을 무료로 사용할 수 있도록 허용합니다.

 

vanilla JS 접근 방식의 기본 주제는 성능이나 번들 크기 감소가 아닙니다. 사람들은 React의 성능에 만족하므로 성능은 분명히 그다지 바람직한 특성이 아닙니다. 우리의 목표는 우리 애플리케이션에 굳이 필요하지 않은 복잡성을 도입하지 않는 것 입니다. 이것은 프로젝트 주변의 모든 도구로 시작하여 코드로 계속됩니다.

 

CSS 추가하기

스타일 시트는 <head> 태그 내부에서 <link>태그를 사용하여 HTML 파일에 연결됩니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My app</title>
    <link rel="stylesheet" href="index.css">
  </head>
  <body>
  </body>
</html>

 

나는 assets들을 참조하기 위해 스타일시트 내에서 상대경로를 사용합니다. 뿐만 아니라 스타일시트를 참조하기 위해서도 상대 경로를 사용합니다. 이렇게 하면 변경 사항을 미리 보기 위해 웹 서버가 필요하지 않습니다.

 

깊이 중첩된 폴더 구조로 인해 상대 경로를 사용할 수 없는 경우 폴더 구조를 지정 하고 복잡한 구조를 유지하기 쉽도록 도구를 추가하지 마십시오. 복잡성을 제거하는 것은 항상 더 저렴합니다.

 

Javascript 추가하기

JavaScript를 HTML에 연결하기 위해 우리 대부분은 바로 이렇게 생각합니다. “나 어떻게 하는지 알아! <body>태그 밑에 <script>태그를 붙이면 되잖아!” 그리고 이 말은 맞습니다. 그러나 더 알아야 할것은, 페이지에 JavaScript를 추가하는 네 가지 다른 방법이 더 존재하며, 모두 용도가 있다는 사실입니다.

 

첫번째로 가장 확실한 방법은 다음과 같습니다.

 

<script type="application/javascript">
  console.log('Hello, world!')
</script>

 

<script>태그는 <head>내에서도 포함될 수 있고, <body>의 모든 부분에서 작성될 수 있습니다. 어디에 배치하든 앞에 오는 모든 요소에 접근할 수 있습니다. 많은 사람들이 이것을 작은 토이 프로젝트에 사용되는 패턴이라고 무시하지만, 코드가 많지 않다면 유용한 패턴입니다. 항상 많은 양의 코드를 작성할 필요는 없고, 코드가 많지 않은데도 굳이 별도의 파일을 생성하는 이유는 무엇일까요? 향후 기사에서 더 큰 응용 프로그램을 분해할 때 다시 다루겠습니다.

 

두번째로 앞에서 언급했듯이 사람들이 JavaScript를 추가하는 일반적인 방법은 다음과 같습니다.

 

<body>
  ....
  <script src="index.js">
</body>

 

이렇게 하는 이유는 코드가 읽히기 전에 페이지 콘텐츠를 존재하도록 하기 위함입니다. 그러나 이것은 스크립트가 로드될 때까지 페이지의 로딩을 연기하고 여러 스크립트가 병렬로 로드되거나 비슷하게 로드되는 것을 허용하지 않기 떄문에 현재 가장 선호하지 않는 방법입니다. 나중에 선호하는 방법에 대해 얘기할 것입니다.

 

세 번째 방법은 <head<내에 <script>태그를 추가하는 겁니다.

 

<head>
  <script src="index.js">
</head>

 

위 방식대로 하면, 스크립트가 <body>요소에 즉시 액세스할 수 없습니다. 반면에 브라우저가 페이지 내용을 파싱하기 전이나 레이아웃을 그리기 전에 실행할 수 있습니다. 또한 <head>와 그 안에 있는 모든 선행 요소에 접근할 수 있습니다,

새 요소를 추가할 수도 있습니다. 스크립트가 완전히 로드될 때까지 페이지의 추가 구문 분석은 중단 됩니다. (즉, 자바스크립트 내부의 모든 코드가 실행됨을 의미함). 이것은 작업자를 등록하거나 동적으로 추가하는 것과 같이 페이지가 표시되기 전에 일부 초기화를 수행하려는 경우 사용할 수 있는 패턴입니다. 그러나 이 방식을 너무 좋아하지 마십시오. 이것은 코드베이스의 복잡성을 증가시킵니다. 다만 초기화를 해야할 때 사용하기 좋은 코드입니다.

 

스크립트 태그는 async, defer 두 가지 속성 중 하나를 가집니다. 앞의 두 가지 방법과 달리, JavaScript 코드가 로드되는 동안 페이지의 파싱 및 그리기를 연기 하지 않으며 이러한 속성이 표시된 모든 태그가 병렬로 로드됩니다. async및 defer 스크립트의 페이지를 연기하지않는 특성으로 인해 일반적으로 페이지 아래에 추가하는 것이 이점이 없기 때문에 일반적으로 <head> 에 추가합니다.

 

async 스크립트는 별도의 스레드에 로드되며 로드되면 페이지가 이미 로드되었는지 여부에 관계없이 즉시 읽혀집니다..

또한 페이지의 다른 스크립트와 읽혀지는 순서에 대한 약속이 없으므로, 이러한 의미에서 스크립트가 둘 이상 있는 경우 스크립트를 평가하는 데 가장 신뢰할 수 없는 방법입니다. 또한 로드 할 때, 페이지 내용이 모두 로딩되는지에 대한 확신이 없기 때문에 DOMContentLoaded 페이지 요소로 작업하려면 이벤트를 기다려야 합니다. 저는 아직 async 스크립트에 대한 좋은 사용 사례를 찾지 못했지만 어떤 경우에는 유용할 것 같은 고유한 특성을 가지고 있습니다.

 

defer 태그는 현재까지 일반적으로 가장 유용한 태그입니다. 그것은 async 태그와 유사하게 작동하지만,  페이지가 파싱 될 때까지 로딩이 연기된다는 데에 주목할만한 차이점이 있습니다. 이는 페이지의 파싱을 멈추지 않는다는 점을 제외하고 script 태그를 바디 맨 아래에 배치하는 것과 사실상 동일 합니다. 병렬로 로드되지만 페이지에 그려주는 것과 동일한 순서로 순차적으로 실행되기 때문에 <body>모두 속성을 사용하는 페이지에 여러 스크립트 태그가 있는 경우 특히 유용합니다 . 이로 인해 스크립트 태그를 맨 아래에 배치하는 <body>것은 거의 쓸모가 없습니다.

 

Javascript 로딩에 대해 중간 정리

더보기

1. 직접 명시

<script>
	console.log("핼로월드");
</script>

 코드가 길지 않다면 사용될 법한 방법.

 

2. body안에 스크립트 태그 사용

<body>
  ....
  <script src="index.js">
</body>

body에서 스크립트 태그를 만나면 스크립트가 다 로드될 때까지 페이지 로딩을 멈춘다. 한번에 한개의 스크립트만 읽는다.

이것의 문제점은 스크립트 아래에 있는 DOM 요소에 접근 할 수 없다는 것이다. 또한, 만약 위쪽에 큰 스크립트가 있는경우, 스크립트가 로드될 때까지 페이지의 로딩을 연기하기 때문에 스크립트 아래쪽의 페이지가 로딩되지 않는다. 

그렇다면 위의 예시 코드처럼 맨 마지막에 script 태그를 둔다면?

HTML 문서 자체가 아주 큰 경우에는 HTML 전체를 로드 한 뒤 스크립트를 다운받게 되므로 페이지가 느려진다.

 

3. head안에 스크립트 태그 사용

<head>
  <script src="index.js">
</head>

스크립트가 body 요소에 즉시 접근이 불가능하다. 그러나 브라우저가 페이지 내용을 파싱하기 전이나 레이아웃을 그리기 전에 실행할 수 있다.

따라서 초기화를 수행할 경우에만 사용하기를 권장한다.

 

4. defer/async 사용

- async 

<script async src="./index.js"></script>

 페이지와 완전히 독립적으로 동작한다.

브라우저가 백그라운드에서 스크립트들을 다운로드 하고 동시에 페이지 파싱을 진행한다. 페이지 파싱과 스크립트 파싱 둘 중에 어느것이 기다리지 않고 바로 진행한다. 

그러므로 순서가 제각각이고 페이지 내용이 모두 로딩되었는지에 대한 확신이 없다.

 

- defer

<script defer src="./index.js"></script>

 가장 유용하다.

브라우저가 백그라운드에서 스크립트들을 다운로드하고 동시에 페이지 파싱을 진행하는것 까지는 async와 똑같다. 그러나 defer는 페이지를 먼저 읽은 후 다운로드 된 스크립트들을 읽는다. 

그러므로 페이지 내용이 모두 로딩 되었는지에 대한 확신이 있다. 

 

그렇다면 동적 스크립트를 생성하면?

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// async=false이기 때문에 long.js가 먼저 실행됩니다.
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

async 속성에 false를 추가하자. 그러면 문서에 추가된 순서대로 실행이 된다.

 

DOM 노드 생성

Vanilla JS를 사용하는 개발자가 두려워하는 것 중 하나는 DOM 노드 생성입니다. 실제로 DOM 노드 생성은 매우 장황한 코드를 생성하는데, 이는 최고는 아닐지라도 적어도 훨씬 더 우수합니다. 이를 더 쉽게 만드는 여러 가지 방법이 있습니다. 많은 프레임워크들은 DOM 생성 코드를 포함하고 있으며 이런 노력을 코드화하기 위한 문법을 명시합니다. JSX가 한 가지 주목할만한 예입니다. 각 기술에는 잃고 얻는것이 있다.

 

그렇다면 DOM 노드를 생성하기 위해 친숙한 HTML 구문만 사용하고, 다른 도구와 코드가 필요하지 않다면, HTML 구문만 사용하고 싶을겁니다. 친숙한 HTML 구문만 사용하는 것은 기본적으로 추가 비용이 들지 않습니다. 그래서 이 기술을 사용하는 방법은 HTML 파일에 HTML을 작성하는 것 입니다. 즉, JavaScript에서 처음부터 노드를 생성하지 않는것입니다.

 

우리가 하고자 하는 것의 본질은 HTML 파일에 있는 필요한 만큼의 DOM 노드를 생성하고, 표시되어서는 안 되는 노드들을 즉시 숨기는 것입니다. index.html 파일을 앱에 필요한 모든 UI 요소의 저장소로 생각할 수 있습니다 .

 

나중에 더 필요한 수 있는 몇 가지의 것들에 대해 자세히 설명하겠지만, HTML 파일에는 필요하지 않은 몇 가지가 있습니다.

  • 텍스트 내용만 변경되는 요소
  • 클래스만 변경되는 요소

즉, 요소의 모든 변경사항들이 필요하지 않습니다. 새 요소를 추가해야 하는 것들만 필요합니다. 종종 그것이 완전히 가능하지 않을 수도 있지만, 그 케이스에 대해서는 나중에 알게 될 것입니다.

 

다음은 두 가지 예입니다.

 

사용자가 어떤 '페이지'에 있는지에 따라 보이고 숨기기:

<div id="view-loading">Loading...</div>
<div id="view-hello" class="hidden">Hello, world</div>

 

에러 메세지:

 

<p class="form-field">
  <label>Name: <input id="name-input" type="text"></label>
  <span class="error hidden">Name is required.</span>
  <span class="error hidden">
    Name cannot be more than 15 characters long
  </span>
</p>

 

클래스에는 처음에 필요하지 않은 요소를 숨기는 hidden과 같은 규칙이 포함되어 있다고 가정합니다 .(display: none;)

 

JavaScript를 통해 액세스해야 하는 모든 태그에는 고유한 id속성이 부여됩니다. 많은 태그가 있을 수 있으므로 태그에 대한 좋은 이름을 지정하는 것은 매우 중요합니다. 많다고 미루지 마십시오. 심지어 많다고 하더라도, 당신은 언제나   document.querySelectorAll('[id]')를 사용해 일치하는 모든 요소를 ​​실행하고 개발자 콘솔에 작성할 수 있습니다. 그리고 복사/ 붙여넣기를 할 수 있는 모든 매칭되는 요소들을 자바스크립트 문자열로 변환할 수 있습니다.. 예를들어, 

 

let s = ''
document.querySelectorAll('[id]').forEach(el => {
  let {id} = el
  let name = id.replace(/-(.)/g, (_, c) => c.toUpperCase())
  s += `const $${name} = document.getElementById('${id}')\n`
})
console.log(s)

 

Javascript 코드의 조직화

많은 경우 대부분의 애플리케이션 코드는 단일 파일에 저장됩니다.

 

저는 개발자들이 서로 다른 포인트를 가지고 있음을 보았고, 저는 누군가가 파일을 구성하는 다양한 방법과 그에 따르는 정교한 근거에 대해 몇 년이 지나도 그들은 불신을 갖고있는 이유를 압니다. 터무니없게 들리겠죠.

 

스스로에게 물어봐야 하는 첫 번째 질문은 다음과 같습니다. 먼저 파일을 모듈로 분할하는 이유는 무엇입니까?

 

코드 구성은 가장 큰 이유 중 하나입니다. (모듈이 존재하게 된 이유는 아니지만). 코드를 구성한다는 것은 일반적으로 코드베이스의 논리적 분리를 의미합니다. 이 접근 방식의 목표는 분리되고 자체 포함된 빌딩 블록을 만들고 구성하여 응용 프로그램을 빌드하는 것이지만 특정 목적보다는 단순히 코딩 스타일로 실행되는 경우가 많습니다.

 

코드를 빌딩 블록으로 분리 하는 방법은 파일/폴더 또는 모듈을 기준으로 할 필요가 없습니다. 파일과 모듈은 사람들이 이 문제를 해결한 많은 방법 중 일부일 뿐입니다.

 

코드 구성을 용이하게 하기 위해 파일 및 폴더 구조를 만들 때, 일반적으로 모듈 시스템을 사용하게 됩니다. 이는 물리적 파일 및 폴더뿐만 아니라 다른 파일 내의 내용을 참조하는 방법 및 도구에 관한 것입니다. 같은 문제를 해결하기 위해 다른 방법을 사용하지 않은 이유를 보면 논쟁이 다소 모호하거나 비논리적이며 결국 실제 장단점 분석보다 개인 스타일과 습관에 더 가깝습니다.

 

논리적/의미론적 코드 분리를 위해 파일과 폴더를 절대적으로 사용해야 하는 이유는 단 한 가지도 없습니다. 하지만 다음과 같은 몇 가지 이유를 생각할 수 있습니다.

 

  • 명명 체계를 만들어야 합니다. 그것만으로도 가장 어려운 프로그래밍 과제 중 하나입니다. 게다가 다른 규칙과 마찬가지로 할 수 있는 것과 할 수 없는 것을 제한하고 규칙을 위반하거나 규칙을 수정하고 파일을 이동해야 하기 때문에 작업을 피하는 상황으로 이어질 수 있습니다. 파일을 이동하는 것은 종종 같은 파일 내에서 코드를 이동하는 것보다 더 힘이 드므로 피하고 싶은 동기를 부여합니다.
  • 모든 파일을 단일 번들로 수집하려면 빌드 도구가 필요합니다. 빌드 도구는 무료로 제공되지 않습니다. 개발 시간이 추가되고 CI 및 배포 중에 특별한 설정이 필요하며 수천 개의 종속성이 있기 때문에 정기적인 감사가 필요한 경우가 많습니다.
  • 리팩토링을 수행하고 프로젝트 내에서 항목을 검색하려면 더 복잡한 도구가 필요합니다. 이것은 DX에 가깝고 어떤 방법이 위의 두 가지 문제를 처리했다면 이 문제로 살 수 있습니다. 하지만 이 문제를 무료로 피할 수 있다면 분명히 불평하지 않을 것입니다.

모듈의 또 다른 인기 있는 이유는 코드 재사용입니다. 이것은 실제로 저의 프로젝트에서 사용하는 것입니다. 필요에 따라 동일한 기능을 공유하는 여러 앱이 있거나 서버와 클라이언트 간에 공유 기능이 필요한 전체 스택 프로젝트를 작성 중일 수 있습니다. 하지만 제가 추구하는 기능을 공유하는 것이라면 많은 파일이 필요하지 않으며 나중에 코드를 공유해야 할 수도 있다는 이유로 미리 많은 파일을 만들 필요도 없습니다. 제 코드를 모듈로 나누는 방식은 Webpack이 공통 chunk 기능으로 수행하는 것과 매우 유사합니다. 실제 사용 패턴에 따라 코드를 그룹화합니다. 간단히 말해서 코드 재사용에는 정교한 파일 기반 구성이 필요하지 않습니다.

 

나는 또한 새로운 1줄 또는 10줄 모듈을 생성하는 것을 피할 수만 있다면 여기 저기에 함수 한두 개를 복제해도 괜찮습니다. (네, 저는 Javascript 커뮤니티에서 이것이 인기있는걸 잘 알고있습니다. 그러나 인기있는게 좋은 것을 의미하진 않습니다.) 그리고 때때로 코드 재사용은 그 자체로 복잡성을 야기한다는 것을 잊지 마십시오. 재사용 가능한 모듈을 기본값으로 설정하는 것은 선행 최적화의 한 형태입니다.

 

단일 스크립트 패턴이 모든 프로젝트에 반드시 적용되는 규칙은 아니며 성공적인 프로젝트의 요구 사항도 아니지만 제 생각에는 이것이 가장 좋은 출발점입니다. 의미가 있는 한 계속 사용하면 일반적으로 유지되는 신념과 달리 전반적으로 코드가 더 단순해집니다.

 

에디터 툴

하나의 큰 파일을 관리할 수 있는지 여부가 걱정된다면 걱정하지 마십시오. 대부분의 편집기는 여러 파일보다 단일 파일에서 더 잘 작동합니다.

 

단일(메인) 파일 설정 때문에 저는 편집기를 평소의 과도하게… 음… 완전히 모듈화된 코드 베이스와 약간 다른 방식으로 사용하는 경향이 있습니다.

 

코드 폴딩이 필수 불가결하다고 생각합니다. 때때로 나는 모든 것을 접고 스크롤하여 순서를 조정하거나 관심 있는 것을 찾습니다. 보다 유능한 편집자는 코드를 확장하지 않고 축소된 섹션을 선택할 수 있도록 하여 파일 재구성을 더 쉽게 만듭니다.

JetBrains에서 코드를 폴딩한 모습

 

대부분의 에디터는 함수 이름만 별도의 패널에 나열되는 개요 보기도 제공합니다. 나는 주어진 시간에 하나 또는 두 개의 파일만 작업하고 시작하기 때문에 많은 파일 탐색이 필요하지 않습니다. 그래서 일반적인 파일 탐색기 대신 왼쪽에 개요 보기를 유지합니다.

 

 

작업해야 하는 모든 파일이 하나 또는 두 개일 때 이러한 도구와 기타 도구도 훨씬 더 유용해집니다. 예를 들어, 단일 파일 패턴을 사용할 때 JetBrains IDE에서 사용하지 않는 기능의 감지가 훨씬 더 안정적입니다. 리팩토링 도구는 더 안정적이며 고급 형태의 코드 인텔리전스를 지원하지 않는 편집기에서도 작동합니다. 핀치에서 간단한 검색 교체로 작업을 완료할 수 있습니다.

 

어떤 사람들은 주석을 사용하여 코드를 구분하고 검색을 사용하여 코드로 이동하는 것을 좋아합니다. 주요 기능 그룹 내에 하위 그룹이 있는 경우에만 이 작업을 수행합니다.

 

편집기가 단일 파일 코드를 처리하는 방법을 알고 있는지 여부는 코드 품질 과 전혀 관련이 없으며 그에 대한 표시도 아닙니다. 작업을 더 쉽게 만들 수 있는 방법이 있다는 점을 지적한 것뿐입니다.

 

스타터 코드 레이아웃

이제 일반적으로 파일 내에 있는 논리적 섹션에 대해 이야기해 보겠습니다. 나는 이것을 '스타터' 코드 레이아웃이라고 부릅니다. 왜냐하면 최종 레이아웃이 무엇인지에 관계없이 항상 이런 식으로 시작하기 때문입니다.

 

내 응용 프로그램에서 JavaScript 코드의 일반 레이아웃은 처음에 다음과 같습니다.

 

  • Constants, helpers // 상수, 도우미
  • Application state // 에플리케이션 상태
  • State accessor functions (getters and setters) // 상태 접근자 함수
  • DOM node references // DOM 노드 참조
  • DOM update functions // DOM 업데이트 기능
  • Event handlers // 이벤트 핸들러
  • Event handler bindings // 이벤트 바인딩
  • Initial setup // 초기 설정

 

상수와 상태는 맨 위에 정의됩니다.

 

const TODAY = Date.now()
const LOADING = 0, READY = 1, ERROR = 2
let state = { ... }

 

예, 모두 모듈 범위 변수입니다. 이것에 대해서는 나중에 다루겠습니다. 애플리케이션 데이터(또는 상태)는 하나 이상의 변수로 정의될 수 있습니다. 너무 많든 적든 간에 얼마나 많은 변수를 가져야 하는지에 대한 규칙은 없습니다. 앱에 따라 다릅니다.

 

응용 프로그램 데이터를 보유하는 변수 바로 아래에는 일반적으로 데이터 작업을 위한 함수가 있습니다. 이것은 권장 사항이 아닙니다. 그러나 저는 이렇게 하기를 좋아합니다.

 

let state = { ... }
let setLoading = () => state.view = LOADING
let setReady = () => state.view = READY
let isLoading = () => state.view === LOADING
let isReady = () => state.view === READY

일반적으로 다양한 상태에 대한 getter 및 setter 기능이 필요합니다. 코드 기반이 커짐에 따라 전용 접근자 기능이 있는 경우 애플리케이션을 추가하거나 수정하는 것이 더 쉽다는 것을 알게 되었습니다. 처음부터가 아니라 필요할 때 접근자를 정의합니다. 나는 또한 그것들이 더 이상 필요하지 않게 되는 즉시 그것들을 제거할 것을 강조합니다. 어떤 사람들은 접근자를 정의하지 않고 애플리케이션 전체에서 상태를 조작하는 것을 선호하는데, 그것도 잘 작동합니다. 

 

데이터를 가져오고 백엔드 서비스로 데이터를 보내는 함수를 접근자 그룹의 일부로 취급합니다.

let isLoading = () => state.view === LOADING
let isReady = () => state.view === READY
let loadSongs = () => fetch('/api/songs/')
  .then(res => res.json())
  .then(data => {
    state.songs = data.songs
    state.view = READY
  })
  .catch(() => state.view = ERROR)

이것을 별도의 그룹으로 묶을 수 있습니다. 저는 공통 주제에 따라 접근자를 그룹화하므로 비동기 함수는 적절한 곳에 배치합니다.

 

데이터 관련 함수 뒤에는 DOM 접근 및 조작과 관련된 모든 것을 배치합니다.

DOM 노드 참조는 구체적인 DOM 노드 또는 DOM 노드 그룹을 가리키는 최상위 변수입니다. 내가 이야기한 다른 Vanila JS 개발자와 마찬가지로 DOM 값과 비DOM 값을 구별하기 위해 이러한 변수에 대한 특별한 명명 체계가 도움이 된다는 것을 알게 되었습니다.

 

let D = document
let $play = D.getElementById('play')
let $stop = D.getElementById('stop')
let $viewLoading = D.getElementById('view-loading')
let $viewReady = D.getElementById('view-ready')
let $$instruments = D.querySelectorAll('.instrument-option')

나는 또한 과거에 다음과 같은 다른 패턴을 사용했습니다.

let $refs = {}
document.querySelectorAll('[id]').forEach($el => {
  let key = $el.id.replace(/-(.)/g, (_, s) => s.toUpperCase())
  $refs[key] = $el
})

다음과 같은 패턴도 보았습니다.

 

let REFS = {
  playback: {
    play: document.getElementById('play'),
    stop: document.getElementById('stop'),
  },
  views: {
    [LOADING]: document.getElementById('play'),
    [READY]: document.getElementById('loading'),
  },
}

나는 첫 번째 패턴을 사용하는데, 그 이유는 단순한 변수 목록을 갖는 것이 가장 유연하다는 것을 알았기 때문입니다(예: 변수를 이동하면 계층 구조에서 어디로 이동하는지 걱정할 필요가 없음). 그러나 각각의 방법에는 장단점이 있습니다.

 

다음 그룹은 DOM 업데이트 기능입니다. 이러한 업데이트는 조작 유형에 따라 선언적으로 또는 명령적으로 DOM 노드를 업데이트합니다. 나는 미래의 기사에서 그것에 대해 더 자세히 쓸 것입니다. 다음은 몇 가지 예를 들겠습니다.

let updateView = () => {
  $viewLoading.classList.toggle('hidden', !isLoading())
  $viewReady.classList.toggle('hidden', !isReady())
  $viewError.classList.toggle('hidden', !isError())
}
let updateSongDetails = (songData, index) => {
  let $song = $$songs[index]
  $song.querySelector('.active')
    .classList.toggle('hidden', !isSelected(index))
  $song.querySelector('.title').textContent = songData.title
  $song.querySelector('.tempo').textContent = songData.tempo + 'bpm'
}
let updateSongs = () => state.songs.forEach(updateSongDetails)

다음으로 내가 이벤트 핸들러라고 부르는 것이 있습니다. ('이벤트 리스너'와 대조적으로; 이름이 좋지 않은 점은 양해 바랍니다. 더 나은 이름에 대한 제안은 환영합니다). 이들은 DOM 및 기타 이벤트(예: 타이머, WebSockets 등)를 수신하고 접근자와 DOM 업데이트 프로그램을 호출하여 애플리케이션 상태와 사용자 인터페이스를 업데이트하는 이벤트 리스너에 의해 호출됩니다. 그들이 이벤트 리스너로 직접 등록되지 않은 이유는 모듈성입니다. 객체가 아닌 일반 값을 인수로 요구함으로써 Event이러한 함수는 서로 호출되거나 다양한 매개변수를 사용하여 다른 이벤트 리스너에서 호출되어 변형을 달성할 수 있습니다.

 

이벤트 핸들러는 일반적으로(반드시 그런 것은 아니지만) 너무 많은 논리와 분기가 없는 매우 깔끔한 기능입니다. 그들은 때때로 약간 더 기술적인 사용자 이야기처럼 읽습니다.

 

let onPlay = () => {
  setPlay()
  updatePlaybackButton()
  updateScoresheet()
  startPlaybackTimer()
}
let onStop = () => {
  stopPlaybackTimer()
  setStop()
  updatePlaybackButton()
  updateScoresheet()
}
let onEdit = scores => {
  onStop()
  setScores(scores)
  updateScoresheet()
}
let onSongLoaded = () => {
  setInitialScores()
  updateScoresheet()
  updatePlaybackButton()
  updateLoadError()
}
let onLoadSong = songId => {
  onStop()
  loadSong(songId).then(onSongLoaded) 
}

 

그것들은 또한 내가 애플리케이션을 디버그하고 싶을 때 참조할 수 있는 체크포인트 역할을 합니다. 버그를 유발하는 사용자 작업을 알고 있으면 어떤 기능으로 시작해야 하는지 알 수 있습니다.

 

나는 여기서 약간 우회하여 여기에 '반응 상태'가 없음을 지적합니다. 나는 여러 가지 다른 패턴을 시도했고, 반응 상태가 데이터와 업데이트 중인 노드 간의 관계를 분산 및/또는 모호하게 만드는 경향이 있다는 결론을 내렸습니다. 나는 이것이 명시적인 것을 선호한다. 단점은 관련 함수 호출의 형태로 이러한 관계를 수동으로 구성해야 한다는 것입니다. 나는 또한 플랫폼이 (작동하는 한) 설계되지 않은 방식으로 수행하기 위해 [미니-]프레임워크를 구축하는 것보다 사용 가능한 것으로 이 작업을 수행하는 것을 선호합니다. 선호도와 마일리지가 다를 수 있습니다.

 

이벤트 핸들러 다음에 이벤트 바인딩이 있습니다. 여기에서 이벤트 리스너를 DOM 노드에 등록합니다. 값을 추출하기 위한 이벤트 객체의 모든 처리는 핸들러를 호출하기 전에 리스너에서 수행됩니다.

 

$play.onclick = () => onPlay()
$stop.onclick = () => onStop()
$scoreEditor.oninput = ev => onEdit(ev.target.value)

마지막으로 초기 상태를 지정합니다.

setReady()
updateView()

이 패턴은 일반적으로 지금까지 작업한 모든 프로젝트에서 잘 작동했습니다. 임베디드 앱이나 풀스택 앱을 작성하는 것과 같이 코드를 구성하는 이러한 방식에서 사소하지 않은 편차가 필요한 프로젝트가 있지만 이러한 경우에도 이 레이아웃은 적절한 시작점이자 레이아웃의 일반적인 정신이었습니다. 그대로 남아 있었다.

 

왜 전역 가변 상태일까?

 

이제 방에 있는 코끼리(어쨌든 그 중 하나)에 대해 설명합니다. 예, 상태는 전역적이며 변경 가능합니다. 이에 대한 많은 개발자들의 냉담한 반응은 제 생각에 "세상에, 변경 가능한 상태를 공유했습니다!"입니다. 우리 업계에는 공유(글로벌) 가변 상태에 대한 오랜 논쟁의 역사가 있습니다. 이 때문에 일부 개발자는 제안을 하는 것은 고사하고 누군가가 제안하는 것을 보고 화를 내기도 합니다.

 

대부분의 동기 코드의 경우 이것은 큰 문제가 아닙니다. 그러나 애플리케이션에 항상 동기 코드만 포함되는 것은 아닙니다. 저는 최근 에 두 개의 개별 인스턴스가 공유 변경 가능한 변수에 저장된 상태를 검사하는 비동기 코드를 실행하고 있는 내 애플리케이션에서 사용하고 있던 라이브러리 의 문제 를 해결했습니다. 이러한 문제가 발생합니다.

 

 

이러한 문제에 대한 두려움으로 개발자는 완전히 근절하기를 희망하는 많은 영리한 솔루션을 찾았습니다. 논쟁의 열기 속에서 우리 중 많은 사람들이 모든 노력에도 불구하고 여전히 프로덕션 버그가 있다는 사실을 잊습니다. 또한 공유 상태를 제거하지 않고도 이러한 문제가 해결된다는 사실도 잊었습니다. 내 생각에 두려움은 사람들을 터널 비전으로 몰아가는 경향이 있기 때문에 일반적으로 진정으로 효과적인 솔루션에 영감을 주지 않습니다.

 

내가 전역 가변 상태를 사용하기로 선택한 이유는 인종이 일반적으로 처음부터 그렇게 큰 문제가 아니라는 점을 제외하고는 공유 액세스의 인종이 이 접근 방식에서 얻는 이점보다 훨씬 더 중요하기 때문입니다. 내 머리 꼭대기에서 몇 가지 이름을 지정하려면 :

 

  • 함수가 인수를 통해 상태 값을 전달할 필요가 없습니다. 얼마나 많은 것을 공유해야 하는지, 인수의 위치, 인수 이름 지정 등에 대해 생각할 필요가 없습니다.
  • 상태의 복사본을 유지하는 함수는 필요 없으며 상태를 전달할 때 발생하는 유사한 기이함, 그로 인한 메모리 누수 등입니다.
  • 상태와 함께 작동해야 하는 코드를 단순화합니다.

상태가 변경되는 이유는 간단합니다. 객체를 변경하는 것은 항상 작성할 수 있는 가장 자연스러운 JavaScript였습니다.

 

다음 데이터를 고려하십시오.

 

let state = {
  tasks: [
    { title: 'Learn Vanilla', completed: false },
    { title: 'Write a blog post', completed: false },
  ]
}

 

작업이 완료된 것으로 표시하는 함수를 작성해 보겠습니다 . 먼저 상태를 변경하지 않고 수행할 수 있는 방법을 살펴보겠습니다.

 

let markCompleted = (index, state) => ({
  ...state,
  tasks: state.tasks.map((task, i) => index === i 
    ? { ...task, completed: true }
    : task
  )
})

 

이제 전역 상태를 변경하면 어떻게 보이는지 봅시다.

 

let markCompleted = index => state.tasks[index].completed = true

 

보시다시피 코드가 극적으로 단순해집니다. 단순한 것 뿐만 아니라 실제로 수행하는 데 드는 비용도 적게 들고 성능이 향상됩니다.

 

여기서 중요한 점은 플랫폼에 자연스럽게 맞는 스타일을 사용하면 추가 도구나 노력 없이 코드를 더 간단하게 만드는 경향이 있다는 것입니다.

 

다음으로

다음 기사에서는 DOM 노드를 효율적으로 만들고 조작하는 데 사용할 수 있는 몇 가지 기본 기술을 안내합니다.

 

 


 

Reference 

https://javascript.plainenglish.io/the-basic-vanilla-js-project-setup-9290dce6403f

 

The Basic Vanilla JavaScript Project Setup

Here’s how to get started with your next Vanilla JS project

javascript.plainenglish.io

 

https://ko.javascript.info/script-async-defer

 

defer, async 스크립트

 

ko.javascript.info