-
320x100
이 글은 Vue.js 중급 강좌 - 웹앱 제작으로 배워보는 Vue.js, ES6, Vuex 강의를 바탕으로 작성한 글입니다.
멋진 강의 감사합니다 (__)
시리즈: Todo 웹앱 만들기(2)7. UX 업그레이드
이제부터는 꼭 필요한 기능은 아니지만, 있으면 사용자 경험을 개선하는 데 도움을 주는 기능을 개발합니다.
이름 저장하기
저는 서비스의 트렌드 요소 중 하나는 '개인 맞춤'이라고 생각합니다. 로그인 한 사용자에게 맞는 홈 화면을 보여주는 네이버처럼요.
그래서 전 이름을 불러주는 서비스를 좋아합니다. (나에게로 와서 꽃이 되었다... 🌸)
Momentum 서비스와 비슷하게, 처음 만난 사용자에게 이름을 묻고 저장하는 과정을 추가해 보겠습니다.
없던 기능이 추가된 거라 개발이 좀 꼬였는데 실무라 생각하고 진행해 보죠 ヽ(๏∀◕ )ノ 에헤라디야
우선 사용자의 이름을 묻는 UI가 필요합니다.
이걸 별도의 페이지로 만들지, 텍스트를 수정하는 걸로 할지, 클래스로 관리할지 고민이 됐습니다.
이 UI는 사용자가 대부분의 경우 한 번만 볼 것이기 때문에, 별도 컴포넌트로 만든 뒤 조건에 따라 보임/숨김 처리를 하기로 했습니다.
v-if
디렉티브를 쓰면 조건부 렌더링이 가능합니다. 조건은 userName이라는 값이 있는지를 기준으로 하기로 했습니다.userName을 getItem()으로 가져오되, 그 값이 없다면 null 곧 false이므로 이름을 물어보는 <TodoHello/>가 표시됩니다.
<!-- App.vue --><template><div id="app"><TodoHeader /><div v-if="userName"><TodoTitle /><TodoInput /></div><div v-else><TodoHello /></div></div></template><script>export default {data() {return {userName: ''};},created() {this.userName = localStorage.getItem("userName");if (localStorage.length > 0) {for (let i = 0; i < localStorage.length; i++) {if (localStorage.key(i) !== "userName") {this.todoItems.push(JSON.parse(localStorage.getItem(localStorage.key(i))));}}}},//...}</script>TodoHello에서는 사용자 이름을 물어봅니다.
<!-- TodoHello.vue --><template><div><div class="hello">What is your name?</div><label for="user-name">Name</label><input type="text" id="user-name" v-model="userName" v-on:keyup.enter="addUserName" /></div></template><script>export default {data() {return {userName: ""};},methods: {addUserName() {this.$emit("addName", this.userName);}}};</script>App.vue는 이렇게 올라온 값을 userName에 넣고, prop으로 다시 내려줍니다.
TodoTitle.vue가 여러 개의 prop을 받기 때문에 기존의
propsdata
에서propCount
와propName
으로 이름을 바꿨습니다.<!-- App.vue --><template><div id="app"><div v-if="userName"><TodoHeader /><TodoTitle v-bind:propCount="checkCount" v-bind:propName="userName" /><TodoInput v-on:addItem="addOneItem" /></div><div v-else><TodoHello v-on:addName="addUserName" /></div></div></template><script>//...export default {data() {return {todoItems: [],userName: ""};},methods: {addUserName(userName) {localStorage.setItem("userName", userName);this.userName = userName;},//...},//...};</script>이름 수정하기
간단한 기능인데도 헤맨 부분이네요@_@
이름 부분은 일반
<span>
태그로 표시하되,contenteditable
속성으로 편집 가능하게 처리했습니다.CSS 스타일링을 통해 사용자가 현재 입력 중임을 알 수 있도록 했고요.
엔터키를 누르거나 포커싱이 없어지면 바로 $emit을 통해 데이터를 전달하게 했습니다.
<!-- TodoTitle.vue --><template><div class="title"><p class="title__text"><span class="title__message">{{ message }},</span><spanv-on:keyup.enter="handleName"v-on:blur="handleBlur"class="title__name"ref="test"contenteditable="true">{{ propName }}</span></p><!-- ... --></div></template><script>export default {props: ["propCount", "propName"],data() {return {message: "Good morning"};},methods: {handleBlur(e) {const originalName = this.propName;const newName = e.target.innerText;if (newName !== originalName) {if (newName === "") {e.target.innerText = originalName;} else {this.$emit("changeName", newName);}}},handleName() {this.$refs.test.blur();}}};</script>시간대에 따른 인사말 보여주기
아침/점심/저녁에 따라 다른 인사를 합니다.
공통함수인
getDate()
에 시간대를 구하는 코드를 추가했어요.<!-- getDate.js -->export default () => {const now = new Date();//....const hour = now.getHours();let daytime = "";if (hour < 12) {daytime = 'morning';} else if (hour < 18) {daytime = 'afternoon';} else {daytime = 'evening';}const dateInfo = {month,date,week,time,daytime}return dateInfo}<!-- TodoTitle.vue --><script>import getDate from "../assets/commonJS/getDate.js";export default {props: ["propCount", "propName"],data() {return {message: ""};},methods: {//...mounted() {this.message = getDate().daytime;}};</script>빈 화면일 때 처리
이번에는 빈 화면일 때
list--empty
라는 클래스가 붙도록 설정합니다.로컬 스토리지는 userName이 들어있기 때문에, todoItems.length가 0 이하일 때를 체크하도록 했습니다.
<!-- App.vue --><template><div id="app"><TodoListv-bind:propEmpty="isEmpty"/></div></template><script>export default {data() {return {todoItems: [],userName: ""};},computed: {isEmpty() {return this.todoItems.length <= 0 ? true : false;}//...},//...};</script><!-- TodoList.vue --><template><ul class="list" v-bind:class="listempty"><!-- ... --></ul></template><script>export default {computed: {listempty() {return this.propEmpty ? "list--empty" : null;}},props: ["propItems", "propEmpty"],//...};</script>그나저나 이름 짓는 게 진짜... 어렵네요...... ໒( •́ ∧ •̀ )
모달 창 띄우기
common 폴더에 잠들어 있던 Modal.vue를 써먹을 시간입니다!
모달은 크게 타이틀+텍스트+닫기 버튼으로 이루어져 있습니다. 타이틀과 텍스트는 각 상황 별로 다른 문구를 출력하게 만들 거에요.
이때
slot
이라는 걸 유용하게 사용할 수 있습니다. 이는 컴포넌트의 재사용성을 높여줍니다. (공식 문서 링크)여기서는
v-slot
으로 모달에 텍스트를 넣었습니다.<!-- Modal.vue --><template><div class="modal"><slot name="modal-text">(여기에 모달 내용이 들어가요)</slot></div></template><!-- App.vue --><template><div id="app"><Modal><template v-slot:modal-text>중복되는 할 일이 있어요!</template></Modal></div></template><script>import Modal from "./components/common/Modal";export default {components: {Modal}}</script>showModal 값으로 모달이 나타나고 숨겨지도록 합니다.
모달은 사용자 이름을 묻는 <TodoHello/>보다 자주 토글될 것으로 생각되어
v-show
디렉티브를 사용했습니다.<!-- App.vue --><template><div id="app"><Modal v-show="showModal" v-on:close="showModal = false"><template v-slot:modal-text>{{ modalText }}</template></Modal></div></template><script>import Modal from "./components/common/Modal";export default {data() {return {showModal: false,modalText: ""};},methods: {addOneItem(todoItem) {// 빈 내용인 경우if (todoItem === "") {this.showModal = !this.showModal;this.modalText = "The form is empty. Please enter your task.";return false;}// 중복되는 내용인 경우for (let i = 0; i < this.todoItems.length; i++) {if (this.todoItems[i].item === todoItem) {this.showModal = !this.showModal;this.modalText = "I think you've already had the task.";return false;}}// 저장할 정보var value = {item: todoItem,date: `${getDate().date} ${getDate().week}`,time: getDate().time,completed: false};localStorage.setItem(todoItem, JSON.stringify(value));this.todoItems.push(value);},//...},components: {Modal}}</script>확인 버튼, 검은 배경 부분을 클릭했을 때 모달이 닫히도록 설정합니다.
<!-- Modal.vue --><template><div class="modal modal__dim" v-on:click.self="$emit('close')"><div class="modal__content"><p class="modal__text"><slot name="modal-text">(여기에 모달 내용이 들어가요)</slot></p><button class="modal__close" v-on:click="$emit('close')">OK</button></div></div></template>추가로 한글 입력 시 메서드가 두 번 실행되는 문제가 있었는데, 아래처럼
keypress.enter
로 수정해 해결했습니다.<!-- TodoInput.vue --><template><div class="add"><inputtype="text"class="add__input"placeholder="Enter your task here"v-model="newTodoItem"v-on:keypress.enter="addTodoItem"/></div></template>이제 모달도 잘 뜹니다 ;)
트랜지션 추가
vue의 장점 중 하나는 멋진 트랜지션을 제공한다는 점입니다.
v-enter
나v-leave
등의 클래스에 트랜지션 내용을 적어두면 때에 맞춰 실행됩니다.이들 클래스에는 트랜지션의 이름이 접두어로 붙습니다. 예를 들어 트랜지션 이름이 hello라면 클래스 이름은
hello-enter
나hello-leave
가 됩니다.모달이 바로 뿅하고 나타나는 대신 트랜지션을 추가해 보겠습니다.
배경이 서서히 검은 색으로 바뀌고 창은 아래에서 위로 스윽 올라오도록 클래스를 만듭니다.
노드의 초기 렌더에 트랜지션을 적용하고 싶다면
appear
속성을 추가 할 수 있습니다.<template><transition name="modal" appear><div class="modal modal__dim" v-on:click.self="$emit('close')"><div class="modal__content"><p class="modal__text"><slot name="modal-text">(여기에 모달 내용이 들어가요)</slot></p><button class="modal__close" v-on:click="$emit('close')">OK</button></div></div></transition></template><style lang="scss">.modal-enter {opacity: 0;.modal__content {transform: translateY(30px);transition: transform 0.6s;}}.modal-enter-active {transition: opacity 0.3s;}.modal-leave-active {transition: opacity 0.3s 0.05s;}.modal-leave-to {opacity: 0;.modal__content {transform: translateY(30px);transition: transform 0.6s;}}</style>이 트랜지션을 사용하면 꽤 복잡한 응용이 가능해 보입니다.
좀 더 깊게 연구해봐도 좋을 것 같아요 🙋♀️
이번에는 할 일 목록 차례입니다.
할 일 목록을 입력하면 왼쪽 화면에서 스르르 나타나고, 삭제하면 오른쪽 화면으로 스르르 사라지게 해봅니다.
이때 이 리스트는 한 번에 하나만 렌더링 되지 않고 여러 개가 동시에 렌더링될 수 있습니다.
이 경우에는
<transition-group>
컴포넌트를 사용합니다.<transition-group name="list" tag="p"><span v-for="item in items" v-bind:key="item" class="list-item">{{ item }}</span></transition-group><!-- TodoList.vue --><template><transition-group name="list" tag="ul" class="list" v-bind:class="listempty"><li class="list__item" v-for="(todoItem, index) in propItems" v-bind:key="todoItem.item"><!-- ... --></li></transition-group></template><style lang="scss">// ==== 트랜지션.list-enter {opacity: 0;transform: translateX(-30px);}.list-enter-active,.list-leave-active {transition: transform 0.5s, opacity 0.5s;}.list-leave-to {opacity: 0;transform: translateX(30px);}</style>
8. 완성!
드디어 구현 완료! 👏👏👏
다른 브라우저에서도 예쁘게 잘 나와요 :D
728x90나나 (nykim)쉽고, 재밌고, 특별한 걸 좋아해요. 걷고 뛰고 구르면서 나아가는 중.
'Blog > Library' 카테고리의 다른 글
[ReactJS] 1. 시작하기 (7) 2022.02.01 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (4) (2) 2020.06.03 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (2) (1) 2020.05.28 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (1) (5) 2020.05.26 [Vue] 기초 스터디 (2) 2020.05.21