Skip to content

Latest commit

 

History

History
482 lines (419 loc) · 23.4 KB

20211027.md

File metadata and controls

482 lines (419 loc) · 23.4 KB

Javascript

비동기 서버 통신 구현 (with AJAX)

  • 지난 시간에 이어 XMLHttpRequest 객체를 통해 서버에 요청을 보내고 응답을 받아 처리해보자.

AJAX GET 복습

XMLHttpRequest 요청 코드

  • GET 요청을 하는 비동기함수 get은 다음과 같다.
    • 엔드포인트를 url로 받아 서버로부터 정보를 가져오게 한다.
    • 서버로부터 받은 응답을 파싱하여 콜백함수의 인자로 넘기도록 한다.
    • 이 함수는 fetchTodos 함수에서 호출하면서, '/todos' 엔드포인트와 setTodos 콜백 함수를 인자로 넘긴다.
// GET '/todos'
const get = (url, callback) => {
    const xhr = new XMLHttpRequest()

    xhr.open('GET', url);
    xhr.send();

    xhr.onload = () => {
        if (xhr.status === 200){
            callback(JSON.parse(xhr.response));
        } else {
            console.log(xhr.status, xhr.statusText);
        }
    }
}

POST

  • 새로운 데이터를 넣는 addTodo 함수는 post를 호출한다.

XMLHttpRequest 요청 코드

  • get과 동일하게 xhr 객체를 만들고 요청을 날리지만, 이번에는 payload가 있다는 점이 다르다.
    • payload의 매개변수 순서는 어디가 될까? 보통은 callback을 마지막에 넣어주니까 그 앞에 넣어주자.
      • 비동기 처리 성공 콜백과 실패 콜백 두개가 올 수도 있고, 콜백이 중간에 있으면 함수라고 인지하지 못할 수도 있으니.
      • 그러나 node.js 진영에는 콜백을 맨 앞에 넣어주는 것이 좋다고 하는 사람들도 있다.
    • payload에 대한 메타데이터 또한 요청의 header에 세팅해줘야 한다. (xhr.setRequestHeader({헤더에 담길 키와 값}))
  • 응답 상태코드도 200 뿐 아니라 201(created)이 넘어올 수 있다. (post한 데이터를 무사히 만들었다는 뜻)
// POST '/todos'
const post(url, payload, callback) {
  const xhr = new XMLHttpRequest();
  
  xhr.open('POST', url);
  xhr.setRequestHeader('content-type', 'application/json');
  xhr.send(JSON.stringify(payload));

  xhr.onload = () => {
    if (xhr.status === 200 || xhr.status === 201) {
      callback(JSON.parse(xhr.response));
    } else {
      console.log(xhr.status, xhr.statusText);
    }
  }
}

서버사이드 응답 코드

  • 서버에는 다음과 같이 post 요청에 대한 응답 로직을 마련해둔다.
    • 요청 Request 객체의 body에는 payload가 들어있다. 이를 todos에 넣어준 후 todos 배열을 다시 보내준다.
// server.js
const express = require('express');
const app = express();

// mock data
let todos = [ {...}, {...}, {...} ];

app.use(express.static('public'));
app.use(express.json());

// 'GET' 요청에 대한 응답 로직
app.get('/todos', (req, res) => {
  res.send(todos)
});

// 'POST' 요청에 대한 응답 로직
app.post('/todos', (req, res) => {
  const newTodo = req.body;
  todos = [ newTodo, ... todos ];

  res.send(todos);
});

요청 함수 호출 코드

  • post를 호출하는 addTodo 함수에 setTodos 함수와 함께 넣어주도록 하자.
// state.js

const addTodo = content => {
  post('/todos', { id: generateNextId(), content, completed: false }, setTodos);
}

PATCH 처리 코드

  • 있는 데이터를 변경하는 것은 'PATCH' HTTP 메서드를 사용한다.
    • todos에서는 toggleAll과 toggleTodoCompleted, updateTodoContent 등에서 쓰인다.
    • 하나의 데이터 전체를 변경할 때는 PUT을 사용하지만 우리는 completed나 content만 변경할 것이므로 semantic하게 PATCH 요청을 쓰는 걸로.

XMLHttpRequest 요청 코드

  • patch함수에서는 id를 통해 변경할 데이터를 식별하고 그 데이터에 변경하여 갱신할 새로운 값, 즉 payload를 전달한다.
    • payload 있으니 setRequestHeader로 헤더 세팅
    • post와 같이 JSON.stringify해주는 것 잊지 말 것
const patch = (url, payload, callback) => {
  const xhr = new XMLHttpRequest();
  xhr.open('PATCH', url);
  xhr.setRequestHeader('content-type', 'application/json');
  xhr.send(JSON.stringify(payload));

  xhr.onload = () => {
    if (xhr.status === 200) {
      callback(JSON.parse(xhr.response));
    } else {
      console.log(xhr.status, xhr.statusText);
    }
  };
};

서버사이드 응답 코드

  • server에서는 id를 가지고 들어오는 patch요청('/todos/:id')과 아닌 요청('/todos/')을 달리 처리한다.
    • id는 payload가 아닌 endpoint로 들어온다. 엔드포인트의 parameter로 넘어오는 정보는 키와 값으로 짝지어져있다
    • endpoint로 들어오는 값은 request 객체의 params라는 프로퍼티를 통해 참조할 수 있으며 이 때 params는 app.patch의 첫 인수로 들어온 endpoint에서 : 뒤에 기재한 매개변수명을 키로, 그리고 실제 요청의 엔드포인트에서 해당 위치에 실제로 들어오는 값을 값으로 갖는 객체를 준다.
// server.js

// (1) 모든 completed 값을 변경하는 경우
app.patch('/todos', (req, res) => {
  const { completed } = req.body

  todos = todos.map(todo => ({ ... todo, completed }));
  res.send(todos);
});

// (2) id로 들어온 값의 completed나 content만 변경하는 경우
app.patch('/todos/:id', (req, res) => {
  const { id } = req.params;
  const payload = req.body;
  // payload는 { completed: true }나 { content: React } 등의 형태로 되어있다. 
  todos = todos.map( todo => todo.id === +id ? { ... todo, ... payload } : todo );
  res.send(todos);
});

요청 함수 호출 코드

  • 응답로직까지 준비된 xhr의 patch 함수를 이제 toggleAll과 toggleTodoCompleted, updateTodoContent에서 호출.
    • completed 변경은 toggle이므로 payload 보낼 필요 없이 매 요청마다 서버에서 상태로 두고 있는 completed 값을 반전하기만 하면 된다.
      • 그러나 content 변경과 일관된 로직 및 함수 재사용을 위해 적용될 completed값을 payload로 인수로 전달하며 호출하자.
      • toggleAll은 해당 checkbox input의 checked 프로퍼티의 값을 반전시킨 completed 값을 이벤트핸들러에서 받아 그대로 payload로 보낸다.
      • toggleTodoCompleted는 state.js가 가진 todos 데이터에서 해당 id의 completed 값을 취득하여, 반전시켜 payload로 보낸다.
    • content는 id값을 endpoint로, 변경할 값을 payload로 보낸다.
// state.js
const toggleAll = completed => {
  patch('/todos', { completed }, setTodos);
};

// id를 가지고 해당 todo 데이터의 completed 값을 찾는다.
const toggleTodoCompleted = id => {
  const { completed } = state.todos.find(todo => todo.id === +id);
  patch(`/todos/${id}`, { completed: !completed }, setTodos);
};

const updateTodoContent = (id, content) => {
  patch(`/todos/${id}`, { content }, setTodos);
};

DELETE 처리 코드

  • DELETE 요청 또한 모든 completed된 데이터를 지우는 요청과 특정 id에 해당하는 데이터만 지우는 요청으로 나뉜다.
  • '/todos/:id'로 오는 DELETE 요청은 해당 id를 갖는 데이터를 삭제하는 것으로 한다.
  • '/todos/completed'라는 엔드포인트로 오는 DELETE 요청에 대해 모든 completed 데이터를 삭제
    • 원래는 엔드포인트에 completed를 쓰기보다 queryString으로 보내는 게 더 바람직

XMLHttpRequest 요청 코드

  • delete는 프로퍼티를 지우기 위해 JS에서 이미 가지고 있는 예약어이기 때문에 아쉽지만 remove라는 함수로 만들자.
  • 삭제요청에 payload를 넘길 필요가 없으므로 그냥 send한다.
const remove = (url, callback) => {
  const xhr = new XMLHttpRequest();

  xhr.open('DELETE', url);
  xhr.send();

  xhr.onload = () => {
    if (xhr.status === 200) {
      callback(JSON.parse(xhr.response));
    } else {
      console.log(xhr.status, xhr.statusText);
    }
  }
}

서버사이드 응답 코드

  • 다음과 같이 id를 받는 경우와 completed라는 엔드포인트까지 가진 경우의 응답 코드를 각각 작성한다.
// DELETE '/todos/:id' (id로 온 todo를 삭제, 즉 id가 같지 않은 todo만 남긴다.)
app.delete('/todos/:id', (req, res) => {
  const { id } = req.params;
  todos = todos.filter(todo => todo.id !== +id );
  res.send(todos);
})

// DELETE '/todos/completed' (completed 값이 false인 todo만 남긴다.)
app.delete('/todos/completed', (req, res) => {
  todos = todos.filter( { completed } => !completed );
  res.send(todos);
})
  • 근데 이렇게 하면 completed를 가지고 한 요청들이 다 첫 번째 app.delete를 호출한다.
    • /todos/completed로 들어온 요청을 /todos/:id에 'completed'라는 문자열을 id로 하는 요청으로 인식하기 때문
    • 이를 위해서는 /todos/completed를 엔드포인트로 받는 delete 함수를 상단으로 옮긴다.
      • 그러나 순서는 언제나 바뀔 수 있으니 여전히 안정적인 해결책은 아니다.
    • /todos/:id를 받을 때 엔드포인트의 매개변수에 이어 소괄호 안에 정규표현식을 / 없이 적으면 해당 정규표현식에 맞는 url만 요청으로 인식하여 호출된다.
app.delete('/todos/completed', (req, res) => {
  todos = todos.filter( { completed } => !completed );
  res.send(todos);
})

app.delete('/todos/:id([0-9]+)', (req, res) => {
  const { id } = req.params;
  todos = todos.filter(todo => todo.id !== +id );
  res.send(todos);
})

요청 함수 호출 코드

// state.js

const removeTodo = id => {
  remove(`/todos/${id}`, setTodos);
};

const clearCompleted = () => {
  remove('/todos/completed', setTodos);
};

xhr 요청 함수 리팩토링

  • 현재 요청 함수의 XMLHttpRequest 객체 생성하고, open하고, send 및 response의 status에 따라 처리하는 로직이 모두 같다.
  • 이를 하나로 모아서 재사용하는 함수를 만들어보자.
    • 공통적으로 받는 것이 url, callback이므로 기본으로 매개변수를 세팅해준다.
    • 이 때 payload는 옵션이니 마지막 매개변수로 세팅줘야만 한다.
    • 어떤 요청인지도 받아야 하니 method를 인수로 받는다.
// utils/xhr.js

const req = ( method, url, callback, payload ) => {
  const xhr = new XMLHttpRequest();

  xhr.open(method, url);
  xhr.setRequestHeader('content-type', 'application/json');
  xhr.send(JSON.stringify(payload));

  xhr.onload = () => {
    if (xhr.status === 200 || xhr.status === 201 ) {
      callback(JSON.parse(xhr.response));
    } else {
      console.log(xhr.status, xhr.statusText);
    }
  }
}
  • get이나 delete는 payload가 없는데 어떻게 send에 전달될까?
    • 매개변수에 아무 값도 전달하지 않은 매개변수는 undefined로 초기화된다.
    • JSON.stringify(undefined)는 undefined를 리턴한다.
    • 그러므로 xhr.send(JSON.stringify(payload))에 payload 인수를 전달하지 않으면 xhr.send()와 동일하게 동작한다.
  • payload가 전달되는 경우 setRequestHeader로 MIME type을 지정해주는데, 이를 필요로 하지 않는 경우라도 이 한 줄의 코드가 크게 성능저하를 일으키는 것이 아니니 모든 요청 응답에 대해 그대로 사용하자.
  • 마찬가지로 post 요청에 대한 응답코드 체크 시 200과 함께 xhr.status === 201 추가된 로직도 그대로 사용하자.

리팩토링한 XHR 요청함수 export하기

  • 각 요청에 따라 이를 호출하는 함수를 하나의 객체 속 메서드로 만들어 그 객체를 default export한다.
    • 각 요청마다 미리 HTTP메서드 문자열을 세팅해준다.
    • 요청 함수 호출할 때는 매개변수 순서를 그대로 넣어도 되게끔 조정해준다.
    • 이제는 default export라 namespace의 프로퍼티키로 받아오므로, remove가 아닌 delete로 HTTP 메서드와 동일한 이름으로 삭제 메서드를 명명해도 된다.
// utils/xhr.js

export default {
  get(url, callback) {
    req('GET', url, callback);
  },

  post(url, payload, callback) {
    req('POST', url, callback, payload);
  },
  
  patch(url, payload, callback) {
    req('PATCH', url, callback, payload);
  },

  delete(url, callback) {
    req('DELETE', url, callback);
  }
}
  • 만약 모듈파일이 아니라면 xhr.js 전체 코드를 감싸면서 위 객체를 리턴하는 ajax라는 이름의 즉시실행함수를 만들어 호출하고 ajax.get 등으로 사용하면 된다.
  • state.js에서는 default export이므로 namespace로 받고 import ajax from './utils/xhr.js'; ajax.get 등으로 사용하면 된다.

비동기 처리가 초래하는 문제

  • 에러 처리의 곤란함 (try/catch문을 통한 에러처리가 불가능)
    • 동기 코드의 경우, 에러가 발생할 가능성이 있는 코드를 try 문에 넣고 try문 안에서 에러가 발생하면 이후에 오는 catch문에서 사용자에게 어떤 메시지/화면을 보여줄지 대응한다.
      • try문 안에서 발생하는 모든 경우의 예외상황에서 에러를 각각 던지도록만 해두면, 에러는 이벤트와 같이 상위 함수(Caller), 즉 에러가 발생한 실행컨텍스트보다 콜스택 하단에 위치한 실행컨텍스트 방향으로 전파된다.
      • catch문을 가장 바탕이 되는 실행컨텍스트의 코드에서 한 번만 감싸두고 하위 함수들에서 던진 모든 에러를 일괄처리한다.
    • 그러나 비동기 코드로 동작하는 코드에서 발생한 에러는 try/catch문에서 잡을 수 없다.
      • 비동기 함수는 태스크 큐로 들어가 try/catch문이 포함된 모든 전역실행컨텍스트가 끝난 후 실행된다.
      • 그러므로 비동기 함수에서 발생하는 에러는 전파될 상위 함수, 즉 caller가 없다.
      • 이 때문에 비동기 함수는 successCallback과 failureCallback 함수를 받아 자체적으로 에러처리를 해줘야 한다.
      • 자체적으로 에러처리를 하는 것은 함수마다 다 에러 로직을 만들어두어야 한다는 불편함과 유지보수의 어려움을 주므로, 현업에서는 잘 쓰지 않는다.
  • 콜백 헬
    • 비동기처리의 결과는 상위 변수에 할당하거나 반환할 수 없기 때문에, 이 결과물에 대한 작업은 콜백 함수를 받아 비동기함수 안에서 호출하는 형태로만 가능하다.
      • 그러나 이 콜백함수 실행의 결과물 또한 비동기 함수 안에서 또다른 콜백함수를 호출하면서 작업할 수밖에 없어 depth가 몇개만 늘어나도 가독성이 매우 떨어진다.
      • 그래서 등장한 게 Promise로, 여전히 콜백패턴은 사용하지만, 후속처리 메서드를 통해 그나마 가독성을 높일 수 있다.
      • Promise로도 여전히 try/catch문으로는 잡을 수 없다. 그래서 등장한 게 async/await!
    • 하지만 콜백 헬은 가독성의 문제일 뿐 에러처리가 더 main problem.

Promise

  • Promise는 '상태'를 갖는 객체이다.
    • pending이었다가 settled로 상태가 변화하는데, settled 되면서 fulfilled와 rejected 중 하나의 상태를 갖는다.
      • resolve나 reject를 호출하지 않으면 pending 상태로 끝나기도 한다.
    • 생성할 때는 pending 상태로 태어나, 첫 인자로 전달하는 함수(resolve)를 명시적으로 호출하면 fulfilled로, 두 번째 인자로 전달하는 함수(reject)를 명시적으로 호출하면 rejected로 settled 된다.
  • Promise 객체는 settled 된 경우 resolve/reject의 인자로 온 값을 들고 있으며, 이를 쓰려면 후속처리 메서드를 사용한다.
    • fulfilled가 된 promise 객체는 resolve 호출 시 인자로 전달된 값을, 후속처리 메서드로 오는 then의 첫 번째 인자로 오는 콜백함수(resolve)에게 인자로 전달한다.
    • rejected 된 promise 객체는 reject 호출 시 인자로 전달된 값(주로 에러객체)을 후속처리 메서드로 오는 then의 두 번째 인자로 오는 콜백함수(reject) 또는 후속처리 메서드 catch에 오는 콜백함수에 인자로 전달한다.
  • then과 catch 또한 promise를 반환하므로, finally로 마지막 후속처리가 가능하다.
    • 모든 then에서 발생하는 에러는 마지막 catch에서 잡을 수 있으므로 가독성 및 편리한 일괄 처리를 위해 then의 두 번째 인자로 Reject 함수를 넣기보다는 마지막 catch 한 번으로 에러 처리 해준다.
    • 결과값을 직접 리턴하지는 못하지만, 해당 값을 들고 있는 promise 객체를 리턴함으로써 후속처리 메서드를 통해 사용할 수 있게 한다.
  • Promise.all: 서로 연관이 없는 비동기처리를 병렬로 실행할 때 쓰는 정적 메서드로, 실무에서 반드시 쓸 일이 있으니 알아두자.

xhr 요청 함수에서 Promise 객체 리턴해주기

  • 기존에는 ajax 함수에서 callback을 받아 실행해주었지만, 이제 callback을 전달하지 않으므로 매개변수에서 뺀다.
  • Promise 객체를 활용하여 결과값을 가진 promise 객체를 state에서 직접 resolve 함수를 주면서 후속 처리하게 만들어주자.
  • 비동기 처리 성공 결과값으로 콜백을 호출했던 부분에서는 resolve를, 에러 시 reject를 호출하게끔 하는 promise 객체를 반환한다.
const req = (method, url, payload) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    xhr.open(method, url);
    xhr.setRequestHeader('content-type', 'application/json');
    xhr.send(JSON.stringify(payload));

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 201) {
        resolve(JSON.parse(xhr.response));
      } else {
        reject(new Error('HTTP Request Rejected'));
      }
    };
  });
};
  • 함수를 모아둔 export하는 객체에서도 callback은 매개변수에서 빼고, req를 return해준다.
export default {
  get(url) {
    return req('GET', url)
  },
  // ... post, patch, delete에서도 callback 빼고 return문을 넣어주는 동일한 작업 해줄 것
}
  • 요청을 보내는 함수를 호출하는 state.js에서는 이제 promise 객체를 반환받으므로, then으로 후속처리메서드를 호출하며 결과값을 state에 반영한다.
    • 기존 요청함수에 전달하던 콜백함수는 then의 첫 번째 인자로 전달한다.
// state.js

const fetchTodos = () => {
  get('/todos').then(setTodos).catch(console.error);
};

const addTodo = content => {
  post('/todos', { id: generateNextId(), content, completed: false }).then(setTodos).catch(console.error);
}

// ... 나머지 함수들도 동일하게 callback을 빼고 then에 넣어준다.

fetch

  • fetch는 XMLHttpRequest가 수행하던 모든 일을 대신 수행해준다.
  • fetch에 url을 담아 보내는 요청은 기본적으로 'GET' 요청이다.
    • 다른 http메서드인 경우 두 번째 인자로 { method: 'POST' } 등으로 메서드를 명시해준다.
  • 결론적으로는 안 쓰는 게 좋은데, 왜냐하면 번거로운 두 가지 처리를 필요로 하는 fetch만의 요상한 동작 때문이다.
    • 내부에서 에러가 발생해도 reject하지 않아 catch 후속처리 메서드에서 잡을 수 없다.
      • 반드시 fetch의 promise 객체의 ok에 담긴 값을 확인하여, 에러를 명시적으로 던져줘야만 한다.
    • response.body로 넘어오는 데이터는 꼭 json으로 파싱하는 후속처리 메서드를 한 번 거친 후에 사용할 수 있다.

fetch를 통해 Promise 객체 리턴해주기

  • fetch로 xhr을 모두 대체하면서, Response.json()으로 처리까지 해준 promise 객체를 리턴해주자.
const req = (method, url, payload) => {
  return fetch(url, {
    method,
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(payload) 
  }).then(res => {
    if (!res.ok) throw new Error(res.status);
    return res.json();
  });
}
  • 또는 export 하는 함수들에서 fetch를 호출하며 생성된 promise 객체를 바로 리턴해줄 수도 있다.
export default {
  // GET 요청이 default이므로 url만 날리면 된다.
  get(url) {
    return fetch(url).then(res => {
      if (!res.ok) throw new Error(res.status);
      return res.json();
    });
  },

  post(url, payload) {
    return fetch(url, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(payload), 
    }).then(res => {
      if (!res.ok) throw new Error(res.status);
      return res.json();
    })
  },

  // ... patch와 delete도 동일하게 한다. 
}
  • response 에러 체크와 json 파싱 로직은 따로 떼어 함수로 만들어 후속처리메서드에 넣어주자.
const parseResponseToJson = res => {
  if (!res.ok) throw new Error(res.status);
  return res.json();
};

axios 라이브러리

  • 웹팩을 쓰면 모듈을 뭉쳐주기도 하지만 모듈을 만들어주기도 한다.
    • 우리가 쓴 모듈은 js가 ES6부터 제공하는 ESM인데, axios는 웹팩 사용을 전제해서 그런지 ESM을 지원하지 않는다.
    • 즉 우리가 쓴 import/export 등으로 가져온 ajax 등을 axios만으로는 쓸 수 없다.
    • 우리는 아직까지는 모듈 번들링으로 정적파일을 서빙하지 않으니, HTML문서에 axios 패키지 공식 문서에서 제공하는 아래와 같은 태그를 적어주자. (반드시 axios를 사용하는 script 태그보다 상단에 작성할 것)
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  • 위 스크립트로 가져온 axios를 전역 변수처럼 참조할 수 있도록 .eslintrc.json에서 { ... "globals": { "axios": true } } 세팅을 해준다.
  • axios를 사용하면 아래와 같은 fetch의 문제를 해결할 수 있다.
    • 내부에서 reject를 안하기 때문에 매번 fetch가 반환하는 response 객체에 대하여 response.ok로 응답상태를 확인해야 한다.
    • { headers: {'content-type': 'application/json'} }을 매번 넣어줘야 한다.
    • 프로미스 resolve값으로 가진 response를 json()으로 파싱하는 후속처리메서드를 한 번 거쳐야만 한다.
  • 그럼 axios는 무엇을 리턴할까? 보다 수월하게 후속처리를 할 수 있는 결과값(+@)을 가진 프로미스!
    • axios.get(url).then(data => console.log(data))의 결과를 찍어 보면, config, data 등이 넘어오는데, 우리가 필요로 하는 것은 data라는 키의 값으로 넘어온다.
    • axios.get(url).then( { data } => data )로 data를 받아 사용하면 되며, 에러가 발생하면 reject 콜백이 실행되므로 catch를 통해 잡을 수 있다.
  • payload를 가진 요청의 경우 axios에서 형태 분석을 통해 알아서 header(content-type) 세팅해주고 stringify도 해준다.
    • 결과값을 객체 속 data 키의 value로 주니까 후속처리메서드 then으로 then(({data}) => data)만 잊지 말고 해주기

axios 라이브러리로 fetch보다 간편하게 후속처리하기

// utils/ajax.js
export default {
  get(url) {
    return axios.get(url).(({data}) => data);
  },
  post(url, payload) {
    return axios.post(url, payload).then(({data})=> data);
  },
  patch(url, payload) {
    return axios.patch(url, payload).then(({data})=> data);
  },
  delete(url) {
    return axios.delete(url).then(({data}) => data);
  }
}

느낀 점

  • 비동기 처리의 동작원리와, 프론트/백을 다 알 수 있어서 힘들지만 재밌었다.