해리의 데브로그

Vue.js 01 - 기본 개념

|

Vue.js 기본개념

1. 싱글 페이지 어플리케이션 (SPA)

싱글 페이지 어플리케이션(single-page application, SPA)란 서버로부터 완전한 새로운 페이지를 불러오지 않고 현재의 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 어플리케이션이나 웹 사이트를 일컫는 말임.

단일 페이지 어플리케이션(SPA)는 현재 웹 개발의 트렌드임.

기존 웹 서비스는 요청시마다 서버로 부터 리소스들과 데이터를 해석하고 화면에 렌더링 하는 방식이었지만, SPA 형태는 브라우저에 최초에 한번 페이지 전체를 로드하고, 이후부터는 특정 부분만 Ajax를 통해 데이터를 바인딩 하는 방식임.

2. Vue.js 시작하기

Vue.js는 웹 개발을 단순화하고 정리하기 위해 개발된 대중적인 자바스크립트 프론트엔드 프레임워크이다.

1) 시작하기

  • head 태그 내 Vue.js CDN를 사용하기 위한 코드 삽입
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">	   
    </script>
</head>

2) 기본 양식 - 선언적 렌더링

“Vue.js의 핵심은 간단한 템플릿 구문을 사용해 선언적으로 DOM에 데이터를 렌더링 하는 것임”

  • vue를 통해 JS 코드를 작성하기 위해서는 vue 인스턴스를 만드는 것부터 시작함.
  • body 태그 내 element를 하나 생성(예; div)하고, 그에 대응하는 vue 인스턴스를 생성하는데 시작함. el 에는 동기화하는 element의 선택자를 입력함.
  • body 태그 내에서는 장고 문법과 동일한 방법으로 data 내 key를 통해 value값을 불러 올 수 있음.
  • vue 객체를 생성할 때는 아래와 같은 options을 포함 시킬 수 있음.
    • el : “어디에” 에 대한 개념. 연결시킬 DOM(element)의 선택자를 넣음 (예, id, class)
    • data : “무엇을” 보여줄건지를 정의
    • methods: “어떻게” 보여줄건지를 정의. 데이터를 조작하는 함수를 정의. 하기 예시의 경우 plus 함수를 실행하면 count 값이 증가됨.
  • thisdata 내 정보를 내부적으로 불러올 때 사용. 예) this.count
<body>
    <div id="app">
         - 8
    </div>
    <script>
        const app = new Vue({
            el: '#app', // 어디에
            data: {
                message: 'hello, Vue',
                count: 0
            },
            methods: {
                plus: function(){
                    this.count++
                }
            }
        })
    </script>    
</body>

3) Vue 인스턴스 내 변수 접근 방법

  • 어트리뷰트를 불러올 때는 $ 를 붙어야함 예) app.$data / app.$el / app.$methods
  • data 내 정보를 불러올 때는 app.$data.message와 같이 입력해야 하나, Vue에서는 app.message 처럼 다이렉트로 접근 가능하도록 편의성을 제공함.

4) 조건문 v-if & 반복문 v-for

  • 조건문 예시
    • v-if 안의 내용에는 자바스크립트 코드가 들어감
    • data 내 count 값을 변경하여 조건문 적용 유무를 확인 해 볼 수 있음.
<body>
    <div id="app">
         - 8
        <p v-if="count > 1"> 이건 보임 </p>
    </div>
...중략
  • 반복문 예시 (1)
<div id="app">
    <!-- 중략 -->
	<li v-for="post in posts">
        
    </li>
</div>
<script>
    const app = new Vue({
        	//...중략
        data: 
			//...중략
            posts: ['첫 게시물', '두번째 게시물', '세번째!']
        },
			//...중략
    })
</script>
  • 반복문 예시 (2) -딕셔너리
    • Vue 에서는 딕셔너리를 기본적으로 반복문을 돌릴 경우, 딕셔너리의 value 값이 출력 됨.
    • key 값을 출력하기 위해서는 Object.keys() 를 사용해야함.
    • key와 value를 둘다 출력 할 수 도 있음.
<div id="app">
    <li v-for="info in students">
    	
    </li>
    <li v-for="info in Object.keys(students)">
    	
    </li>
    <li v-for="info in Object.keys(students)">
         - 
    </li>
</div>
<script>
    const app = new Vue({
        data: {
            students: {
                name: 'Harry',
                uni: 'UVIC',
            }
        },
    })
</script>

5) MVC vs MVVM

  • Django의 MTV(MVC) pattern 처럼, Vue.js 또한 유사한 패턴을 갖고 있음.
  • Django내 model과 View 사이에서 controller의 역할을 했던 views.py 처럼, Vue가 Model과 View 사이를 데이터 바인딩을 통해 controller의 역할을 함. 여기서는 controller 대신 View-Model의 앞글자를 따서 VM이라고 함.
Pattern M V C / VM
Django Model View(Template.html) Controller(views.py)
Vue.js Model View(HTML) View-Model(Vue)

JavaScript 06 - Django 내에서 JS 적용하기 (2) (댓글 생성 기능 동적 구현)

|

Django 내에서 JS 적용하기 (2) (댓글 생성 & 리스트 기능 동적 구현)

Django 인스타 프로젝트에서 좋아요 기능을 동적으로 구현했던 것 처럼, 댓글 기능 또한 동적으로 구현해 보도록 하자.

1) 기존 코드

  • _post.html 내 form 태그로 댓글 생성 코드가 작성되어있음.
{% if user.is_authenticated %}
<form action="{% raw %}{% url 'posts:comment_create' post.id %}" method="POST">
    {% raw %}{% csrf_token %}
    {% raw %}{{ comment_form }}
    <input type="submit", value="submit">
</form>
{% raw %}{% endif %}

2) _post.html 수정

  • 댓글 작성 폼 태그 수정
    • 자바스크립트에서 이 태그를 불러 올 용도로 클래스(class="comment-form") 추가
{% if user.is_authenticated %}
<form action="{% raw %}{% url 'posts:comment_create' post.id %}" method="POST" class="comment-form">
    {% raw %}{% csrf_token %}
    {% raw %}{{ comment_form }}
    <input type="submit", value="submit">
</form>
{% raw %}{% endif %}

3) list.html

  • _post.html 내 폼태그를 불러옴
  • document.querySelectorAll("comment-form") 을 통해 모든 포스트의 댓글들을 갖고옴.
  • addEventListener 의 첫번째 인자로 submit 입력. submit 은 폼태그에서 submit을 누를 때 이벤트가 발생하게 설계되어있는 트리거이며, 이 트리거는 폼태그만 들고 있음.
  • 아래의 코드를 통해 댓글 작성 후 submit을 누를 경우, 2가지의 이벤트가 작동함.
    • 기존의 이벤트(폼태그가 하는 행동. post 요청을 통해 action으로 url주소를 넘김)
    • addEventListener 통해 console.log 이벤트 작동
  • 우리는 페이지를 새로고치지 않고 동적으로 변경되게 하고 싶으므로 기존의 이벤트를 막아야함 => preventDefault 사용 (기존의 이벤트는 막히게 됨)
  • preventDefault 로 막지 않을 경우, 댓글 생성 버튼을 눌렀을때, MTV 패턴에 따라 함수가 실행되고 페이지가 새로고침되어 console창에 출력되는 event 객체를 확인할 수가 없음!
const commentForms = document.querySelectorAll('.comment-form')
commentForms.forEach(function(form){
    form.addEventListener('submit', function(event){
        event.preventDefault()
        console.log(event)
    })
})

4) list.html (2)

  • 댓글의 주소 뿐만 아니라 텍스트(생성된 댓글)도 갖고와야함.
  • event.target 에는 이벤트를 전달한 객체(이벤트가 시작된 DOM 요소)가 담겨 있음 (form 태그 안의 모든 정보가 담겨있음)
  • FormData 검증하기
    • FormData 인터페이스는 form필드와 그 값을 나타내는 일련의 key/value 쌍을 쉽게 생성할 수 있는 방법을 제공함 (폼에 글을 작성하고, html에서 submit 버튼을 눌렀을 때, 보내게 되는 요청을 객체에 실어서 던지게 되는데, 그게 담겨있는 바구니의 개념)
    • FormData 는 일종의 클래스로, 클래스로부터 인스턴스를 만들 때는 new 키워드를 붙여 줘야함.
    • FormData.entries() : 객체가 담긴 모든 key/value 쌍을 순회할 수 있는 iterator 을 반환함.
  • event.target.action 에는 댓글 생성에 대한 url 정보가 담겨있음.
  • axios.post의 두번째 인자로, Formdata를 통해갈무리된 객체인 data를 넘겨줌.
  • response 에는 html 문서 전체가 담겨있음. 댓글 관련 정보만 갖고오면 되므로, Django가 하는 응답을 수정해야함.
const commentForms = document.querySelectorAll('.comment-form')
commentForms.forEach(function(form){
    form.addEventListener('submit', function(event){
        event.preventDefault()
        
        const data = new FormData(event.target)
        
        //Inspect FormData
        //for (const item of data.entries()){
        //		console.log(item)
        //}
        
        axios.post(event.target.action, data)
        	.then(function(response){
            	console.log(response)
        })
    })
})

4-1) FormData.entries()

  • 위에서 FormData.entries()에는 form 태그를 통해 전송되는 값을 나타내는 key/value 쌍을 순회할 수 있는 iterator을 반환한다는 것을 알 수있음.
  • 위의 코드 박스 내 “Inspect FormData” 내 반복문 코드를 입력한 후, 댓글을 생성하고 console창에 찍힌 로그를 보면 아래와 같이 출력됨을 알수 있음
  • Form 태그를 통해 POST방식으로 요청을 보낼 때는 아래와 같이 2가지의 정보가 전송됨 을 알 수 있음.
    • CSRF_TOKEN & 댓글 내용(content)
(2) ["csrfmiddlewaretoken", "AuGAXE7Jio1BxQePvUfC85eDDcYU0OtSD2hF2EhvCVhrLsPxzNi6xfnMUz3K17Ll"]
	0: "csrfmiddlewaretoken"
	1: "AuGAXE7Jio1BxQePvUfC85eDDcYU0OtSD2hF2EhvCVhrLsPxzNi6xfnMUz3K17Ll"
	length: 2
	__proto__: Array(0)

(2) ["content", "댓글입니다"]
    0: "content"
    1: "댓글입니다"
    length: 2
    __proto__: Array(0)

5) views.py

  • 좋아요 기능 동적 구현때와 마찬가지로 from django.http import JsonResponse 입력 후 코드 수정
  • 댓글 생성 함수의 return 값을 JsonResponse로 변경. JsonResponse 에는 다음과 같은 정보를 저장하여 넘길 필요가 있음.
    • comment.id : 댓글의 id
    • post_id : 게시글의 id (어떠한 글에서 댓글이 생성되었는지 알아야 함)
    • comment.user.username : 댓글 작성자
    • comment.content : 댓글 내용
from django.http import JsonResponse

def comment_create(request, post_id):
    comment_form = CommentForm(request.POST)
    if comment_form.is_valid():
        comment = comment_form.save(commit=False)
        comment.user = request.user
        comment.post_id = post_id
        comment.save()
        
    # return redirect('posts:list')
    return JsonResponse({
                            'id': comment.id,
                            'postId':post_id,
                            'username':comment.user.username,
                            'content':comment.content,
                        })

6) _post.html (2)

아직까지는 어떤 포스트에 대한 댓글인지 자바스크립트는 알 수 없음. <ul> 는 댓글들이 존재하는, 댓글들을 보여주는 태그이며 이곳에 id값을 부여 해줘야함.

  • <ul id="comment-list-{{ post.id }}"> 를 기반으로 댓글이 달릴 위치를 갖고오고, 새로운 댓글을 붙여주면 됨.
<ul id="comment-list-{{ post.id }}">
{% for comment in post.comment_set.all %}
    <li>
    <strong>{{ comment.user }}</strong> | {{ comment.content }}
    {% if comment.user == user %}
        <a href="{% url 'posts:comment_delete' post.id comment.id %}">댓글 삭제</a>
        <a href="{% url 'posts:comment_update' post.id comment.id %}">댓글 수정</a>
    {% endif %}
    </li>
{% endfor %}
</ul>

7) list.html (3)

  • response.data 에는 댓글 생성 함수(comment_create)를 통해 return 되는 JsonResponse 오브젝트가 담겨있음. 이를 comment 변수에 저장
  • document.querySelector('#comment-list-${comment.postId}')을 통해 어떠한 게시글에 댓글이 달린건지를 확인 후, 그 게시글에 담긴 댓글 전체 리스트를 갖고 옴. 후, commentList 변수에 저장. comment.postId 를 통해 post_id에 접근 가능 (JsonResponse 값 참조)
  • 새 댓글을 표시하는 부분을 코드로 작성하여 newComment 변수에 저장시킴. 이때 변수의 값으로는 _post.html 에서 작성하였던 <li> 태그 이하 전체 코드를 갖고옴.
  • 자바스크립트에서는 장고 템플릿을 사용할 수 없으므로 수정 요
    • {{ comment.content }}와 같이 사용했던 장고 템플릿은 comment 변수에 저장된 JsonResponse 오브젝트를 사용하여 변경
    • {{ comment.user }} => ${comment.username}
    • {{ comment.content }} => ${comment.content}
    • url 주소는 하드하게 수정
    • {% if comment.user == user %} & {% endif %} 조건문은 필요 없음 (댓글을 쓴사람이 당연히 나이므로, 삭제도 당연히 가능)
  • 진자 템플릿을 붙일 때는 insertAdjacentHTML 사용 (2개의 인자를 가짐 - 위치, 내용)
  • 댓글 작성 후, 작성한 댓글 내용이 input 태그 양식 안에 남아있으므로, 생성 버튼 후 내용이 사라지게 하기 위해서는 폼 태그 그 자체를 의미하는 event.target 을 초기화 해야함. => event.target.reset()
const commentForms = document.querySelectorAll('.comment-form')
commentForms.forEach(function(form){
    form.addEventListener('submit', function(event){
        event.preventDefault()
        
        const data = new FormData(event.target)

        axios.post(event.target.action, data)
        	.then(function(response){
            	const comment = response.data
                const commentList = document.querySelector(
                    `#comment-list-${comment.postId}`)
                const newComment = `<li>
				<strong>${ comment.username }</strong> | ${ comment.content}
				<a href="/posts/${comment.postId}/comments/${comment.id}/delete/">
삭제</a>
				<a href="/posts/${comment.postId}/comments/${comment.id}/update/">
수정</a>
    </li>`
                commentList.insertAdjacentHTML('beforeEnd', newComment)
            	event.target.reset()
        })
    })
})

7-1) insertAdjacentHTML 구문

element.insertAdjacentHTML(position, text);

position은 아래 있는 단어만 사용 가능하다.

  • 'beforebegin' : element 앞에
  • 'afterbegin' : element 안에 가장 첫번째 child
  • 'beforeend' : element 안에 가장 마지막 child
  • 'afterend' : element 뒤에
text(인자)는 HTML 또는 XML로 해석될 수 있는 문자열이고(html code), (DOM) tree에 삽입할 수 있다.

※비동기식으로 동적으로 만들 때는, 하나의 댓글을 뒤에 갖다붙이기보다는 전체 댓글을 갖고와서 뒤에 쓰는 것을 선호함.

JavaScript 05 - Django 내에서 JS 적용하기 (좋아요 기능 동적 구현)

|

Django 내에서 JS 적용하기 (좋아요 기능 동적 구현)

Django 카테고리 내 인스타그램 기반의 프로젝트 포스팅을 통해 좋아요 기능을 구현해보았다. 이 때, 구현한 기능은 MTV 패턴에 따라서, url 경로를 요청 보내면, views.py 내 함수에 따라 return 되는 값을 template으로 보여주는 방식으로 구현 되었는데, 페이지를 새로고침을 해야 동작이 되었음.

이제부터, 페이지 redirect 없이 JS를 이용하여 좋아요 기능이 동적으로 동작하도록 코드를 수정해보자.

1. 좋아요 기능 구현

1) 기존 코드

  • _post.html 내 좋아요 기능이 아래와 같이 구현되어있음.
  • base.html 내 head 태그 내 axios CDN 추가
  • 좋아요 버튼을 자바스크립트 요청으로 변경해보도록 하자.
    <a href="{% url 'posts:like' post.id %}">
        {% if user in post.like_users.all %}
        <!-- 좋아요 취소 -->
        <i class="fas fa-heart"></i>
        {% else %}
        <!-- 좋아요 -->
        <i class="far fa-heart"></i>
        {% endif %}
    </a>

2) _post.html 수정

  • a 태그 삭제 -> 자바스크립트로 요청을 보내므로 필요 없음
  • 조건문을 i 태그 클래스 안에 삽입
  • 어떠한 포스트에 대한 버튼인지 하트에 정보를 더 추가해야함 (고유 클래스 부여)
    • like-button 클래스 속성(자바스크립트에서 클래스를 찾기 위한 용도)
    • 자바스크립트는 data-id를 통해 개별적인 오브젝트에 접근 가능
  • data-idHTMLElement.dataset 속성을 활용한 것으로 HTML 이나 DOM 요소의 커스텀 데이터 속성(data-*)에 대한 읽기와 쓰기 접근을 허용시킴.
<i class="{% if user in post.like_users.all %}
          fas 
          {% else %}
          far
          {% endif} fa-heart
          like-button" data-id="{{post.id}}">
</i>

<!-- 좋아요 취소할 경우, fas fa-heart 클래스가 적용
	 좋아요 경우, far fa-heart 클래스가 적용 -->

3) list.html 내 JS 코드 작성

_post.html은 아래와 같이 list.html의 for문안에 있으므로, 그 안에서 코드를 작성할 경우, 똑같은 스크립트가 계속 생김. 따라서 자바스크립트 코드는 list.html에서 작성함

{% for post in posts %}
    {% include 'posts/_post.htm' %}
{% endfor %}
  • 버튼이 한개만 있는 것이 아니므로 (각 post 별로 좋아요 버튼이 있음), 모든 버튼을 다 가지고 와야함. document.querySelectorAll 을 이용하여 모든 데이터를 가져오며, 반복문을 통해 개별적인 버튼에 대한 addEventListentListener 구현.
  • 이때 querySelectorAll의 인자로 i 태그 클래스 내에서 정의한 클래스명인 like-button를 사용
  • event 오브젝트 객체 안, 정확히는 event.target.dataset.id의 value에는 data-id의 값인 “{{post.id}}이 저장되어있으며, 이를 좋아요 기능에 대한 url 주소를 불러올 variable routing값으로 쓰임.
  • urlpattern에 따라 좋아요의 url 주소는 <int:post_id>/like/ 와 같이 정의 되어 있으며, 이 주소를 axios.get의 인자로 넘김.
  • 요청에 대한 응답(.then 메서드)으로, response 객체의 반환 값으로 views.py를 통해 redirect된 list.html 전체가 반환됨. 우리는 “좋아요” 기능만 구현하면 되므로, HTML 문서 전체를 반환할 필요가 없음. 이는 불필요한 응답이므로 views.py 수정이 필요함
<script>
    const likebuttons = document.querySelectorAll('.like-button')
    likebuttons.forEach(function(button){
        button.addEventListener('click', function(event){
            // console.log(event)
            const postId = event.target.dataset.id
            axios.get(`/posts/${postId}/like/`)
            		.then(function(response){
                		console.log(response)
            		})
        })
    })
</script>

4) views.py 수정

  • 기존의 return 값이었던 redirect('posts:list') 를 Json의 형태로 반환해줌. 사용을 위해 JsonResponse를 import 해줌
  • 좋아요를 취소하는 경우, liked 변수는 False로, 반대의 경우(좋아요를 누르는 경우)는 liked 변수에 True 값을 적용
from django.http import JsonResponse

@login_required
def like(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    
    if request.user in post.like_users.all():
        post.like_users.remove(request.user)
        liked = False
    else:
        post.like_users.add(request.user)
        liked = True
        
    #return redirect('posts:list')
    return JsonResponse({'liked':liked})

5) list.html 내 JS 코드 추가 작성

  • response.data 에는 views.py 내 like 함수를 통해 반환되는 Json 오브젝트가 저장되어 있으며, response.data.liked 를 통해 오브젝트의 key에 접근이 가능함.
  • event.target.classList 에는 해당 클래스 내 데이터가 리스트와 같은 형태로 모두 저장되어 있음. remove 와 add를 통해 클래스 속성을 조작 할 수 있음.

classList: DOMTokenList(3) 0: “fa-heart” 1: “like-button” 2: “fas” length: 3 value: “fa-heart like-button fas”

  • if (response.data.liked), 즉 liked 값이 True 라면, (좋아요 기능), 빈 하트를 의미하는 i 태그의 class 값인 fas fa-heart 에서 fas 를 제거한 후 far 를 추가 => 빈 하트에서 꽉찬 하트로 변경
  • 좋아요를 취소하는 경우는 반대로 생각하면 됨.
<script>
    const likebuttons = document.querySelectorAll('.likebutton')
    likebuttons.forEach(function(button){
        button.addEventListener('click', function(event){
            // console.log(event)
            const postId = event.target.dataset.id
            axios.get(`/posts/${postId}/like`)
            		.then(function(response){
						// response.data => {'liked':True} 혹은 {'liked':False}
                		if (response.data.liked) {
                            event.target.classList.remove('far')
                            event.target.classList.add('fas')
                        } else {
                            event.target.classList.remove('fas')
                            event.target.classList.add('far`)
                        }
            		})
        })
    })
</script>

2. 좋아요 갯수 카운팅 기능 구현

좋아요 갯수 또한 자바스크립트를 통해 동적으로 구현이 가능함. 현재 _post.html에는 다음과 같이 코드가 구현되어있음. 자바스크립트는 해당 코드만을 정학히 캐치해서 변경하는 것이 불가능하므로, 이 코드만을 특정 태그로 묶은 후, 코드를 식별하기 위한 추가 정보를 삽입해야 함 (어떠한 게시글에 대한 숫자인지 등..)

<!-- 기존 코드 -->
<p> 명이 좋아합니다. </p>

1) _post.html 코드 수정

  • 변경되는 부분을 span 태그로 묶은 후, 태그의 id 값을 {{ post.id }} 을 통해 동적으로 할당함. 이 id를 가지고 자바스크립트는 각 게시글 내 좋아요 갯수에 대한 고유 코드에 접근이 가능하게 됨.
<!-- 변경한 코드 -->
<span id="like-count-{{ post.id }}"> {{ post.like_users.count }}</span> 명이 좋아합니다.

2) views.py 수정

  • like 함수의 return 값으로 좋아요 갯수를 나타내기 위한 post.like_users.count() 를 JSON 객체로 넘김.
@login_required
def like(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    
    if request.user in post.like_users.all():
        post.like_users.remove(request.user)
        liked = False
    else:
        post.like_users.add(request.user) 
        liked = True
    
    return JsonResponse({'liked': liked, 'count':post.like_users.count()})

3) list.html 수정

  • document.querySelector() 을 통해 _post.html 내 좋아요 갯수 코드에 접근 가능함. 인자로 우리가 설정한 span 태그의 id 값인 like-count-{{ post.id }} 사용
  • postIdevent.target.dataset.id 가 저장된 변수로, 이는 좋아요 기능 내 data-id="{{post.id}}" 을 통해 가져온 post의 id 값임.
  • 해당 태그의 내용을 불러오는 .innerText 에 like 함수의 반환 값인 Json 객체 내 count의 value 값을 저장시킴.
<script>
    const likebuttons = document.querySelectorAll('.likebutton')
    likebuttons.forEach(function(button){
        button.addEventListener('click', function(event){
            // console.log(event)
            const postId = event.target.dataset.id
            axios.get(`/posts/${postId}/like`)
            		.then(function(response){
                
                		// 추가한 내용
                		document.querySelector(
                        	`#like-count-${postId}`)
                			.innerText = response.data.count
                		// 끝
                
                		if (response.data.liked) {
                            event.target.classList.remove('far')
                            event.target.classList.add('fas')
                        } else {
                            event.target.classList.remove('fas')
                            event.target.classList.add('far`)
                        }
            		})
        })
    })
</script>

JavaScript 04 - Event Listener & Axios를 통한 요청보내기

|

Event Listener & Axios를 통한 요청보내기

1. Event Listener: [무엇]을 [언제], [어떻게]한다.

Event Listener란 이벤트가 발생했을 때 그 처리를 담당하는 함수를 가리킴. 지정된 타입의 이벤트가 특정 요소에서 발생하면, 웹 브라우저는 그 요소에 등록된 Event Listener를 실행시킴.

Event Listener는 말그대로 해당 Event에 대해 대기중인 것을 말함. 항상 “Listen”하고 있는 상태라고 할 수 있음. 사용자가 지정한 Event가 발생했을 때, 등록한 Event Listener가 실행됨. 여기서 Event는 “ [무엇] 을 [언제], [어떻게] 한다. “ 라고 정의 할 수 있음.

버튼을 클릭하면 메세지가 나온다” 를 event Listener를 통해 구현해보도록 하자.

1) DOM Selector

  • querySelector
  • querySelectorAll()

2) Event Listener 등록

“특정한 DOM element[무엇] 이 어떠한 행동을 했을 때[언제] **, 어떻게 한다[어떻게]**”

  • id값은 html 문서에서 유일하게 존재하는 고유값으로, id값을 바탕으로 버튼을 갖고와 변수에 저장시킨다. document.querySelector('#this-button')
  • addEventListener 매개변수
    • 1번째 인자: 반응할 이벤트의 유형을 나타냄. click 은 마우스 클릭시 이벤트가 실행됨.
    • 2번째 인자: 지정한 타입의 이벤트가 발생했을 때, 알림을 받는 객체. 하기 예시의 경우 callback 함수를 받음.
  • .innerHTML : 요소 내 포함된 HTML을 가져올 때 사용
  • Event Listener에서의 콜백함수에는 arrow function 을 사용하지 않음.
<body>
    <div id="my"></div>
    <button id="this-button"> Click me </button>
    <script>
        /*
            Event Listener
            [무엇]을 [언제] [어떻게]한다.
            버튼을 클릭하면 메세지가 나온다.
        */
    
    	// 1. 무엇 -> 버튼
        const button = document.querySelector('#this-button')
        
        // 2. 언제 -> 버튼을 '클릭' 하면
        button.addEventListener('click', function(event){
            const area = document.querySelector('#my')
            area.innerHTML = '<h1>뿅!</h1>'
        })
    </script>
</body>

3) 이벤트의 유형

  • click – 마우스버튼을 클릭하고 버튼에서 손가락을 떼면 발생한다.
  • mouseover – 마우스를 HTML요소 위에 올리면 발생한다.
  • mouseout – 마우스가 HTML요소 밖으로 벗어날 때 발생한다.
  • mousemove – 마우스가 움직일때마다 발생한다. 마우스커서의 현재 위치를 계속 기록하는 것에 사용할 수 있다.
  • keypress – 키를 누르는 순간에 발생하고 키를 누르고 있는 동안 계속해서 발생한다.
  • keydown – 키를 누를 때 발생한다.
  • keyup – 키를 눌다가 떼는 순간에 발생한다.
  • load – 웹페이지에서 사용할 모든 파일의 다운로드가 완료되었을때 발생한다.
  • scroll – 스크롤바를 드래그하거나 키보드(up, down)를 사용하거나 마우스 휠을 사용해서 웹페이지를 스크롤 할 때 발생한다. 페이지에 스크롤바가 없다면 이벤트는 발생하지 않다.
  • change – 폼 필드의 상태가 변경되었을 때 발생한다. 라디오 버튼을 클릭하거나 셀렉트 박스에서 값을 선택하 는 경우를 예로 들수 있다.
  • input - input 또는 textarea 요소의 값이 변경되었을 때
  • submit - form을 submit 할 때

Axios를 이용한 요청 보내기(+ EventListener)

자바스크립트를 통해서 요청을 보내 보도록 하자. (강아지 사진을 random으로 가지고 오는 API활용)

API에 요청을 보낼 때, 순수한 자바스크립트로 요청을 보내는 코드는 매우 지저분함. (XHR 객체를 통함). 따라서 axios를 사용해보도록 하자. axios는 XHR(XMLHttpRequest)를 보내주고 그 결과를 promise 객체로 반환해주는 라이브러리이다.

1) 기본 코드 작성

  • 외부 라이브러리인 Axios를 사용하기 위해 <head> 태그에 CDN 삽입
  • EventListener를 이용하여, 마우스 클릭 시, 강아지 사진을 random으로 갖고 오게 코드를 작성
  • axios.get() 으로 요청을 보내면, .then 이하로 응답을 함.
  • 요청이 들어오면, .then 이하의 익명함수를 실행 시킴. 그렇지 않을 경우, .catch 이하의 익명함수를 실행.
<head>
	<!-- Axion CDN 추가 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <button id="dog">Dog!</button>
    <script>
        const button = document.querySelector('#dog')
        button.addEventListener('click', function(event){
            //API로 요청을 보냄
            axios.get('https://dog.ceo/api/breeds/image/random')
            	.then(function(response){
                	//handle sucess
                	console.log(response);
            })
            	.catch(function(error){
                	//handle error
                	console.log(error);
            })
        })
    </script>
</body>

※자바스크립트는 기본적으로 모든 코드가 비동기적으로 동작함. 시간이 오래걸리는 코드가 중간에 있다고 가정할경우(응답이 언제 올지 모르는 코드가 실행되었다 가정), 자바 스크립트는 그 코드가 끝나길 기다리지 않고 그다음 코드를 바로 실행시킴.

비동기적으로 작동하지 않으면(싱글 스레드이므로), 이 코드가 실행될 때 까지 브라우저는 동작하지 않고 멈추어버리기 때문임. 따라서 요청을 기다리지 않고, 브라우저가 작동할 수 있도록 비동기적으로 동작해야함.

따라서 비동기적으로 동작하는 것을 막기 위해 .then 으로 이어줌. 위의 코드는 가독성을 위해 줄바꿈을 하였지만 실제로 .then.catch 등은 앞의 코드와 이어진 총 한줄의 코드라고 할 수 있음.

2) console.log 확인

  • console.log(response) 의 결과 값을 개발자도구 console 창을 통해 확인되는 값은 아래와 같음 (오브젝트 객체로 반환됨을 알 수 있음)
  • 여기서 우리가 얻고자 하는 이미지는 data 내 message의 값으로 저장되어있음.
  • 따라서, response.data.message 로 접근하여 이미지를 가지고 올 수 있음.
{data: {}, status: 200, statusText: "", headers: {}, config: {}, }
config: {adapter: ƒ, transformRequest: {}, transformResponse: {}, timeout: 0, xsrfCookieName: "XSRF-TOKEN", }
data: {status: "success", message: "https://images.dog.ceo/breeds/briard/n02105251_6387.jpg"}
headers: {content-type: "application/json", cache-control: "no-cache, private"}
request: XMLHttpRequest {onreadystatechange: ƒ, readyState: 4, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, }
status: 200
statusText: ""
__proto__: Object

3) 코드 확장

.then 은 앞의 코드가 끝나면 뒤의 코드가 실행되게 하는 메소드임. 앞서 설명한 것 처럼, 비동기 처리를 막기 위해 사용하는 메소드인데, .then 이하의 코드가 끝난 후, 다른 코드가 실행 시키고 싶을 경우, .then 을 이어서 또 사용할 수 있음. 이 때, 앞의 익명함수의 return 값이 함수의 인자로 들어감.

.then을 여러번 사용하는 경우는 앞의 함수와 뒤의 함수가 처리하는 것이 다를 경우를 의도적으로 구분하기 위해서 사용하는 것이 일반적임.

한단계 더 나아가, .then 은 모든 함수의 메서드로 쓸 수 있는 것은 아님. .then 사용 유무는 get의 return값이 무엇이냐에 따라 결정됨. axios.get의 return 값은 Promise 객체임. 이는 응답을 보내주겠다는 약속을 하는 것! 수행되는 시간이 보장되지 못할 때 사용되며, .then 메소드는 Promise 객체의 메소드임. (.catch 도 동일함)

  • response.data.message 를 통해 이미지 주소를 갖고 올 수 있음.
  • 두번째 콜백함수의 인자인 url은 바로 위 함수의 return 값인 response.data.message를 갖고 있음.
  • document.createElement() : 새로운 태그를 만듦. 인자에 img를 넣어 img 태그르 만든 후 새롭게 선언한 변수에 저장함.
  • imgTag.src = url; : img 태그에 경로를 설정하는 속성인 src에 들어오는 네임인자인 url을 저장시킴.
  • document.querySelector('body').append(imgTag) : 이후, body 태그에 imgTag 값을 append함.
<head>
	<!-- Axion CDN 추가 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <button id="dog">Dog!</button>
    <script>
        const button = document.querySelector('#dog')
        button.addEventListener('click', function(event){
            //API로 요청을 보냄
            axios.get('https://dog.ceo/api/breeds/image/random')
            	.then(function(response){
                	//handle sucess
                	console.log(response);
                	return response.data.message
            })
            	.then(function(url){
                	const imgTag = document.createElement('img')
                    imgTag.src = url;
                	document.querySelector('body').append(imgTag)
            })
            	.catch(function(error){
                	//handle error
                	console.log(error);
            })
        })
    </script>
</body>

JavaScript 03 - 배열 조작 메서드(forEach, map, filter, find, every, some, reduce)

|

JavaScript 기본 문법 (3)

7. Array Helper Methods - 배열 조작 메서드

1) forEach

  • ES5 표준화 버전에서는 반복문을 돌릴때 var 메서드를 사용했었음
var colors = ['red', 'blue', 'green']
for (var i=0; i<colors.length, i++){
    console.log(colors[i]);
}
  • ES6+ 에는 forEach 메서드 사용
    • 가장 기본적인 형태의 반복문으로, 단순 반복을하고 return 값을 따로 가지지 않음.
    • 함수 자체가 파라미터로 넘어가서 동작하는 것을 callback 함수라고 하며 JS에서 가장 대표적으로 사용하는 방법임. callback 함수로서 동작하게 코드를 아래와 같이 작성 할 수 도 있다.
const colors = ['red', 'blue', 'green']

// 기본적인 방법
const f = function(color){
    console.log(color)
}
colors.forEach(f)

// callback 함수로서 동작
colors.forEach(function(color){
    console.log(color)
})

2) map

  • ES5
var numbers = [1,2,3]
var doubleNumbers = []

for (var i=0; i<numbers.length; i++){
    doubleNumber.push(numbers[i]*2)
}

console.log(doubleNumbers)
  • ES6+ 에는 map 메서드 사용
    • return 값을 가지며 새로운 배열을 만듦.
const numbers2 = [1,2,3]
const doubleNumbers2 = numbers2.map(function(number2){
    return number2 * 2
})
console.log(doubleNumber2)

3) filter

  • 원하는 값들롤만 새로운 배열을 만듦.
  • 해당 조건이 true인 값에 대해서만 가져와 배열에 넣음
const products = [
    {name: 'cucumber', type:'vege'},
    {name: 'banana', type: 'fruit'},
    {name: 'onion', type:'vege'},
    {name: 'apple', type: 'fruit'},
]

const fruitproducts = products.filter(function(product){
    return product.type == 'fruit'
})

console.log(fruitproducts)

4) find

  • 단 하나의 결과만을 출력함.
  • 조건을 만족하는 값을 찾을 경우, 반복문을 바로 종료시킴.
const users = [
    {name: 'HARRY'},
    {name: 'ADMIN'},
    {name: 'MANSU'},
]

const foundUser = users.find(function(user){
    return user.name == 'ADMIN'
})

console.log(foundUser)

5) every & some

  • every: 배열안의 모든 데이터가 조건을 만족하는 경우 true를 반환함. else, false를 반환.
  • some: 배열안의 데이터 중 하나라도 조건을 만족하는 경우 true를 반환함. else, false를 반환.
const computers = [
    {name: 'macbook', ram: 16},
    {name: 'gram', ram: 8},
    {name: 'series9', ram:32},
]

const everyComputersAavailable = computers.every(function(computer){
    return computer.ram > 16
})

const someComputersAvailable = computers.some(function(computer){
    return computer.ram > 16
})

console.log(everyComputerAvailable)
console.log(someComputerAvailable)

7-1. reduce

배열의 각 요소에 대해 주어진 reduce 함수를 실행시키고 하나의 결과 값을 반환함.

1) 구문

arr.reduce(callback[, initalValue])

2) 매개변수

  • callback
    • accumulator: 콜백의 반환값을 누적. 콜백의 이전 반환 값 또는 콜백의 첫번째 호출이면서 initialValue를 제공한 경우 initalValue의 값임.
    • currentValue: 배열 내 현재 처리되고 있는 요소
    • currentIndex(optional): 배열 내 현재 처리되고 있는 요소의 인덱스. initalValue를 제공한 경우 0, 아니면 1부터 시작함.
    • array(optional): reduce()를 호출한 배열
  • initialValue(optional): callback의 첫호출 때 첫번째 인수에 제공하는 값. 초기값을 제공하지 않을 경우, 배열의 첫번째 요소를 사용함

reduce의 return 값에는 배열이 들어 갈 수도 있으며, 특정한 값도 들어갈 수 있음.

3) reduce() 작동방식

  • 1부터 5까지 있는 배열 내 숫자들의 합 계산하기
var arr = [1,2,3,4,5]
var cnt = 0
var sum = arr.reduce(function(acc, value){
    cnt ++ 
    return acc + value
});

console.log(cnt) // 4
console.log(sum) // 15

콜백은 4번 호출되며, 각 호출의 인수와 반환값은 다음과 같음.

  • initialValue 의 값을 따로 지정하지 않았기 때문에, 배열의 첫번째 요소(1)이 initialValue 으로 사용되었으며, 콜백의 누적값인 accumulator에 저장되어있음.
  • 배열의 첫번째 요소(1)을 사용하였으므로 순서대로 currentValue 에는 2가 들어가고 첫번째 반환값은 3이된다. 같은 방식으로 콜백은 총 4번 호출되며 각 호출의 인수와 반환값은 아래와 같이 정리할 수 있음.
  • cnt 를 통해 몇번 콜백이 되었는지 쉽게 알 수있음.
call back acc (accumulator) value(currentValue) 반환값(return)
1st 호출 1 2 3
2nd 호출 3 3 6
3rd 호출 6 4 10
4nd 호출 10 5 15
  • initalValue 값을 5로 지정했을 경우, 값이 아래와 같이 달라짐을 알 수 있다.
  • 5라는 초기값이 새로 주어졌으므로 callback은 5번 호출되며, 최종 합은 20으로 변한다.
var arr = [1,2,3,4,5]
var cnt = 0
var sum = arr.reduce(function(acc, value){
    cnt ++ 
    return acc + value
}, 5);

console.log(cnt) // 5
console.log(sum) // 20

4) reduce vs map

  • reduce
    • initalValue 으로 빈 배열을 우선 만들었음. 이에 따라, value 에는 순차적으로 1,2,3이 들어가고 콜백이 총 3번됨.
    • 사이사이에 삽입한 console.log를 면밀히 살펴 보면 어떠한 프로세스로 진행되는지를 알 수 있음.
var num = [1,2,3]
var count = 0
var doubleNum = num.reduce(function(acc, value){
    console.log(acc, value)
    acc.push(value*2)
    console.log(acc)
    count ++
    return acc
}, []);

console.log(count) // 3
console.log(doubleNum) // [ 2, 4, 6 ]
call back acc (accumulator) value(currentValue) acc.push(value*2) 반환값(return)
1st 호출 [] 1 [2] 2
2nd 호출 [2] 2 [2,4] 4
3rd 호출 [2,4] 3 [2,4,6] 6

5) reduce vs filter

  • reducer
    • map 때와 동일한 방식으로, initalValue 으로 빈 배열을 우선 만듦. 그 후, 분기문에 따라, type== 'vegetable' 을 만족하는 값에 대해서만 배열에 추가한 후, 배열을 return 함.
const products = [
    {name: 'banana', type:'fruit'},
    {name: 'onion', type:'vegetable'},
    {name: 'apple', type: 'fruit'},
    {name: 'lettuce', type: 'vegetable'},
]

var vegetableProducts = products.reduce(function(acc, value){
    if (value.type == 'vegetable') {
        acc.push(value)
    }
    return acc
}, [])
console.log(vegetableProducts)
  • filter
const products2 = [
    {name: 'banana', type:'fruit'},
    {name: 'onion', type:'vegetable'},
    {name: 'apple', type: 'fruit'},
    {name: 'lettuce', type: 'vegetable'},
]

const vegetableProducts2 = products2.filter(function(product){
    return product.type == 'vegetable'
})

console.log(vegetableProducts2)

6) reduce vs find

  • reduce
    • find의 속성은 원하는 값을 찾을 경우 반복문이 바로 끝나야함. 이 조건을 reduce 에서 걸어주기 위해 type of acc == 'undefined' 인 조건을 추가로 삽입하였음.
const users = [
    {name:'HARRY'},
    {name:'ADMIN'},
    {name:'MANSU'}
]

const findUser = users.reduce(function(acc, value){
    if (typeof acc == 'undefined' && value.name == 'ADMIN'){
        acc = value;
    }
    return acc;
}, undefined)

console.log(findUser)
  • find
const users2 = [
    {name:'HARRY'},
    {name:'ADMIN'},
    {name:'MANSU'}
]

const findUser2 = users2.find(function(user){
    return user.name == 'ADMIN'
})

console.log(findUser2)