References: Creating your first app
프로젝트 폴더에서 콘솔로 meteor list를 입력해 봅시다.
몇 가지 패키지가 눈에 띄시지 않나요 ?
지난 포스팅에서도 언급했듯이 저 패키지를 그대로 내보내는 건 보안 이슈가 있습니다.
이번에는 이 패키지를 제거해 보겠습니다.
1. 메서드를 이용한 보안
일단 insecure 패키지를 제거합시다. 콘솔에서 meteor remove insecure를 입력해 패키지를 제거해 주세요
이로써 클라이언트 측에 기본으로 주어지던 DB 사용 권한이 다 사라졌습니다.
이제 새로 디비에 접근하기 위한 메서드를 정의합니다.
/imports/api/tasks.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import { Mongo } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the user is logged in before inserting a task if (! this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(), owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); Tasks.update(taskId, { $set: { checked: setChecked } }); }, }); | cs |
기존 task 컬렉션에 직접 붙어서 수행하던 것을 새로 정의한 메서드로 대체 합니다.
/imports/ui/App.jsx
1 2 3 4 5 6 7 8 | handleSubmit(event) { event.preventDefault(); // Find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // Clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } | cs |
/imports/ui/Task.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // Set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); } deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); } render() { // Give tasks a different className when they are checked off, // so that we can style them nicely in CSS const taskClassName = this.props.task.checked ? 'checked' : ''; return ( <li className={taskClassName}> <button className="delete" onClick={this.deleteThisTask.bind(this)}> × </button> <input type="checkbox" readOnly checked={!!this.props.task.checked} onClick={this.toggleChecked.bind(this)} /> <span className="text"> <strong>{this.props.task.username}</strong>: {this.props.task.text} </span> </li> ); } } | cs |
그리고 화면을 확인합니다.
모든 인풋과 버튼이 동작함을 확인할 수 있습니다.
위와 같이 수정함으로써 클라이언트 코드와 DB코드가 분리되는 장점을 추가로 얻게 됩니다.
2. 발행과 구독
자동으로 DB에서 모든 데이터를 가져오는걸 막기 위해 autopublish 패키지를 제거합니다.
$ meteor remove autopublisk
이제 자동으로 서버측 몽고디비에서 데이터를 가져오지 않습니다.
Publish/Subscribe 메서드를 구현합니다.
/imports/api/tasks.js
1 2 3 4 5 6 7 8 9 10 11 12 | ... export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server Meteor.publish('tasks', function tasksPublication() { return Tasks.find(); }); } Meteor.methods({ ... | cs |
/imports/ui/App.jsx
1 2 3 4 5 6 7 8 9 | ... export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user(), }; })(App); | cs |
이제 다시 작업 리스트가 보입니다.
서버에서 Meteor.publish를 호출하면 tasks라는 이름으로 발행을 진행합니다.
이후 클라이언트에서 Meteor.subscribe를 통해 발행된 정보를 가져옵니다.
3. 비공개 작업 추가하기.
css 클래스의 이름별로 다른 설정을 하기 위해 classnames 모듈을 설치합니다.
$ meteor npm install --save classnames
공개/비공개 버튼을 추가하고 비공개일 경우 작업을 추가한 유저가 아니면 보이지 않도록 하는 코드를 추가합니다.
/imports/api/tasks.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import { Mongo } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; export const Tasks = new Mongo.Collection('tasks'); if (Meteor.isServer) { // This code only runs on the server // Only publish tasks that are public or belong to the current user Meteor.publish('tasks', function tasksPublication() { return Tasks.find({ $or: [ { private: { $ne: true } }, { owner: this.userId }, ], }); }); } Meteor.methods({ 'tasks.insert'(text) { check(text, String); // Make sure the user is logged in before inserting a task if (!this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.insert({ text, createdAt: new Date(), owner: this.userId, username: Meteor.users.findOne(this.userId).username, }); }, 'tasks.remove'(taskId) { check(taskId, String); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.userId) { // If the task is private, make sure only the owner can delete it throw new Meteor.Error('not-authorized'); } Tasks.remove(taskId); }, 'tasks.setChecked'(taskId, setChecked) { check(taskId, String); check(setChecked, Boolean); const task = Tasks.findOne(taskId); if (task.private && task.owner !== this.userId) { // If the task is private, make sure only the owner can check it off throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { checked: setChecked } }); }, 'tasks.setPrivate'(taskId, setToPrivate) { check(taskId, String); check(setToPrivate, Boolean); const task = Tasks.findOne(taskId); // Make sure only the task owner can make a task private if (task.owner !== this.userId) { throw new Meteor.Error('not-authorized'); } Tasks.update(taskId, { $set: { private: setToPrivate } }); }, }); | cs |
/imports/ui/App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | import React, { Component } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Tasks } from '../api/tasks.js'; import ReactDOM from 'react-dom'; import Task from './Task'; import AccountsUIWrapper from './AccountsUIWrapper.jsx'; import { Meteor } from 'meteor/meteor'; // App component - represents the whole app class App extends Component { constructor(props) { super(props); this.state = { hideCompleted: false, }; } handleSubmit(event) { event.preventDefault(); // Find the text field via the React ref const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim(); Meteor.call('tasks.insert', text); // Clear form ReactDOM.findDOMNode(this.refs.textInput).value = ''; } renderTasks() { let filteredTasks = this.props.tasks; if (this.state.hideCompleted) { filteredTasks = filteredTasks.filter(task => !task.checked); } return filteredTasks.map((task) => { const currentUserId = this.props.currentUser && this.props.currentUser._id; const showPrivateButton = task.owner === currentUserId; return ( <Task key={task._id} task={task} showPrivateButton={showPrivateButton} /> ); }); } toggleHideCompleted() { this.setState({ hideCompleted: !this.state.hideCompleted, }); } render() { return ( <div className="container"> <header> <h1>Todo List ({this.props.incompleteCount})</h1> <label className="hide-completed"> <input type="checkbox" readOnly checked={this.state.hideCompleted} onClick={this.toggleHideCompleted.bind(this)} /> Hide Completed Tasks </label> <AccountsUIWrapper /> { this.props.currentUser ? <form className="new-task" onSubmit={this.handleSubmit.bind(this)} > <input type="text" ref="textInput" placeholder="Type to add new tasks" /> </form> : '' } </header> <ul> {this.renderTasks()} </ul> </div> ); } } export default withTracker(() => { Meteor.subscribe('tasks'); return { tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(), incompleteCount: Tasks.find({ checked: { $ne: true } }).count(), currentUser: Meteor.user(), }; })(App); | cs |
/imports/ui/Task.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import React, { Component } from 'react'; import { Meteor } from 'meteor/meteor'; import classnames from 'classnames'; // Task component - represents a single todo item export default class Task extends Component { toggleChecked() { // Set the checked property to the opposite of its current value Meteor.call('tasks.setChecked', this.props.task._id, !this.props.task.checked); } deleteThisTask() { Meteor.call('tasks.remove', this.props.task._id); } togglePrivate() { Meteor.call('tasks.setPrivate', this.props.task._id, !this.props.task.private); } render() { // Give tasks a different className when they are checked off, // so that we can style them nicely in CSS const taskClassName = classnames({ checked: this.props.task.checked, private: this.props.task.private, }); return ( <li className={taskClassName}> <button className="delete" onClick={this.deleteThisTask.bind(this)}> × </button> <input type="checkbox" readOnly checked={!!this.props.task.checked} onClick={this.toggleChecked.bind(this)} /> {this.props.showPrivateButton ? ( <button className="toggle-private" onClick={this.togglePrivate.bind(this)}> {this.props.task.private ? 'Private' : 'Public'} </button> ) : ''} <span className="text"> <strong>{this.props.task.username}</strong>: {this.props.task.text} </span> </li> ); } } | cs |
이제 화면을 보면 Private 로 작업을 변경할 수 있습니다.
Private작업은 로그아웃 하면 보이지 않습니다.
'Programming > JavaScript' 카테고리의 다른 글
NodeJS - Webpack 소개 (0) | 2019.04.15 |
---|---|
NodeJS - Meteor with React Tutorial (5) (0) | 2019.02.19 |
NodeJS - Meteor with React Tutorial (3) (0) | 2019.02.18 |
NodeJS - Meteor with React Tutorial (2) (0) | 2019.02.18 |
NodeJS - Meteor with React Tutorial (1) (0) | 2019.02.18 |