[38][23.11.15][구디아카데미 후기/국비지원 IT개발자취업/김승수 선생님]

DANA·2023년 11월 16일

KDT-구디아카데미

목록 보기
38/56

프로토타입

  • 프로토타입: 객체가 다른 객체로부터 상속받을 수 있는 속성과 메서드를 정의한 객체
  • JavaScript에서 모든 객체는 다른 객체로부터 상속됩니다. 이때 상속은 프로토타입 체인(prototype chain)을 통해 이루어집니다. 객체는 자신의 프로토타입에 정의된 속성과 메서드를 참조할 수 있습니다.

프로토1.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./프로토1.js"></script>
</head>
<body>
  
</body>
</html>

프로토1.js

// prototype

const fruits = ['🍎','🍅','🥝'];
console.log(fruits);
const fruits2 = new Array('🍎','🍅','🥝');
console.log(fruits2);
console.log(fruits2.includes('🥝'));

Array.prototype.method = function (){
  console.log('1');
  console.log(this);
}

fruits2.method()

const arr = [];
arr.method();//[]출력

프로토2.js

const person = {
  firstName: '초보',
  lastName: '나',
  printName: function(){
    return `${this.firstName} ${this.lastName}`
  }
}
const student = {
  firstName: '신입',
  lastName: '나'
}

console.log(person.printName());
//console.log(student.printName());
console.log(person.printName.call(student));

프로토3.js

function Emp(fName, lName){
  this.fName = fName;//자바와는 다르게 (선언부에 선언할 수 없다 -class껍데기없다) 전변이다.
  this.lName = lName;
}
// 화살표함수를 썼을때와 function으로 함수를 정의할 때 this가 달라진다. - 주의
Emp.prototype.printName = function() {
  return  `${this.lName} ${this.fName}`;
}

const james = new Emp('초보','나');//생성자 함수라고 함
const king = new Emp('신입','나');//생성자 함수라고 함

console.log(james.printName());
console.log(james.printName);
console.log(king.printName());
console.log(king.printName);

프로토4.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./프로토4.js"></script>
</head>
<body>
  
</body>
</html>

프로토4.js

function Emp(fName, lName){
  this.fName = fName;//자바와는 다르게 (선언부에 선언할 수 없다 -class껍데기없다) 전변이다.
  this.lName = lName;
}
// 화살표함수를 썼을때와 function으로 함수를 정의할 때 this가 달라진다. - 주의
Emp.prototype.printName = function() {
  return  `${this.lName} ${this.fName}`;
}

Emp.prototype.see = function(pic){
  console.log(pic);//노을사진 - 브라우저 통해서 결과를 확인해 본다
}

const james = new Emp('초보','나');//생성자 함수라고 함
const king = new Emp('신입','나');//생성자 함수라고 함

console.log(james.printName());
console.log(james.printName);
james.see('노을사진');
console.log(Emp);
console.log(king.printName());
console.log(king.printName);

프로토타입에 대해서....
결국 프로토타입 이라는 건 new를 통해서 만드는 생성자 함수에서 반환된 결과이다
그래서 fruits2라는 하나의 배열 데이터, 또 다른 말로는 인스턴스라고 말하는데
이렇게 instance에서 쓸 수 있는 별도의 속성이나 혹은 메소드를 등록하는 객체를
말하는 것이다
그래서 우리가 배열 데이터를 만들 때 사용하는 array라는 객체에서 프로토타입으로 method
라는 메소드를 등록한 것처럼......

Fetch함수

  • fetch() 함수: 웹 브라우저 및 Node.js 환경에서 네트워크 요청(HTTP 요청)을 수행하기 위한 JavaScript API

fetch()를 사용해 서버에 데이터를 보낼 때

1. 데이터 전송 방법
: body 속성을 통해 서버에 정보를 전달하는 경우 GET 메소드를 사용할 수 없고, POST 방식을 사용해야 함

2. 데이터 형식
: body 속성에 전달되는 정보는 문자열 형식이어야 함('GET' 메서드를 사용하는 경우 쿼리 문자열을 사용하여 서버에 값을 전달할 수 있음)

3. GET/HEAD 방법에 대한 제한 사항
: GET 또는 HEAD 메소드를 사용한 요청에는 본문이 있을 수 없음

패치1.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./패치1.js"></script>
</head>
<body>
  
</body>
</html>

패치1.js

// 헤더 객체 생성
var myHeaders = new Headers();
// Authorization 헤더에 Kakao API 키를 추가
myHeaders.append("Authorization", "KakaoAK 4b140cf1d4428a43b2d0318382e7b264");

// HTTP 요청에 사용될 옵션 객체 생성
var requestOptions = {
  // POST 메서드 사용
  method: 'POST',
  // 위에서 설정한 헤더 사용
  headers: myHeaders,
  // JSON 형태로 변환된 데이터를 요청 본문으로 설정
  body: JSON.stringify({
    name: '나신입',
    email: 'nice@hot.com'
  }), 
  redirect: 'follow'
};

// fetch 함수를 사용하여 Kakao API에 POST 요청
fetch("https://dapi.kakao.com/v3/search/book?target=title&query=오라클&size=3", requestOptions)
	// JSON 형태로 응답을 해석
  .then(response => response.json())/
  .then(result => console.log(result))//Promise제공하는 함수나 속성을 호출할 수 있다.
  .catch(error => console.log('error', error));
var myHeaders = new Headers();
  • Headers 객체: HTTP 헤더의 컬렉션을 나타냄. 이 객체는 키-값 쌍으로 이루어진 헤더 정보를 포함하고, HTTP 요청이나 응답에 사용되는 헤더를 조작하는 메서드를 제공
body: JSON.stringify({})
  • body 속성에 있는 데이터가 JSON 형식
  • JSON.stringify() 함수를 사용하여 JavaScript 객체를 JSON 문자열로 변환
  • 이로 인해 name 속성의 값은 JSON 형식에 맞게 표현됨
redirect: 'follow'
  • fetch 함수의 옵션 중 하나(HTTP 리다이렉션을 어떻게 다뤄야 하는지를 지정)
  • follow: HTTP 리다이렉션을 자동으로 따른다. 즉, 서버가 3xx 상태 코드를 반환하면 브라우저가 자동으로 새로운 위치로 이동
.then(response => response.json())
  • 특히 fetch API를 사용하여 HTTP 요청을 할 때 Promise 체인의 일부
  • xx.then(): Promise의 해결된 값을 처리하는 메소드. Promise가 성공적으로 해결되면 실행될 콜백 함수가 필요한데, 'fetch' 에서 이 콜백은 'Response' 개체를 수신
.then(response => {})
  • Response 객체에는 .json()이라는 메서드가 있dma. 이 메서드는 응답 본문을 읽고 본문을 JSON으로 구문 분석한 결과를 확인하는 Promise를 반환

ES6

구조분해할당.js

const Sonata = {
  carColor: 'black', 
  speed: 30
}

// 기존 방식: 객체의 속성 값을 변수에 할당
const carColor = Sonata.carColor;
console.log(carColor);
const speed = Sonata.speed;
console.log(speed);
  
// 구조 분해 할당: 객체의 속성 값을 한 번에 변수에 할당
{
const { carColor,  speed } = Sonata;
console.log(carColor, speed);
}
  
// 구조 분해 할당 시 다른 변수명 사용 가능
{
const { carColor: myColor,  speed:  mySpeed } = Sonata;
console.log(myColor, mySpeed);
}

const fruits = ['토마토','키위','사과'];
  
// 기존 방식: 배열의 각 요소 값을 변수에 할당
const first = fruits[0]
const second = fruits[1]
const third = fruits[2]

// 구조 분해 할당: 배열의 각 요소 값을 한 번에 변수에 할당
{
  const [first, second, third] = fruits;
  console.log(first, second,third);
}

기본값.js

  • 기본값 매개변수(default function parameter)
    : 기본값 함수 매개변수를 사용하면 값이 전달되지 않거나 undefined인 경우 명명된 매개변수를 기본값으로 초기화할 수 있음
// 함수 선언: msg가 주어지면 해당 메시지를 출력하고
// 주어지지 않으면 'default message'를 출력
const msgPrint = (msg) => {
  // 매개변수 msg가 null이면 기본값 'default message'를 할당
  if(msg == null) {
    msg = 'default message';
  }
  // 메세지 출력
  console.log(msg);
}

// 함수 호출: 'hello'를 전달하여 메시지 출력
  msgPrint('hello');
// 매개변수가 주어지지 않은 경우, 기본값 'default message'로 메시지 출력
  msgPrint()
  
// 블록 스코프 내에서 함수 재선언 및 매개변수 기본값 활용
{
  const msgPrint = (msg = 'default message') => {
  console.log(msg);
}
  // 함수 호출: 'hello'를 전달하여 메시지 출력
  msgPrint('hello');
  // 매개변수가 주어지지 않은 경우, 기본값 'default message'로 메시지 출력
  msgPrint()  
}

초기화.js

  • Object Initailizer
  • key와 value가 동일할 때는 생략이 가능하다
// 객체 생성
const object1 = { a: 'foo', b: 42, c: {} };

// 객체의 속성 a 출력
console.log(object1.a);
// Expected output: "foo"

// 변수 선언 및 초기화
const a = 'foo';
const b = 42;
const c = {};
  
// 객체 생성
const object2 = { a, b, c };
// 객체의 내용 출력
console.log(object2);
// 객체의 속성 b 출력
console.log(object2.b);
// Expected output: 42

// 이미 선언된 변수를 이용하여 객체 생성
const object3 = { a, b, c };
// 객체의 속성 a 출력
console.log(object3.a);

전개연산자.js

  • Spread syntax: 배열의 요소, 객체의 속성 또는 함수 인수 등 요소의 확장을 허용
// 객체 생성
let emp = { key: 'empno' }
let dept = { key: 'deptno' }
let member = { key: 'memno'}

// 배열 생성
const array = [emp, dept]
console.log(array);

// 배열 복사: 스프레드 연산자(...)를 사용하여 배열 복사
const arrayCopy = [...array]; // 얕은복사
// 배열에 member 객체 추가
arrayCopy.push(member);
// 원본 배열과 복사된 배열 출력
console.log(array);
console.log(arrayCopy);

// 배열에 새로운 객체 추가
// 스프레드 연산자를 사용하여 기존 배열과 새로운 객체를 합침
const arrayCopy2 =  [...arrayCopy, {key:'bookno'}]
// 합쳐진 배열 출력
console.log(arrayCopy2);

//Object merge
let emp1 = { key1: 'empno' }
let dept1 = { key2: 'deptno' }
  
const obj1 = {emp1, dept1}
console.log(obj1);
  
  
const obj2 = {...emp1, ...dept1}
  
// 객체 합치기: 스프레드 연산자를 사용하여 emp1과 dept1 객체를 합친 객체 생성
console.log(obj2);
  1. 얕은복사(Shallow Copy)
    복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.
    어느 한쪽을 변경하면 나머지도 같이 변경된 결과 확인가능

  2. 깊은복사(Deep Copy)
    참조를 공유하지 않는다.

템플릿리터럴.js

// Template Literals
//https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals

 const time ='11:13:26';
 const date = '2023-11-15';

console.log(`오늘은 ${date} 이고 현재시간은 ${time} 입니다.`);

삼항연산자.js

const isCar = true;

let carName;

if(isCar) carName = '소나타';
else carName = '자동차가 아니다';

console.log(carName);
{
  let carName = isCar ? '소나타':'아니다';
  console.log(carName);
}

달력1

basic.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>기본달력 테스트 - 실습</title> 
  <!-- CDN방식 링크 -->

  <script src='https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js'></script>
  <script>

    document.addEventListener('DOMContentLoaded', function() {
      const calendarEl = document.querySelector('#calendar')
      //카렌더 객체 생성하기 및 생성자 호출 - 초기화 속성값 지정
      const calendar = new FullCalendar.Calendar(calendarEl, {
        initialView: 'dayGridMonth',
        headerToolbar: {
          left: 'prev, next, today',
          center: 'title',
          right: 'dayGridMonth, timeGridWeek, listWeek'
        },
        //일정에 대한 데이터셋 가져오기
        events:[
          {
            title: '월간회의',
            start: '2023-11-01'
          },
          {
            title: 'other event',
            start: '2023-11-01',
            end: '2023-11-04'
          },
          {
            title: '휴가일정',
            start: '2023-11-07',
            end: '2023-11-15'
          },
          {
            groupId: 999,
            title: '업체회의',
            start: '2023-11-17T15:00:00',
          },
          {
            groupId: 999,
            title: '기획회의',
            start: '2023-11-17T17:00:00',
          }
        ]


      })
      calendar.render()//위에서 초기화된 정보로 달력을 그려줘
    })

  </script>

</head>
<body>
  <div id="calendar"></div>
</body>
</html>

basic2.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>기본달력 테스트 - 실습</title> 
  <!-- CDN방식 링크 -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
  <script src='https://cdn.jsdelivr.net/npm/fullcalendar/index.global.min.js'></script>
  <script>

    document.addEventListener('DOMContentLoaded', function() {
      const calendarEl = document.querySelector('#calendar')
      //카렌더 객체 생성하기 및 생성자 호출 - 초기화 속성값 지정
      const calendar = new FullCalendar.Calendar(calendarEl, {
        initialView: 'dayGridMonth',
        headerToolbar: {
          left: 'prev, next, today',
          center: 'title',
          right: 'dayGridMonth, timeGridWeek, listWeek'
        },
        //일정에 대한 데이터셋 가져오기 -  Back-End 만나는 부분이다.
        eventSources: [{
          events: function(info, successCallback, failureCallback) {
            $.ajax({
              url: 'events2.json',
              type: 'GET',
              dataType: 'json',
              success: function(data) {
                console.log(data);
                //console.log(JSON.stringify(data));배열을 문자열로 변경해줌
                //const temp = JSON.stringify(data);
                //console.log(JSON.parse(temp));//문자열로 된 데이터를 다시 배열로 변경해줌
                data.map((item) => {
                  console.log(item.title);
                  console.log(item.start);
                  console.log(item.username);
                })
                successCallback(data);
              }
            });///end of ajax
          },///////end of events
          //color: '#FF0000',
          //textColor: '#FFFF00'
        }]//////// end of eventSources
      })
      calendar.render()//위에서 초기화된 정보로 달력을 그려줘
    })

  </script>

</head>
<body>
  <div id="calendar"></div>
</body>
</html>

events2.json

[{
  "_id": 1,
  "title": "거래처 미팅",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-01 09:30",
  "end": "2023-11-01 15:00",
  "type": "회의실1",
  "username": "다현",
  "backgroundColor": "#D25565",
  "textColor": "#ffffff",
  "allDay": false
}, {
  "_id": 2,
  "title": "거래처 미팅",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-06 12:30",
  "end": "2023-11-06 15:30",
  "type": "회의실1",
  "username": "나연",
  "backgroundColor": "#D25565",
  "textColor": "#ffffff",
  "allDay": false
}, {
  "_id": 3,
  "title": "회의",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-12",
  "end": "2023-11-12",
  "type": "회의실4",
  "username": "다현",
  "backgroundColor": "#74c0fc",
  "textColor": "#ffffff",
  "allDay": true
}, {
  "_id": 4,
  "title": "회의",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-16",
  "end": "2023-11-16",
  "type": "회의실4",
  "username": "지효",
  "backgroundColor": "#74c0fc",
  "textColor": "#ffffff",
  "allDay": true
}, {
  "_id": 5,
  "title": "회의",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-18",
  "end": "2023-11-18",
  "type": "회의실2",
  "username": "지효",
  "backgroundColor": "#ffa94d",
  "textColor": "#ffffff",
  "allDay": true
}, {
  "_id": 6,
  "title": "회의",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-21",
  "end": "2023-11-21",
  "type": "회의실2",
  "username": "사나",
  "backgroundColor": "#ffa94d",
  "textColor": "#ffffff",
  "allDay": true
}, {
  "_id": 7,
  "title": "거래처 미팅",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-22",
  "end": "2023-11-22",
  "type": "회의실3",
  "username": "사나",
  "backgroundColor": "#a9e34b",
  "textColor": "#ffffff",
  "allDay": true
},{
  "_id": 8,
  "title": "회의",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-24 09:00",
  "end": "2023-11-24 10:00",
  "type": "회의실3",
  "username": "정연",
  "backgroundColor": "#a9e34b",
  "textColor": "#ffffff",
  "allDay": false
},{
  "_id": 9,
  "title": "거래처 미팅",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-24",
  "end": "2023-11-24",
  "type": "회의실2",
  "username": "정연",
  "backgroundColor": "#ffa94d",
  "textColor": "#ffffff",
  "allDay": true
},{
  "_id": 10,
  "title": "세미나참석",
  "description": "Lorem ipsum dolor sit incid idunt ut Lorem ipsum sit.",
  "start": "2023-11-25",
  "end": "2023-11-29",
  "type": "회의실2",
  "username": "다현",
  "backgroundColor": "#ffa94d",
  "textColor": "#ffffff",
  "allDay": true
}]

Ajax

picture.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./picture.css">
  <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
  <script>
    startMethod = (td) => {
      const cid =$(td).attr("id");//1,2,3,4
      console.log(cid);
      //console.log('startMethod');
      //$("#d_pic").html("사진에 마우스가 오버되었을때");//innerHTML로 사용했던 함수
      //Get방식 - 최초한번은 서버에 요청하고 응답을 받아오지만 동일한 요청이 반복되면 
      //인터셉트를 해서 버퍼캐시메모리에 있는 응답결과를 내보냄 
      //확인할 수 있나요? - 304 
      //POST방식 - 브라우저로 부터 인터셉트를 안당함 - 왜냐면 body 에 값이 담겨서 노출되지 않음
      //무조건 서버로 전달함
      $.ajax({
        type:"GET",
        url:"http://127.0.0.1:5501/23Ajax/pictureDetail.html?id="+cid+"&timestamp="+new Date().getTime(),
        dataType:"html",
        success:function(view){
          console.log(view);
          $("#d_pic").html(view)
          //document.querySelector("#d_pic").innerHTML=view;
        }
      })
    }
    clearMethod = () => {
      //console.log('clearMethod');
      //아래 함수는  jquery-1.12.4.js에서 제공함
      //min옵션은 들여쓰기 떼고 줄바꿈 떼고 파일크기를 최소화한 파일을 말함
      //속도차이에 영향이 있다 - 왜냐면 서버에서 클라이언트로 다운로드된 후에야 동작이 됨 - 3초안에 출력
      $("#d_pic").html("");//html()-태그는 인터프리터 됨와 text()-태그그대로 출력됨가 있는데 
    }
    moveTo = () => {
      //아래코드가 실행되면 새로운 페이지가 열린다 -http -stateless -비상태프로토콜
      //상태가 유지되지 않아서 쿠키와 세션을 공부한다- 유지문제
      //기존의 요청이 끊어지므로 기존에 쥐고 있던 값을 접근이 불가함
      //기존의 요청이 끊어지고 새로운 요청이 발생함
      location.href="pictureChange.html";
    }
  </script>
</head>
<body>
  <div id="d_pic">큰사진</div>
  <table border="1">
    <thead>
      <th colspan="2">그림 목록</th>
    </thead>
    <tbody>
      <tr>
        <td align="center">
          <img src="../../images/sample/회의-4.jpg" width="50" height="50" />
        </td>
        <td id="1" onmouseover="startMethod(this)" onmouseout="clearMethod()">
          사진1
        </td>
      </tr>
      <tr>
        <td align="center">
          <img src="../../images/sample/회의-1.jpg" width="50" height="50" />
        </td>
        <td id="2" onmouseover="startMethod(this)" onmouseout="clearMethod()">
          사진2
        </td>
      </tr>
      <tr>
        <td align="center">
          <img src="../../images/sample/회의-2.jpg" width="50" height="50" />
        </td>
        <td id="3" onmouseover="startMethod(this)" onmouseout="clearMethod()">
          사진3
        </td>
      </tr>
      <tr>
        <td align="center">
          <img src="../../images/sample/회의-3.jpg" width="50" height="50" />
        </td>
        <td id="4" onmouseover="startMethod(this)" onmouseout="clearMethod()">
          사진4
        </td>
      </tr>
    </tbody>
  </table>  
  <input type="button" value="이동" onclick="moveTo()">
</body>
</html>

pictureDetail.html

<img id="imgDetail"  alt="사진" />
<script src="https://code.jquery.com/jquery-1.12.4.min.js" defer></script>
<script>
  $(document).ready(function () {
    console.log('pictureDetai.html');
    const queryString = new URLSearchParams(location.search);
    console.log(queryString);
    const cid =  queryString.get("id");// 1 , 2 , 3, 4
    console.log(cid);
    const pics = ["회의-1.jpg","회의-2.jpg","회의-3.jpg","회의-4.jpg"];
    let img;
    for(let i=0;i<pics.length;i++){//i = 0, 1, 2, 3
      //선택한 id와 배열의 index가 같니?
      if((cid-1)==i){
        img = pics[i];
        console.log(img);
        $("#imgDetail").attr("src", "../../images/sample/"+img)
      }else{
        console.log('else');
      }
    }
  })

</script>

picture.css

div#d_pic {
  position: absolute;
}

pictureChange.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>새로운 요청임</title>
</head>
<body>
  바뀐 화면
</body>
</html>

Eclipse 실행

pictureChange.html

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>새로운 요청임</title>
</head>
<body>
  바뀐 화면
</body>
</html>

pictureAction.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	String cid = request.getParameter("id");
	//out.print(cid);
	String pics[] = {"회의-1.jpg","회의-2.jpg","회의-3.jpg","회의-4.jpg"};
	String img = null;
	int id = -1;
	if(cid != null){
		id = Integer.parseInt(cid);
	}
	for(int i=0;i<pics.length;i++){
		if(id==i){
			img = pics[id];
		}
	}
	out.print(img);
%>

0개의 댓글