프레임워크 없이 프로그레시브 웹 애플리케이션 설계 및 구축(2부)
게시 됨: 2022-03-10이 모험의 존재 이유는 당신의 겸손한 작가를 시각 디자인과 자바스크립트 코딩 분야에서 조금 더 밀어붙이기 위해서였습니다. 내가 구축하기로 결정한 애플리케이션의 기능은 '할 일' 애플리케이션과 다르지 않았습니다. 이것은 독창적인 사고 방식의 연습이 아니라는 점을 강조하는 것이 중요합니다. 목적지는 여행보다 훨씬 덜 중요했습니다.
신청서가 어떻게 끝났는지 알고 싶으십니까? https://io.benfrain.com에서 휴대폰 브라우저를 가리킵니다.
이 기사에서 다룰 내용을 요약하면 다음과 같습니다.
- 프로젝트 설정과 빌드 도구로 Gulp를 선택한 이유
- 애플리케이션 디자인 패턴과 실제로 의미하는 것
- 애플리케이션 상태를 저장하고 시각화하는 방법
- CSS가 구성 요소로 범위가 지정된 방법;
- 어떤 UI/UX를 사용하여 보다 '앱처럼' 만들었는지,
- 반복을 통해 송금이 어떻게 변경되었는지.
빌드 도구부터 시작하겠습니다.
빌드 도구
TypeScipt 및 PostCSS의 기본 도구를 시작하고 실행하고 적절한 개발 환경을 만들려면 빌드 시스템이 필요합니다.
직장에서 지난 5년 정도 동안 HTML/CSS로 인터페이스 프로토타입을 만들고 그 정도는 덜했지만 JavaScript를 사용했습니다. 최근까지 나는 상당히 겸손한 빌드 요구 사항을 달성하기 위해 거의 독점적으로 많은 플러그인과 함께 Gulp를 사용했습니다.
일반적으로 CSS를 처리하고 JavaScript 또는 TypeScript를 더 널리 지원되는 JavaScript로 변환해야 하며 때때로 코드 출력 축소 및 자산 최적화와 같은 관련 작업을 수행해야 합니다. Gulp를 사용하면 항상 여유롭게 이러한 문제를 해결할 수 있습니다.
익숙하지 않은 사용자를 위해 Gulp를 사용하면 로컬 파일 시스템의 파일에 '무언가' 작업을 수행하는 JavaScript를 작성할 수 있습니다. Gulp를 사용하려면 일반적으로 프로젝트 루트에 단일 파일( gulpfile.js
라고 함)이 있어야 합니다. 이 JavaScript 파일을 사용하면 작업을 함수로 정의할 수 있습니다. 본질적으로 특정 작업을 처리하는 추가 JavaScript 기능인 타사 '플러그인'을 추가할 수 있습니다.
Gulp 작업의 예
Gulp 작업의 예는 작성 스타일 시트(gulp-postcss)를 변경할 때 PostCSS를 CSS로 처리하기 위해 플러그인을 사용하는 것입니다. 또는 TypeScript 파일을 저장할 때 바닐라 JavaScript(gulp-typescript)로 컴파일합니다. 다음은 Gulp에서 작업을 작성하는 방법에 대한 간단한 예입니다. 이 작업은 'del' gulp 플러그인을 사용하여 'build'라는 폴더의 모든 파일을 삭제합니다.
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
require
는 del
플러그인을 변수에 할당합니다. 그런 다음 gulp.task
메서드가 호출됩니다. 첫 번째 인수로 문자열을 사용하여 작업 이름("clean")을 지정한 다음 함수를 실행합니다. 이 경우 'del' 메서드를 사용하여 인수로 전달된 폴더를 삭제합니다. 별표 기호에는 기본적으로 빌드 폴더의 '모든 폴더의 모든 파일'을 나타내는 '글로브' 패턴이 있습니다.
Gulp 작업은 힙을 더 복잡하게 만들 수 있지만 본질적으로 이것이 처리 방법의 역학입니다. 사실 Gulp를 사용하면 JavaScript 마법사가 될 필요가 없습니다. 3학년 복사 및 붙여넣기 기술만 있으면 됩니다.
나는 Gulp를 기본 빌드 도구/작업 실행기로 몇 년 동안 '만약 고장나지 않았다면; 고치려고 하지마'.
그러나 나는 내 길에 끼어들지 않을까 걱정했다. 빠지기 쉬운 함정입니다. 첫째, 매년 같은 장소에서 휴가를 보낸 다음 결국에는 새로운 패션 트렌드를 받아들이는 것을 거부하고 새로운 빌드 도구를 시도하는 것을 단호하게 거부합니다.
나는 인터넷에서 'Webpack'에 대한 이야기를 많이 들었고 프론트엔드 개발자 쿨키즈의 새로운 축배를 사용하여 프로젝트를 시도하는 것이 내 의무라고 생각했습니다.
웹팩
나는 관심을 가지고 webpack.js.org 사이트로 건너뛰었던 것을 분명히 기억합니다. Webpack이 무엇인지에 대한 첫 번째 설명은 다음과 같이 시작됩니다.
import bar from './bar';
뭐라고? Dr. Evil의 말에 따르면 "여기에 뼈를 버려라, Scott".
나는 그것이 처리해야 할 내 자신의 전화 끊김이라는 것을 알고 있지만 'foo', 'bar' 또는 'baz'를 언급하는 모든 코딩 설명에 대해 혐오감을 갖게 되었습니다. 거기에 Webpack이 실제로 무엇 을 위한 것인지에 대한 간결한 설명이 전혀 없기 때문에 그것이 아마도 나를 위한 것이 아닐 수도 있다고 의심하게 만들었습니다.
Webpack 문서를 조금 더 파고들면 "기본적으로 webpack은 최신 JavaScript 애플리케이션을 위한 정적 모듈 번들러입니다"라는 약간 덜 불투명한 설명이 제공되었습니다.
흠. 정적 모듈 번들러. 그게 내가 원했던 걸까? 나는 확신하지 못했다. 계속 읽었지만 읽을수록 명확하지 않았습니다. 그 당시에는 종속성 그래프, 핫 모듈 재로딩 및 진입점과 같은 개념이 본질적으로 저에게 사라졌습니다.
며칠 저녁에 Webpack을 조사한 후 나는 그것을 사용하는 개념을 포기했습니다.
나는 올바른 상황과 더 많은 경험이 있는 손에 Webpack이 엄청나게 강력하고 적절하다고 확신하지만 나의 겸손한 요구에는 완전히 과도하게 보였습니다. 모듈 번들링, 트리 흔들기 및 핫 모듈 재로딩은 훌륭하게 들렸습니다. 나는 단지 내 작은 '앱'에 그것들이 필요하다는 확신이 없었습니다.
그럼 다시 Gulp로 돌아가세요.
변경을 위해 변경하지 않는다는 주제로 평가하고 싶은 또 다른 기술은 프로젝트 종속성을 관리하기 위한 Yarn over NPM입니다. 그 시점까지 저는 항상 NPM을 사용해 왔으며 Yarn은 더 좋고 더 빠른 대안으로 선전되었습니다. Yarn에 대해 현재 NPM을 사용하고 있고 모든 것이 정상이면 Yarn을 사용해 볼 필요가 없다는 것 외에는 별로 할 말이 없습니다.
이 응용 프로그램을 평가하기에는 너무 늦게 도착한 도구 중 하나가 Parceljs입니다. 구성이 전혀 없고 브라우저 다시 로드와 같은 BrowserSync가 지원되므로 그 이후로 훌륭한 유틸리티를 찾았습니다! 또한 Webpack의 방어에서 Webpack의 v4 이상에는 구성 파일이 필요하지 않다고 들었습니다. 일화적으로, 내가 트위터에서 실시한 보다 최근의 설문조사에서 87명의 응답자 중 절반 이상이 Gulp, Parcel 또는 Grunt보다 Webpack을 선택했습니다.
시작 및 실행을 위한 기본 기능으로 Gulp 파일을 시작했습니다.
'기본' 작업은 스타일 시트 및 TypeScript 파일의 '소스' 폴더를 감시하고 기본 HTML 및 관련 소스 맵과 함께 build
폴더로 컴파일합니다.
BrowserSync도 Gulp와 함께 작동합니다. Webpack 구성 파일로 무엇을 해야 할지 모를 수도 있지만 그렇다고 해서 내가 일종의 동물인 것은 아닙니다. HTML/CSS로 반복하는 동안 브라우저를 수동으로 새로 고쳐야 하는 것은 2010년이 되었고 BrowserSync 는 프런트 엔드 코딩에 매우 유용한 짧은 피드백과 반복 루프를 제공합니다.
2017년 11월 6일 현재 기본 꿀꺽꿀꺽 파일입니다.
ugilify를 사용하여 축소를 추가하여 배송이 끝나갈 무렵 Gulpfile을 어떻게 조정했는지 확인할 수 있습니다.
프로젝트 구조
내가 선택한 기술의 결과로 응용 프로그램에 대한 코드 구성의 일부 요소가 스스로 정의되었습니다. 프로젝트 루트의 gulpfile.js
, node_modules
폴더(Gulp가 플러그인 코드를 저장하는 위치), 저작 스타일 시트를 위한 preCSS
폴더, TypeScript 파일을 위한 ts
폴더, 컴파일된 코드를 위한 build
폴더.
아이디어는 비동적 HTML 구조를 포함하여 응용 프로그램의 '쉘'을 포함하는 index.html
을 갖고 응용 프로그램이 작동하도록 하는 스타일과 JavaScript 파일에 대한 링크를 포함하는 것이었습니다. 디스크에서는 다음과 같이 보일 것입니다.
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
해당 build
폴더를 보도록 BrowserSync를 구성한다는 것은 내 브라우저가 localhost:3000
을 가리키도록 할 수 있다는 것을 의미했으며 모든 것이 좋았습니다.
기본 빌드 시스템이 마련되고 파일 구성이 정리되고 시작해야 할 몇 가지 기본 디자인으로 인해 실제로 빌드하는 것을 방지하기 위해 합법적으로 사용할 수 있는 미루는 재료가 부족해졌습니다!
지원서 작성
응용 프로그램이 작동하는 방식의 원칙은 이러했습니다. 데이터 저장소가 있을 것입니다. JavaScript가 로드되면 해당 데이터를 로드하고 데이터의 각 플레이어를 반복하여 레이아웃에서 각 플레이어를 행으로 나타내는 데 필요한 HTML을 만들고 적절한 인/아웃 섹션에 배치합니다. 그런 다음 사용자의 상호 작용은 플레이어를 한 상태에서 다른 상태로 이동합니다. 단순한.
실제로 응용 프로그램을 작성할 때 이해해야 하는 두 가지 큰 개념적 문제는 다음과 같습니다.
- 쉽게 확장하고 조작할 수 있는 방식으로 응용 프로그램의 데이터를 표현하는 방법;
- 사용자 입력에서 데이터가 변경되었을 때 UI가 반응하도록 하는 방법.
JavaScript에서 데이터 구조를 나타내는 가장 간단한 방법 중 하나는 객체 표기법을 사용하는 것입니다. 그 문장은 약간 컴퓨터 과학-y를 읽습니다. 간단히 말해서 JavaScript 용어의 '객체'는 데이터를 저장하는 편리한 방법입니다.
ioState
(In/Out State용)라는 변수에 할당된 다음 JavaScript 객체를 고려하십시오.
var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }
JavaScript를 잘 모른다면 적어도 무슨 일이 일어나고 있는지 이해할 수 있을 것입니다. 중괄호 안의 각 행은 속성(또는 JavaScript 용어로 '키')과 값 쌍입니다. 모든 종류의 것을 JavaScript 키로 설정할 수 있습니다. 예를 들어, 함수, 다른 데이터의 배열 또는 중첩된 개체가 있습니다. 다음은 예입니다.
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
최종 결과는 이러한 종류의 데이터 구조를 사용하여 개체의 모든 키를 가져오고 설정할 수 있다는 것입니다. 예를 들어 ioState 객체의 개수를 7로 설정하려는 경우:
ioState.Count = 7;
텍스트를 해당 값으로 설정하려는 경우 표기법은 다음과 같이 작동합니다.
aTextNode.textContent = ioState.Count;
값을 가져오고 해당 상태 개체에 값을 설정하는 것은 JavaScript 측면에서 간단하다는 것을 알 수 있습니다. 그러나 사용자 인터페이스에 이러한 변경 사항을 반영하는 것은 그다지 중요하지 않습니다. 이것은 프레임워크와 라이브러리가 고통을 추상화하려고 하는 주요 영역입니다.
일반적으로 상태 기반 사용자 인터페이스 업데이트를 처리할 때 DOM 쿼리를 피하는 것이 좋습니다. 이는 일반적으로 차선책으로 간주되기 때문입니다.
In/Out 인터페이스를 고려하십시오. 일반적으로 게임의 잠재적인 플레이어 목록을 표시합니다. 페이지 아래에 세로로 하나씩 나열됩니다.
아마도 각 플레이어는 확인란 input
을 래핑하는 label
로 DOM에 표시됩니다. 이런 식으로 플레이어를 클릭하면 레이블이 입력을 '체크'로 지정하여 플레이어를 '인'으로 토글합니다.
인터페이스를 업데이트하기 위해 JavaScript의 각 입력 요소에 '리스너'가 있을 수 있습니다. 클릭 또는 변경 시 이 함수는 DOM을 쿼리하고 몇 개의 플레이어 입력이 확인되었는지 계산합니다. 그 수를 기반으로 DOM의 다른 항목을 업데이트하여 몇 명의 플레이어가 확인되었는지 사용자에게 보여줍니다.
그 기본 작업의 비용을 생각해 봅시다. 우리는 입력의 클릭/확인을 위해 여러 DOM 노드에서 수신 대기한 다음 DOM에 쿼리하여 몇 개의 특정 DOM 유형이 확인되었는지 확인한 다음 DOM에 무언가를 작성하여 사용자, UI 현명, 플레이어 수를 표시합니다. 우리는 방금 계산했습니다.
대안은 애플리케이션 상태를 메모리에 JavaScript 객체로 유지하는 것입니다. DOM의 버튼/입력 클릭은 JavaScript 개체를 업데이트한 다음 JavaScript 개체의 해당 변경 사항을 기반으로 필요한 모든 인터페이스 변경 사항에 대한 단일 패스 업데이트를 수행할 수 있습니다. JavaScript 개체가 이미 해당 정보를 보유하고 있기 때문에 플레이어를 계산하기 위해 DOM 쿼리를 건너뛸 수 있습니다.
그래서. 상태에 대한 JavaScript 객체 구조를 사용하는 것은 간단하지만 주어진 시간에 애플리케이션 상태를 캡슐화하기에 충분히 유연해 보였습니다. 이것이 어떻게 관리될 수 있는지에 대한 이론도 충분히 타당해 보였습니다. 이것이 '단방향 데이터 흐름'과 같은 문구가 전부였을 것입니다. 그러나 첫 번째 실제 트릭은 해당 데이터의 변경 사항을 기반으로 UI를 자동으로 업데이트하는 일부 코드를 만드는 것입니다.
좋은 소식은 나보다 더 똑똑한 사람들이 이미 이 사실을 알아냈다는 것입니다( 맙소사! ). 사람들은 응용 프로그램의 시작부터 이러한 종류의 도전에 대한 접근 방식을 완성해 왔습니다. 문제의 이 범주는 '디자인 패턴'의 빵과 버터입니다. '디자인 패턴'이라는 이름은 처음에는 난해하게 들렸지만 조금만 파고들자 모든 것이 덜 컴퓨터 과학과 더 상식적으로 들리기 시작했습니다.
디자인 패턴
컴퓨터 과학 사전에서 디자인 패턴은 일반적인 기술 문제를 해결하기 위해 미리 정의되고 입증된 방법입니다. 디자인 패턴을 요리 레시피와 동일한 코딩으로 생각하십시오.
디자인 패턴에 대한 가장 유명한 문헌은 아마도 1994년에 나온 "Design Patterns: Elements of Reusable Object-Oriented Software"일 것입니다. C++와 스몰토크를 다루고 있지만 개념은 양도할 수 있습니다. JavaScript의 경우 Addy Osmani의 "Learning JavaScript Design Patterns"가 비슷한 근거를 다룹니다. 여기에서 온라인으로 무료로 읽을 수도 있습니다.
관찰자 패턴
일반적으로 디자인 패턴은 생성, 구조 및 행동의 세 그룹으로 나뉩니다. 저는 응용 프로그램의 다른 부분에서 변경 사항을 전달하는 데 도움이 되는 행동 양식을 찾고 있었습니다.
보다 최근에 Gregg Pollack의 앱 내부 반응성 구현에 대한 정말 훌륭한 심층 분석을 보고 읽었습니다. 여기에 귀하의 즐거움을 위한 블로그 게시물과 비디오가 있습니다.
Learning JavaScript Design Patterns
에서 '관찰자' 패턴의 시작 설명을 읽었을 때 나는 그것이 나를 위한 패턴이라고 확신했습니다. 다음과 같이 설명되어 있습니다.
Observer는 개체(주제라고 함)가 개체(관찰자)에 따라 개체 목록을 유지 관리하여 상태 변경 사항을 자동으로 알리는 디자인 패턴입니다.
주제가 흥미로운 일이 발생했음을 관찰자에게 알려야 할 때 관찰자에게 알림을 브로드캐스트합니다(여기에는 알림 주제와 관련된 특정 데이터가 포함될 수 있음).
내 흥분의 핵심은 이것이 필요할 때 스스로 업데이트할 수 있는 방법을 제공하는 것 같았습니다.
사용자가 "Betty"라는 이름의 플레이어를 클릭하여 그녀가 게임에 'In'인 것으로 선택했다고 가정합니다. UI에서 몇 가지 작업을 수행해야 할 수 있습니다.
- 재생 횟수에 1 추가
- '아웃' 플레이어 풀에서 Betty 제거
- 플레이어의 '인' 풀에 Betty 추가
앱은 UI를 나타내는 데이터도 업데이트해야 합니다. 내가 매우 피하고 싶었던 것은 다음과 같습니다.
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
목표는 중앙 데이터가 변경되었을 때 DOM에 필요한 것을 업데이트하는 우아한 데이터 흐름을 갖는 것이었습니다.
Observer 패턴을 사용하면 상태에 대한 업데이트를 보낼 수 있으므로 사용자 인터페이스가 매우 간결해집니다. 다음은 목록에 새 플레이어를 추가하는 데 사용되는 실제 함수의 예입니다.
function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }
Observer 패턴과 관련된 부분은 io.notify
메소드입니다. 애플리케이션 상태의 items
부분을 수정하는 것을 보여주듯이 'items'에 대한 변경 사항을 수신한 관찰자를 보여 드리겠습니다.
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
데이터를 변경하는 알림 메서드가 있고 관심 있는 속성이 업데이트될 때 응답하는 해당 데이터에 대한 관찰자가 있습니다.
이 접근 방식을 사용하면 앱이 데이터 속성의 변경 사항을 감시하는 관찰 가능 항목을 가질 수 있고 변경 사항이 발생할 때마다 함수를 실행할 수 있습니다.
내가 선택한 Observer 패턴에 관심이 있다면 여기에서 더 자세히 설명하겠습니다.
이제 상태를 기반으로 UI를 효과적으로 업데이트하는 접근 방식이 있었습니다. 복숭아 같은. 그러나 이것은 여전히 두 가지 눈부신 문제를 남겼습니다.
하나는 페이지 새로고침/세션 전반에 걸쳐 상태를 저장하는 방법과 UI가 작동함에도 불구하고 시각적으로 '앱과 유사'하지 않다는 사실이었습니다. 예를 들어 버튼을 누르면 UI가 화면에서 즉시 변경됩니다. 특별히 매력적이지는 않았습니다.
먼저 물건의 저장 측면을 처리합시다.
상태 저장
여기에 들어가는 개발 측의 주요 관심은 앱 인터페이스를 어떻게 구축하고 JavaScript와 대화형으로 만들 수 있는지 이해하는 것이었습니다. 서버에서 데이터를 저장 및 검색하거나 사용자 인증 및 로그인을 처리하는 방법은 '범위 밖'이었습니다.
따라서 데이터 저장 요구 사항을 위해 웹 서비스에 연결하는 대신 모든 데이터를 클라이언트에 유지하기로 결정했습니다. 클라이언트에 데이터를 저장하는 웹 플랫폼 방법에는 여러 가지가 있습니다. 나는 localStorage
를 선택했다.
localStorage용 API는 매우 간단합니다. 다음과 같이 데이터를 설정하고 가져옵니다.
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage에는 두 개의 문자열을 전달하는 setItem
메서드가 있습니다. 첫 번째는 데이터를 저장할 키의 이름이고 두 번째 문자열은 저장하려는 실제 문자열입니다. getItem
메서드는 localStorage의 해당 키 아래에 저장된 모든 항목을 반환하는 인수로 문자열을 사용합니다. 멋지고 간단합니다.
그러나 localStorage를 사용하지 않는 이유 중 하나는 모든 것이 '문자열'로 저장되어야 한다는 사실입니다. 이것은 배열이나 객체와 같은 것을 직접 저장할 수 없다는 것을 의미합니다. 예를 들어 브라우저 콘솔에서 다음 명령을 실행해 보십시오.
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
'myArray'의 값을 배열로 설정하려고 했지만; 검색했을 때 문자열로 저장되었습니다('1,2,3,4' 주위의 따옴표에 주의).
localStorage를 사용하여 객체와 배열을 확실히 저장할 수 있지만 문자열에서 앞뒤로 변환해야 한다는 점을 염두에 두어야 합니다.
따라서 상태 데이터를 localStorage에 쓰기 위해 다음과 같이 JSON.stringify()
메서드를 사용하여 문자열에 작성했습니다.
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
localStorage에서 데이터를 검색해야 할 때 문자열은 다음과 같이 JSON.parse()
메서드를 사용하여 다시 사용 가능한 데이터로 바뀌었습니다.
const players = JSON.parse(storage.getItem("players"));
localStorage
를 사용한다는 것은 모든 것이 클라이언트에 있음을 의미했으며 타사 서비스나 데이터 저장 문제가 없음을 의미했습니다.
데이터는 이제 새로 고침과 세션을 지속하고 있었습니다. — 예! 나쁜 소식은 localStorage가 브라우저 데이터를 비우는 사용자로부터 살아남지 못한다는 것입니다. 누군가 그렇게 하면 모든 In/Out 데이터가 손실됩니다. 심각한 결점입니다.
`localStorage`가 아마도 '적절한' 애플리케이션을 위한 최상의 솔루션이 아니라는 사실을 이해하는 것은 어렵지 않습니다. 앞서 언급한 문자열 문제 외에도 '메인 스레드'를 차단하기 때문에 진지한 작업에는 느립니다. KV Storage와 같은 대안이 제공되고 있지만 현재로서는 적합성을 기준으로 사용에 주의해야 합니다.
사용자 장치에 로컬로 데이터를 저장하는 것은 취약하지만 서비스 또는 데이터베이스에 연결하는 것은 거부되었습니다. 대신 '로드/저장' 옵션을 제공하여 문제가 해결되었습니다. 이렇게 하면 In/Out의 모든 사용자가 데이터를 JSON 파일로 저장할 수 있으며 필요한 경우 앱에 다시 로드할 수 있습니다.
이것은 Android에서는 잘 작동했지만 iOS에서는 훨씬 덜 우아했습니다. iPhone에서는 다음과 같이 화면에 텍스트가 넘쳐났습니다.
상상할 수 있듯이, 나는 이 단점에 대해 WebKit을 통해 Apple을 질책하는 데 혼자가 아니었습니다. 관련 버그가 여기에 있었습니다.
이 버그를 작성하는 시점에서 이 버그에는 솔루션과 패치가 있지만 iOS Safari에는 아직 적용되지 않았습니다. 의심되는 바에 따르면, iOS13은 그것을 수정하지만 내가 쓰는 시점에서는 베타 버전입니다.
따라서 최소 실행 가능한 제품의 경우 스토리지가 해결되었습니다. 이제 좀 더 '앱 같은' 것을 시도할 때입니다!
앱아이네스
많은 사람들과의 많은 토론 끝에 '앱과 같은'이 의미하는 바를 정확히 정의하는 것은 매우 어려운 것으로 나타났습니다.
궁극적으로 나는 '앱 같은 것'이 일반적으로 웹에서 놓치는 시각적인 매끄러움과 동의어로 결정했습니다. 사용하기 좋은 앱을 생각하면 모두 모션 기능이 있습니다. 불필요하지만 당신의 행동에 대한 이야기를 추가하는 모션. 메뉴가 표시되는 방식인 화면 사이의 페이지 전환일 수 있습니다. 말로 설명하기는 어렵지만, 우리 대부분은 그것을 보면 압니다.
필요한 시각적 감각의 첫 번째 부분은 선택 시 플레이어 이름을 'In'에서 'Out'으로 또는 그 반대로 이동하는 것이었습니다. 플레이어가 한 섹션에서 다른 섹션으로 즉시 이동하도록 하는 것은 간단하지만 확실히 '앱 같은' 것은 아닙니다. 플레이어 이름을 클릭한 애니메이션은 플레이어가 한 카테고리에서 다른 카테고리로 이동하는 상호작용의 결과를 강조하기를 바랍니다.
이러한 많은 종류의 시각적 상호 작용과 마찬가지로 겉보기에는 단순하기 때문에 실제로 제대로 작동하는 데 관련된 복잡성이 있습니다.
움직임을 올바르게 하는 데 몇 번의 반복이 필요했지만 기본 논리는 다음과 같습니다.
- '플레이어'를 클릭하면 페이지에서 기하학적으로 해당 플레이어의 위치를 캡처합니다.
- 위로 올라갈 경우('In') 플레이어가 이동해야 하는 영역의 상단이 얼마나 떨어져 있고, 내려가는 경우('Out') 하단이 얼마나 멀리 떨어져 있는지 측정합니다.
- 올라가는 경우 플레이어가 위로 이동할 때 플레이어 행의 높이와 동일한 공간을 남겨 두어야 하며 위의 플레이어는 플레이어가 해당 공간에 착지하는 데 걸리는 시간과 동일한 속도로 아래로 붕괴되어야 합니다. 기존 '인' 플레이어(존재하는 경우)가 내려옴으로써 자리를 비웠습니다.
- 플레이어가 '아웃'으로 이동하고 아래로 이동하는 경우 다른 모든 항목은 왼쪽 공간으로 이동해야 하며 플레이어는 현재 '아웃' 플레이어 아래에 있어야 합니다.
휴! 생각했던 것보다 더 까다롭습니다. JavaScript는 신경쓰지 마세요!
전환 속도와 같이 고려해야 할 추가적인 복잡성과 시도가 있었습니다. 처음에는 일정한 이동 속도(예: 20ms당 20px) 또는 일정한 이동 지속 시간(예: 0.2s)이 더 좋아 보일지 여부가 명확하지 않았습니다. 전자는 플레이어가 이동해야 하는 거리에 따라 '즉석에서' 속도를 계산해야 하기 때문에 약간 더 복잡했습니다. 거리가 멀수록 전환 시간이 더 길어야 합니다.
그러나 일정한 전환 지속 시간이 코드에서 단순하지 않다는 것이 밝혀졌습니다. 실제로 더 유리한 효과를 냈습니다. 그 차이는 미묘했지만 두 옵션을 모두 본 후에만 결정할 수 있는 종류의 선택입니다.
이 효과를 구현하려고 시도하는 동안 시각적 결함이 눈에 띄었지만 실시간으로 분해하는 것은 불가능했습니다. 최고의 디버깅 프로세스는 애니메이션의 QuickTime 녹화를 만든 다음 한 번에 한 프레임씩 진행하는 것입니다. 변함없이 이것은 어떤 코드 기반 디버깅보다 더 빨리 문제를 드러냈습니다.
지금 코드를 보면, 내 겸손한 앱 이외의 다른 것에서 이 기능이 거의 확실히 더 효과적으로 작성될 수 있다는 점을 이해할 수 있습니다. 앱이 플레이어 수와 고정된 슬랫 높이를 알고 있다는 점을 감안할 때 DOM 읽기 없이 JavaScript만으로 모든 거리 계산을 수행하는 것이 완전히 가능해야 합니다.
배송된 제품이 작동하지 않는 것이 아니라 인터넷에서 선보일 코드 솔루션의 종류가 아닐 뿐입니다. 오, 잠깐.
다른 '앱과 같은' 상호작용은 훨씬 쉽게 해낼 수 있었습니다. 디스플레이 속성을 토글하는 것과 같은 간단한 작업으로 메뉴를 간단히 인앤아웃하는 대신, 조금만 더 정교하게 메뉴를 노출함으로써 많은 마일리지를 얻을 수 있었습니다. 여전히 간단하게 트리거되었지만 CSS가 모든 무거운 작업을 수행하고 있었습니다.
.io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }
data-evswitcher-showing="true"
속성이 상위 요소에서 토글되면 메뉴가 페이드 인되고 기본 위치로 다시 변환되며 메뉴가 클릭을 수신할 수 있도록 포인터 이벤트가 다시 활성화됩니다.
ECSS 스타일 시트 방법론
저작의 관점에서 이전 코드에서 CSS 재정의가 상위 선택기 내에 중첩되고 있음을 알 수 있습니다. 이것이 내가 항상 UI 스타일 시트 작성을 선호하는 방식입니다. 각 선택기에 대한 단일 정보 소스 및 단일 중괄호 세트 내에 캡슐화된 해당 선택기에 대한 재정의. CSS 프로세서(Sass, PostCSS, LESS, Stylus 등)를 사용해야 하는 패턴이지만 중첩 기능을 사용하는 유일한 긍정적인 방법이라고 생각합니다.
나는 내 책인 Enduring CSS에서 이 접근 방식을 확고히 했습니다. 인터페이스 요소에 대해 CSS를 작성하는 데 사용할 수 있는 더 많은 관련 방법이 있음에도 불구하고 ECSS는 접근 방식이 처음 문서화된 이후로 저와 함께 잘 일하는 대규모 개발 팀에 도움이 되었습니다. 2014년으로 돌아왔다! 이 경우에도 효과가 입증되었습니다.
TypeScript의 부분화
CSS 프로세서나 Sass와 같은 상위 집합 언어가 없어도 CSS는 import 지시문을 사용하여 하나 이상의 CSS 파일을 다른 파일로 가져올 수 있습니다.
@import "other-file.css";
JavaScript를 시작할 때 나는 그에 상응하는 것이 없다는 것에 놀랐습니다. 코드 파일이 화면보다 길거나 너무 높을 때마다 항상 더 작은 조각으로 나누는 것이 도움이 될 것 같은 느낌이 듭니다.
TypeScript 사용의 또 다른 장점은 코드를 파일로 분할하고 필요할 때 가져오는 매우 간단한 방법이 있다는 것입니다.
이 기능은 기본 JavaScript 모듈보다 먼저 사용되었으며 매우 편리한 기능이었습니다. TypeScript가 컴파일될 때 모든 것을 단일 JavaScript 파일로 다시 연결했습니다. 이는 저작을 위해 애플리케이션 코드를 관리 가능한 부분 파일로 쉽게 분할하고 메인 파일로 쉽게 가져올 수 있음을 의미했습니다. 기본 inout.ts
의 상단은 다음과 같습니다.
/// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />
이 간단한 집안일과 정리 작업이 큰 도움이 되었습니다.
여러 이벤트
처음에는 기능적인 측면에서 "화요일 밤의 축구"와 같은 단일 이벤트로 충분하다고 느꼈습니다. 그 시나리오에서 인/아웃을 로드했다면 플레이어를 추가/제거하거나 인/아웃으로 이동한 것뿐입니다. 다중 이벤트에 대한 개념은 없었습니다.
나는 (심지어 최소한의 실행 가능한 제품을 위해 가더라도) 이것이 꽤 제한된 경험을 만들 것이라고 빠르게 결정했습니다. 누군가 다른 날짜에 다른 선수 명단으로 두 개의 게임을 조직했다면 어떻게 될까요? 확실히 In/Out이 그 요구를 수용할 수 있었습니까? 이를 가능하게 하고 다른 세트에서 로드하는 데 필요한 메소드를 수정하기 위해 데이터를 재구성하는 데 너무 오랜 시간이 걸리지 않았습니다.
처음에 기본 데이터 세트는 다음과 같았습니다.
var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];
각 플레이어에 대한 개체를 포함하는 배열입니다.
여러 이벤트를 고려한 후 다음과 같이 수정되었습니다.
var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];
새 데이터는 각 이벤트에 대한 개체가 있는 배열이었습니다. 그런 다음 각 이벤트에는 이전과 같이 플레이어 개체가 포함된 배열인 EventData
속성이 있었습니다.
인터페이스가 이 새로운 기능을 가장 잘 처리할 수 있는 방법을 재고하는 데 훨씬 더 오랜 시간이 걸렸습니다.
처음부터 디자인은 항상 매우 무미건조했습니다. 이것도 디자인 연습으로 해야 하는 일이라 용기가 부족하다는 생각이 들었습니다. 그래서 헤더부터 시작하여 시각적인 감각이 조금 더 추가되었습니다. 이것이 내가 Sketch에서 조롱한 것입니다.
그것은 상을 수상하지 않을 것이지만 확실히 시작했던 곳보다 더 매력적이었습니다.
미학은 제쳐두고, 다른 사람이 지적할 때까지는 헤더의 큰 더하기 아이콘이 매우 혼란스럽다는 점에 감사했습니다. 대부분의 사람들은 다른 이벤트를 추가하는 방법이라고 생각했습니다. 실제로는 현재 이벤트 이름이 있던 동일한 위치에 플레이어 이름을 입력할 수 있는 멋진 전환과 함께 '플레이어 추가' 모드로 전환되었습니다.
신선한 눈이 귀한 또 다른 사례였습니다. 놓아주는 것도 중요한 교훈이었다. 솔직히 말해서 헤더에서 입력 모드 전환이 멋지고 영리하다고 느꼈기 때문에 계속 누르고 있었습니다. 그러나 사실은 디자인과 애플리케이션 전체에 서비스를 제공하지 않는다는 것이었습니다.
이것은 라이브 버전에서 변경되었습니다. 대신 헤더는 이벤트만 처리합니다. 보다 일반적인 시나리오입니다. 한편, 플레이어 추가는 하위 메뉴에서 수행됩니다. 이것은 앱에 훨씬 더 이해하기 쉬운 계층 구조를 제공합니다.
여기서 배운 또 다른 교훈은 가능할 때마다 동료로부터 솔직한 피드백을 받는 것이 매우 유익하다는 것입니다. 그들이 착하고 정직한 사람들이라면, 그들은 당신이 자신을 통과하도록 허용하지 않을 것입니다!
요약: 내 코드에서 악취가 난다
오른쪽. 지금까지는 평범한 테크-어드벤처 회고편입니다. 이러한 것들은 Medium에서 10페니입니다! 공식은 다음과 같습니다. 개발자는 어떻게 모든 장애물을 부수고 인터넷에 정교하게 조정된 소프트웨어를 출시한 다음 Google에서 인터뷰를 받거나 어딘가에서 인수하게 되었는지 자세히 설명합니다. 그러나 문제의 진실은 내가 이 앱 구축 malarkey에서 처음이었기 때문에 코드는 궁극적으로 '완성된' 애플리케이션으로 높은 천국으로 스턴된 상태로 배송되었다는 것입니다!
예를 들어 사용된 Observer 패턴 구현은 매우 잘 작동했습니다. 나는 처음에는 조직적이고 조직적이었지만 일을 끝내고자 더 필사적으로 변하면서 그 접근 방식은 '남쪽'으로 갔다. 연속 다이어트를 하는 사람처럼 오래된 친숙한 습관이 다시 스며들어 코드 품질이 떨어졌습니다.
이제 제공된 코드를 보면 깔끔한 관찰자 패턴과 늪지 표준 이벤트 수신기가 함수를 호출하는 이상적인 뒤죽박죽이 아닙니다. In the main inout.ts
file there are over 20 querySelector
method calls; hardly a poster child for modern application development!
I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.
The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.