Ember.js 개발자가 저지르는 가장 흔한 8가지 실수
게시 됨: 2022-03-11Ember.js는 복잡한 클라이언트 측 애플리케이션을 구축하기 위한 포괄적인 프레임워크입니다. 그 신조 중 하나는 "구성에 대한 관례"이며 대부분의 웹 응용 프로그램에 공통적인 개발의 상당 부분이 있으므로 이러한 일상적인 문제의 대부분을 해결할 수 있는 단일 최선의 방법이 있다는 확신입니다. 그러나 올바른 추상화를 찾고 모든 경우를 포괄하려면 전체 커뮤니티의 시간과 의견이 필요합니다. 추론이 진행됨에 따라 시간을 내어 핵심 문제에 대한 올바른 솔루션을 찾은 다음 프레임워크에 적용하는 것이 해결책을 찾아야 할 때 손을 내밀고 모든 사람이 스스로 해결하도록 하는 것보다 낫습니다.
Ember.js는 개발을 더욱 쉽게 하기 위해 지속적으로 발전하고 있습니다. 그러나 모든 고급 프레임워크와 마찬가지로 Ember 개발자가 빠질 수 있는 함정이 여전히 있습니다. 다음 포스트를 통해 이를 피할 수 있는 지도를 제공하고자 합니다. 바로 뛰어들자!
일반적인 실수 1: 모든 컨텍스트 개체가 전달될 때 모델 후크가 실행될 것으로 예상
애플리케이션에 다음 경로가 있다고 가정해 보겠습니다.
Router.map(function() { this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
band
경로에는 동적 세그먼트 id
가 있습니다. 앱이 /bands/24
와 같은 URL로 로드되면 해당 경로 band
의 model
후크에 24
가 전달됩니다. 모델 후크는 세그먼트를 역직렬화하여 템플릿에서 사용할 수 있는 개체(또는 개체 배열)를 만드는 역할을 합니다.
// app/routes/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); // params.id is '24' } });
여태까지는 그런대로 잘됐다. 그러나 브라우저의 탐색 표시줄에서 응용 프로그램을 로드하는 것보다 경로를 입력하는 다른 방법이 있습니다. 그 중 하나는 템플릿에서 link-to
도우미를 사용하는 것입니다. 다음 스니펫은 대역 목록을 살펴보고 해당 band
경로에 대한 링크를 생성합니다.
{{#each bands as |band|}} {{link-to band.name "band" band}} {{/each}}
link-to의 마지막 인수인 band
는 경로에 대한 동적 세그먼트를 채우는 객체이므로 해당 id
는 경로에 대한 id 세그먼트가 됩니다. 많은 사람들이 빠지는 함정은 모델이 이미 알려져 있고 전달되었기 때문에 이 경우 모델 후크가 호출되지 않는다는 것입니다. 이해가 되고 서버에 대한 요청을 저장할 수 있지만 분명히 그렇지 않습니다. 직관적인. 이를 우회하는 독창적인 방법은 객체 자체가 아니라 해당 ID를 전달하는 것입니다.
{{#each bands as |band|}} {{link-to band.name "band" band.id}} {{/each}}
Ember의 완화 계획
라우팅 가능한 구성 요소는 버전 2.1 또는 2.2로 곧 Ember에 제공될 예정입니다. 그들이 착륙하면 모델 후크는 동적 세그먼트가 있는 경로로 전환하는 방법에 관계없이 항상 호출됩니다. 여기에서 해당 RFC를 읽으십시오.
일반적인 실수 2: 경로 기반 컨트롤러가 싱글톤이라는 사실을 잊었습니다.
Ember.js의 경로는 해당 템플릿의 컨텍스트 역할을 하는 컨트롤러의 속성을 설정합니다. 이러한 컨트롤러는 싱글톤이므로 컨트롤러에 정의된 모든 상태는 컨트롤러가 더 이상 활성화되지 않은 경우에도 유지됩니다.
이것은 매우 간과하기 쉬운 일이며 저도 이것을 우연히 발견했습니다. 제 경우에는 밴드와 노래가 포함된 음악 카탈로그 앱이 있었습니다. songs
컨트롤러의 songCreationStarted
플래그는 사용자가 특정 밴드의 노래를 만들기 시작했음을 나타냅니다. 문제는 사용자가 다른 밴드로 전환하면 songCreationStarted
의 값이 유지되고 반쯤 완성된 곡이 다른 밴드를 위한 것 같아서 혼란스러웠습니다.
해결책은 오래 머물고 싶지 않은 컨트롤러 속성을 수동으로 재설정하는 것입니다. 이를 수행하는 한 가지 가능한 장소는 해당 경로의 setupController
후크입니다. 이 후크는 afterModel
후크(이름에서 알 수 있듯이 model
후크 다음에 옴) 이후의 모든 전환에서 호출됩니다.
// app/routes/band.js export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); controller.set('songCreationStarted', false); } });
Ember의 완화 계획
다시 말하지만, 라우팅 가능한 구성 요소의 여명은 이 문제를 해결하여 컨트롤러를 완전히 끝낼 것입니다. 라우팅 가능한 구성 요소의 장점 중 하나는 수명 주기가 더 일관되고 해당 경로에서 전환할 때 항상 끊어진다는 것입니다. 그들이 도착하면 위의 문제는 사라질 것입니다.
일반적인 실수 3: setupController
에서 기본 구현을 호출하지 않음
Ember의 경로에는 애플리케이션별 동작을 정의하는 소수의 수명 주기 후크가 있습니다. 우리는 이미 해당 템플릿에 대한 데이터를 가져오는 데 사용되는 model
과 템플릿의 컨텍스트인 컨트롤러를 설정하는 setupController
를 보았습니다.
후자의 setupController
는 컨트롤러의 model
속성으로 model
후크에서 모델을 할당하는 합리적인 기본값을 가지고 있습니다.
// ember-routing/lib/system/route.js setupController(controller, context, transition) { if (controller && (context !== undefined)) { set(controller, 'model', context); } }
( context
는 위에서 내가 model
이라고 부르는 것에 대한 ember-routing
패키지에서 사용하는 이름입니다)
setupController
후크는 컨트롤러의 상태를 재설정하는 것과 같은 여러 목적으로 재정의될 수 있습니다(위의 일반적인 실수 2번에서와 같이). 그러나 Ember.Route에서 위에서 복사한 상위 구현을 호출하는 것을 잊어버린 경우 컨트롤러에 model
속성이 설정되어 있지 않기 때문에 긴 머리를 긁는 세션에 들어갈 수 있습니다. 따라서 항상 this._super(controller, model)
호출하십시오.
export default Ember.Route.extend({ setupController: function(controller, model) { this._super(controller, model); // put the custom setup here } });
Ember의 완화 계획
앞에서 언급했듯이 컨트롤러와 컨트롤러와 함께 setupController
후크가 곧 사라질 것이므로 이 함정은 더 이상 위협이 되지 않습니다. 그러나 여기서 배워야 할 더 큰 교훈이 있는데, 이는 조상의 구현을 염두에 두어야 한다는 것입니다. Ember에 있는 모든 객체의 어머니인 Ember.Object
에 정의된 init
함수는 주의해야 하는 또 다른 예입니다.
일반적인 실수 4: 부모가 아닌 경로와 함께 this.modelFor
사용
Ember 라우터는 URL을 처리할 때 각 경로 세그먼트에 대한 모델을 확인합니다. 애플리케이션에 다음 경로가 있다고 가정해 보겠습니다.
Router.map({ this.route('bands', function() { this.route('band', { path: ':id' }, function() { this.route('songs'); }); }); });
/bands/24/songs
의 URL이 주어지면 bands
, bands.band
및 bands.band.songs
의 model
후크가 이 순서로 호출됩니다. 경로 API에는 해당 모델이 해당 지점에서 확실히 해결되었기 때문에 상위 경로 중 하나에서 모델을 가져오기 위해 하위 경로에서 사용할 수 있는 특히 편리한 modelFor
메서드가 있습니다.
예를 들어, 다음 코드는 bands.band
경로에서 band 객체를 가져오는 유효한 방법입니다:
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); return bands.filterBy('id', params.id); } });
그러나 일반적인 실수는 경로의 부모가 아닌 modelFor에서 경로 이름을 사용하는 것입니다. 위 예의 경로가 약간 변경된 경우:
Router.map({ this.route('bands'); this.route('band', { path: 'bands/:id' }, function() { this.route('songs'); }); });
URL에 지정된 밴드를 가져오는 방법은 bands
경로가 더 이상 부모가 아니므로 해당 모델이 해결되지 않았기 때문에 중단됩니다.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { var bands = this.modelFor('bands'); // `bands` is undefined return bands.filterBy('id', params.id); // => error! } });
해결책은 부모 경로에만 modelFor
를 사용하고 store에서 가져오는 것과 같이 modelFor
를 사용할 수 없는 경우 필요한 데이터를 검색하기 위해 다른 수단을 사용하는 것입니다.
// app/routes/bands/band.js export default Ember.Route.extend({ model: function(params) { return this.store.find('band', params.id); } });
일반적인 실수 5: 구성 요소 작업이 실행되는 컨텍스트를 오해하기
중첩된 구성 요소는 항상 Ember에서 추론하기 가장 어려운 부분 중 하나였습니다. Ember 1.10에 블록 매개변수가 도입되면서 이러한 복잡성이 많이 완화되었지만 많은 상황에서 자식 구성 요소에서 실행된 작업이 트리거될 구성 요소를 한 눈에 보는 것은 여전히 까다롭습니다.
밴드 band-list-items
이 있는 band-list
구성 요소가 있고 각 밴드를 목록에서 즐겨찾기로 표시할 수 있다고 가정해 보겠습니다.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band faveAction="setAsFavorite"}} {{/each}}
사용자가 버튼을 클릭할 때 호출되어야 하는 작업 이름은 band-list-item
구성 요소에 전달되고 faveAction
속성의 값이 됩니다.
이제 band-list-item
의 템플릿 및 구성 요소 정의를 살펴보겠습니다.

// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "faveBand"}}>Fave this</button>
// app/components/band-list-item.js export default Ember.Component.extend({ band: null, faveAction: '', actions: { faveBand: { this.sendAction('faveAction', this.get('band')); } } });
사용자가 "Fave this" 버튼을 클릭하면 faveBand
작업이 트리거 되어 상위 구성 요소 인 band-list
에서 전달된 구성 요소의 faveAction
(위의 경우 setAsFavorite
)을 실행합니다.
경로 기반 템플릿의 작업이 컨트롤러에서 발생하는 것과 동일한 방식으로 작업이 시작될 것으로 예상하기 때문에 많은 사람들이 문제를 일으키게 됩니다. 이것을 더 나쁘게 만드는 것은 오류 메시지가 기록되지 않는다는 것입니다. 상위 구성 요소는 오류를 삼켜 버립니다.
일반적인 규칙은 작업이 현재 컨텍스트에서 시작된다는 것입니다. 비 구성 요소 템플릿의 경우 해당 컨텍스트는 현재 컨트롤러이고 구성 요소 템플릿의 경우 상위 구성 요소(있는 경우)이거나 구성 요소가 중첩되지 않은 경우 다시 현재 컨트롤러입니다.
따라서 위의 경우 band-list
구성 요소는 band-list-item
에서 수신한 작업을 다시 시작하여 컨트롤러 또는 경로로 버블링해야 합니다.
// app/components/band-list.js export default Ember.Component.extend({ bands: [], favoriteAction: 'setFavoriteBand', actions: { setAsFavorite: function(band) { this.sendAction('favoriteAction', band); } } });
band-list
이 bands
템플릿에 정의된 경우 setFavoriteBand
작업은 bands
컨트롤러 또는 bands
경로(또는 상위 경로 중 하나)에서 처리되어야 합니다.
Ember의 완화 계획
더 많은 수준의 중첩이 있는 경우(예: band-list-item
내부에 fav-button
구성 요소가 있는 경우) 이것이 더 복잡해진다고 상상할 수 있습니다. 메시지를 내보내려면 내부에서 여러 레이어를 통해 구멍을 뚫고 각 수준에서 의미 있는 이름을 정의해야 합니다( setAsFavorite
, favoriteAction
, faveAction
등).
이것은 이미 마스터 브랜치에서 사용할 수 있고 아마도 1.13에 포함될 "개선된 작업 RFC"에 의해 더 간단해졌습니다.
위의 예는 다음과 같이 단순화됩니다.
// app/templates/components/band-list.hbs {{#each bands as |band|}} {{band-list-item band=band setFavBand=(action "setFavoriteBand")}} {{/each}}
// app/templates/components/band-list-item.hbs <div class="band-name">{{band.name}}</div> <button class="fav-button" {{action "setFavBand" band}}>Fave this</button>
일반적인 실수 6: 배열 속성을 종속 키로 사용
Ember의 계산된 속성은 다른 속성에 종속되며 이 종속성은 개발자가 명시적으로 정의해야 합니다. 역할 중 하나가 admin
인 경우에만 true여야 하는 isAdmin
속성이 있다고 가정해 보겠습니다. 다음과 같이 작성할 수 있습니다.
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles')
위의 정의에서 isAdmin
값은 roles
배열 객체 자체가 변경되는 경우에만 무효화되지만 항목이 기존 배열에 추가되거나 제거되는 경우에는 무효화되지 않습니다. 추가 및 제거도 재계산을 트리거해야 함을 정의하는 특수 구문이 있습니다.
isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]')
일반적인 실수 7: 관찰자 친화적인 방법을 사용하지 않음
Common Mistake No. 6의 (현재 수정된) 예제를 확장하고 애플리케이션에서 User 클래스를 생성해 보겠습니다.
var User = Ember.Object.extend({ initRoles: function() { var roles = this.get('roles'); if (!roles) { this.set('roles', []); } }.on('init'), isAdmin: function() { return this.get('roles').contains('admin'); }.property('roles.[]') });
이러한 User
에 admin
역할을 추가하면 우리는 깜짝 놀라게 됩니다.
var user = User.create(); user.get('isAdmin'); // => false user.get('roles').push('admin'); user.get('isAdmin'); // => false ?
문제는 기본 Javascript 메서드가 사용되는 경우 관찰자가 실행되지 않고 따라서 계산된 속성이 업데이트되지 않는다는 것입니다. 이는 브라우저에서 Object.observe
의 글로벌 채택이 개선되면 변경될 수 있지만 그때까지는 Ember가 제공하는 메서드 집합을 사용해야 합니다. 현재의 경우 pushObject
는 관찰자에게 친숙한 push
와 동일합니다.
user.get('roles').pushObject('admin'); user.get('isAdmin'); // => true, finally!
일반적인 실수 8번: 컴포넌트의 속성에 전달된 돌연변이
항목의 등급을 표시하고 항목의 등급을 설정할 수 있는 star-rating
구성 요소가 있다고 상상해 보십시오. 등급은 노래, 책 또는 축구 선수의 드리블 기술에 대한 것일 수 있습니다.
템플릿에서 다음과 같이 사용합니다.
{{#each songs as |song|}} {{star-rating item=song rating=song.rating}} {{/each}}
구성 요소가 별을 표시하고 각 포인트에 대해 하나의 완전한 별을 표시하고 그 이후에는 최대 등급까지 빈 별을 표시한다고 가정해 보겠습니다. 별을 클릭하면 컨트롤러에서 set
action이 실행되고 이는 사용자가 등급을 업데이트하려는 것으로 해석되어야 합니다. 이를 달성하기 위해 다음 코드를 작성할 수 있습니다.
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); item.set('rating', newRating); return item.save(); } } });
그렇게 하면 작업이 완료되지만 몇 가지 문제가 있습니다. 첫째, 전달된 항목에 rating
속성이 있다고 가정하므로 이 구성 요소를 사용하여 레오 메시의 드리블 기술(이 속성을 score
라고 할 수 있음)을 관리할 수 없습니다.
둘째, 구성 요소에서 항목의 등급을 변경합니다. 이로 인해 특정 속성이 변경되는 이유를 파악하기 어려운 시나리오가 발생합니다. 예를 들어 축구 선수의 평균 점수를 계산하는 데 해당 등급이 사용되는 동일한 템플릿에 다른 구성 요소가 있다고 상상해 보십시오.
이 시나리오의 복잡성을 완화하기 위한 슬로건은 "Data down, actions up"(DDAU)입니다. 데이터는 경로에서 컨트롤러로 구성 요소로 전달되어야 하며 구성 요소는 이러한 데이터의 변경 사항을 컨텍스트에 알리는 작업을 사용해야 합니다. 그렇다면 여기에 DDAU를 어떻게 적용해야 할까요?
등급 업데이트를 위해 보내야 하는 작업 이름을 추가해 보겠습니다.
{{#each songs as |song|}} {{star-rating item=song rating=song.rating setAction="updateRating"}} {{/each}}
그런 다음 해당 이름을 사용하여 작업을 전송합니다.
// app/components/star-rating.js export default Ember.Component.extend({ item: null, rating: 0, (...) actions: { set: function(newRating) { var item = this.get('item'); this.sendAction('setAction', { item: this.get('item'), rating: newRating }); } } });
마지막으로, 액션은 컨트롤러 또는 경로에 의해 업스트림으로 처리되며 여기에서 항목의 등급이 업데이트됩니다.
// app/routes/player.js export default Ember.Route.extend({ actions: { updateRating: function(params) { var skill = params.item, rating = params.rating; skill.set('score', rating); return skill.save(); } } });
이 경우 이 변경 사항은 star-rating
구성 요소에 전달된 바인딩을 통해 아래쪽으로 전파되고 결과적으로 표시되는 전체 별 수가 변경됩니다.
이렇게 하면 구성 요소에서 돌연변이가 발생하지 않으며 앱 특정 부분은 경로에서 작업을 처리하는 것뿐이므로 구성 요소의 재사용성이 저하되지 않습니다.
축구 기술에 동일한 구성 요소를 사용할 수도 있습니다.
{{#each player.skills as |skill|}} {{star-rating item=skill rating=skill.score setAction="updateSkill"}} {{/each}}
마지막 단어
내가 여기에 쓴 것을 포함하여 사람들이 저지르는(또는 스스로 저지른) 실수 중 일부(대부분?)가 2.x 시리즈 초기에 사라지거나 크게 완화될 것이라는 점에 주목하는 것이 중요합니다. Ember.js의.
남은 것은 위의 제 제안으로 해결되었으므로 Ember 2.x에서 개발하고 나면 더 이상 오류를 범할 변명의 여지가 없습니다! 이 기사를 pdf로 원하시면 제 블로그로 이동하여 게시물 하단의 링크를 클릭하십시오.
나에 대해서
저는 2년 전에 Ember.js를 사용하여 프론트 엔드 세계에 왔고 여기 머물기 위해 왔습니다. 나는 Ember에 대한 열정이 너무 강해져서 게스트 포스트와 내 블로그, 그리고 컨퍼런스에서 발표하기 시작했습니다. 나는 Ember를 배우고자 하는 사람을 위해 Ember.js로 Rock and Roll 이라는 책을 쓰기도 했습니다. 여기에서 샘플 챕터를 다운로드할 수 있습니다.