클라이언트와 서버의 역할은 무엇이며, 통신에서 각각 어떤 기능을 수행할까?
HTML, CSS, JavaScript의 역할과 차이점은 무엇일까?
프론트엔드 개발자와 백엔드 개발자의 차이는 무엇인가? (난이도, 커리어 측면)









참고: 웹 브라우저의 변화
과거 익스플로러 곡점적 사용(윈도우 기본 브라우저였기 때문) → 보안 이슈와 성능 문제로 크롬이 점차 대세로 자리 잡음
스티브 잡스: 플래시 대신 자바스크립트 기반 웹 개발 선포 → 크롬 성장 촉진
익스플로러 공식 종료 후 발표된 엣지 또한 크롬 엔진을 사용하고 있어 최신 웹 개발은 크롬 브라우저를 사용하는 것이 매우 권장됨
1교시 정리
- 핵심 개념
- 웹 개발은 '클라이언트-서버' 통신 구조를 이해하고 HTML, CSS, 자바스크립트로 웹 페이지를 구성하는 과정 → 프론트엔드
- 핵심 단어
- 클라이언트
- 서버
- HTML
- CSS
- 자바스크립트
- 프론트엔드
- 백엔드
- 통신
- VScode
- Chrome
- 요약
- 웹 개발은 클라이언트와 서버 간 요청과 응답의 통신 구조를 이해하는 것에서 시작
- HTML, CSS, JavaScript가 결합되어 웹 페이지의 구조, 디자인, 동작을 완성함
- 프론트엔드 개발자와 백엔드 개발자는 역할, 난이도, 커리어 측면에서 차이가 있음
- AI 기술의 영향도 존재하지만 개발자의 역할의 중요성은 여전함
HTML 태그의 시작 태그와 닫는 태그의 역할과 차이는?
h1부터 h6까지 태그의 의미와 사용처는?
HTML에서 헤드 영역과 바디 영역의 역할은?


<tag> content </tag> → element(요소)<tag/>: 특정 내용에 표시를 하는 방법


.html 확장자 파일 만들고 코드 입력하는 곳에 ! 입력하고 tab 누르면 탬플릿 자동 완성됨html: 5 해도 바로 생성됨<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>

</tag> 형태로 닫을 필요가 없는 태그도 있음 (스스로 닫힘)<b></b> → 글자를 두껍게 만드는 태그
<h1>~<h6><p><span><br><strong>, <b>HTML에서 텍스트 정보를 담는 태그는 주로
<h1>~<h6>, <p>, <span>, <strong>, <em>, <code>등이 있습니다. 이러한 태그들은 텍스트의 역할(제목, 단락, 강조 등)과 의미를 정의하여 웹 페이지의 구조와 내용을 구성합니다.
<h1>~<h6>:
제목을 나타내는 태그입니다. 숫자가 낮을수록 중요도가 높고 글자 크기가 큽니다.
(<h1>- 가장 큰 제목,<h6>- 가장 작은 제목)
<p>:
단락을 나타내는 태그입니다. 텍스트를 문단으로 묶을 때 사용합니다.
<span>:
텍스트의 특정 부분에 스타일을 적용하거나 의미를 부여할 때 사용합니다. 주로 CSS와 함께 사용됩니다.
<strong>:
텍스트를 강하게 강조할 때 사용합니다. 굵은 글씨로 표시됩니다.
<em>:
텍스트를 강조할 때 사용합니다. 이탤릭체로 표시됩니다.
<code>:
컴퓨터 코드를 나타낼 때 사용합니다. 코드 조각을 표시할 때 유용합니다.
<a>:
하이퍼링크를 정의하는 태그입니다. 텍스트를 클릭하여 다른 페이지로 이동할 수 있게 합니다.
<li>, <ol>, <ul>:
목록을 정의하는 태그입니다.<li>는 목록 아이템을,<ol>은 순서가 있는 목록을,<ul>은 순서가 없는 목록을 나타냅니다.
예시:<h1>HTML 텍스트 태그</h1> <p>이것은 단락입니다. 문단을 구분할 때 사용합니다.</p> <p><span>일부 텍스트</span>를 강조할 수 있습니다.</p> <p><strong>중요한 텍스트</strong>는 강하게 표시됩니다.</p> <p><em>이탤릭체로 강조된 텍스트</em>입니다.</p> <p><code>코드 예시</code></p>

2교시 정리
- 핵심 개념
- HTML은 웹 페이지의 뼈대를 구성하는 마크업 언어
- 태그를 통해 컨텐츠의 의미와 구조를 표시
- 핵심 단어
- HTML
- Tag
- h 태그
- p 태그
- 요약
- HTML은 웹 페이지의 구조(뼈대)를 만드는 언어
- 헤드는 기능과 설정을 담당하고, 바디는 사용자에게 보이는 컨텐츠를 담당함
- 태그는 컨텐츠의 의미를 표시하며 시작 태그와 닫는 태그로 구성
- h 태그는 제목, p 태그는 문잗을 나타냄
p 태그와 span 태그의 차이점은?
br 태그 사용 시 주의해야 할 점은?
b 태그와 strong 태그의 차이점은?



<br><br></br>와 같은 형태의 종료 태그를 사용하지 않음
더 알아보기: 스스로 닫히는, 닫지 않아도 되는 태그들
- 명시적으로
</tag>형태로 닫을 필요가 없는 태그
- 종료 태그(
</tagname>) 없이 시작 태그만으로 구성된 요소들 → 빈 요소(Empty Element) 또는 Void Element라고 부르며, 셀프 클로징 태그(Self-Closing Tag)라는 용어와 연관되어 사용됨
- self-closing tags == empty tag == singleton tags
- 빈 요소(Empty Element)
- HTML 요소 중 내부에 어떠한 컨텐츠(텍스트나 다른 자식)도 포함할 수 없는 요소를 의미
- 일반적인 요소(예:
<p>내용</p>)가 시작 태그와 종료 태그 사이에 내용을 가지는 것과 달리 빈 요소는 그 존재 자체로 특정 기능을 수행(예: 줄바꿈)하거나 객체(예: 이미지)를 나타냄- 태그 내부에 텍스트나 컨텐츠를 갖지 않음
- 시작 태그와 종료 태그 사이에 들어갈 내용이 없기 때문에 '비어 있다'고 표현
- 요소의 의미와 기능은 태그 이름 자체와 태그에 포함된 속성(Attributes)에 의해 완전히 정의됨
- 자체적으로 닫아줘도 되지만 브라우저는 동일하게 해석하니 편한 방식으로 사용(optional slash)
<br>와<br/>: 브라우저에겐 동일하게 보임
<tagname>형식: HTML5에서는 슬래시 없이 작성하는 것이 더 간결하고 표준적인 방식으로 간주될 수 있음<tagname/>형식: 이 형식을 흔히 셀프 클로징(self-closing)이라고 함. XML, XHTML 문법 규칙에서 유래- 단 XTML에서는 self-closing tag를 닫아주는 것이 규칙이라고 함
- HTML 빈 요소와 셀프 클로징
- 중요한 건 어떤 형식을 사용하든 코드 가독성을 위해 프로젝트 내에서 일관된 스타일을 유지하는 것

<strong> 태그와 <b> 태그는 겉보기에는 비슷하지만, <strong> 태그는 의미적으로 중요한 부분을 강조하는 반면, <b> 태그는 단순히 텍스트를 굵게 표시하는 데 사용 → b 태그는 디자인 목적이며, strong 태그는 의미 강조 목적임3교시 정리
- 핵심 개념
- HTML 태그는 컴퓨터와 소통하기 위한 영역 표시 수단
- p 태그는 문단, span 태그는 특정 영역 표시
- br 태그는 줄바꿈 역할
- 핵심 단어
- p 태그
- span 태그
- br 태그
- b 태그
- strong 태그
- 요약
- p 태그는 문단을 만들며 한 줄 전체 공간을 차지하고 위아래 공백을 자동 생성함
- span 태그는 특정 글자 영역을 표시하며 줄바꿈 없이 글자 크기만큼의 공간을 차지
- br 태그는 줄바꿈을 위한 홀태그이며 태그 내 글자 사이에서만 사용
- b 태그는 단순 시각적 강조, strong 태그는 의미적 강조
BIO 태그 방식이 개체명 인식에서 중요한 이유는 무엇일까?
서브워드 토크나이징 후 레이블 정렬 문제를 해결하는 방법은?
word_ids 기반으로 테이블 정정이 필요한 이유는?
word_ids 기반으로 레이블 정정word_ids는 각 토큰이 "원래 몇 번째 단어에 속하는지" 알려주는 인덱스word_ids가 없음 → -100 값을 부여해 학습에서 무시하도록 처리: if word_idx is None: labels.append(-100)tokens = ['강', '릉', '휴', '게', '소', '에', '서'] # tokens
labels = ['B-LC', 'I-LC', 'I-LC', 'I-LC', 'I-LC', 'O', 'O'] # ner_tags
subwords = ['[CLS]', '강', '##릉', '##휴', '##게', '##소', '##에', '##서', '[SEP]']
| Subword | 붙은 라벨 |
|---|---|
[CLS] | B-LC ❌ |
강 | I-LC ❌ |
##릉 | I-LC ✅ |
##휴 | I-LC ✅ |
##게 | I-LC ✅ |
##소 | O ❌ |
##에 | O ✅ |
##서 | - 없음 ❌ |
[SEP] | - 없음 ❌ |
| Subword | word_ids | 정렬된 라벨 |
|---|---|---|
[CLS] | None | -100 |
강 | 0 | B-LC ✅ |
##릉 | 1 | I-LC ✅ |
##휴 | 2 | I-LC ✅ |
##게 | 3 | I-LC ✅ |
##소 | 4 | I-LC ✅ |
##에 | 5 | O ✅ |
##서 | 6 | O ✅ |
[SEP] | None | -100 |
# 토큰화 함수 정의
def tokenizer_function(example):
# 토큰화 → 학습에 필요한 input_ids, word_ids 값 등을 포함
tokenized_inputs = tokenizer(
example["tokens"] # 이미 띄어쓰기 된 토큰 리스트
, max_length=128
, truncation=True
, padding="max_length"
, is_split_into_words=True # 이미 글자 단위로 분리되어 있으므로 토큰화하지 말라는 의미 (NER 작업)
)
# NER 학습을 위하여 토큰화된 결과의 레이블을 BIO 인덱스로 변환
# word_ids: 각 토큰이 원래 입력의 몇 번째 단어에 해당하는지 인덱스 번호를 알려줌
word_ids = tokenized_inputs.word_ids()
labels = [] # subword 후의 태그 정보
previous_word_idx = None # 비교를 위한 변수: 이전 단어와 인덱스 번호 비교용
for word_idx in word_ids: # subwords를 labels에 매칭
if word_idx is None: # 현재 값이 padding이나 특수 토큰(시작 토큰, 끝 토큰)인 경우 학습 대상에서 제외하기 위함
labels.append(-100) # 학습이 필요 없는 대상은 무시 (-100: 내부적으로 무시하겠다는 의미로 적어주는 게 약속임)
elif word_idx != previous_word_idx: # 현재 인덱스와 이전 인덱스가 다름 → 새로운 단어라는 의미
labels.append(example["ner_tags"][word_idx])
else: # 같은 단어의 후속 단어(현재 인덱스와 이전 인덱스가 같음 → 한 개의 단어에서 쪼개진 경우): 그대로 유지
# 지금은 음절 단위로 넣어서 의미가 없지만 단어 단위로 넣었을 때는 의미가 있으
if label_list[example["ner_tags"][word_idx]].startswith('I'):
labels.append(example["ner_tags"][word_idx])
else:
labels.append(-100)
# 다음 루프 때 비교하기 위해 현재 word_idx를 저장(업데이트)
previous_word_idx = word_idx
# 길이 맞춰주기 → 설정한 max_length의 길이에 맞게끔 padding
labels += [-100]*(len(tokenized_inputs["input_ids"])-len(labels))
# 잘 매칭된 NER 정답 레이블을 학습용 딕셔너리에 추가
tokenized_inputs["labels"] = labels
# 반환
return tokenized_inputs
is_split_into_words=True 설정 주기labels.append(-100)의 역할과 적용["Hugging", "##Face", "is", "cool"] → [B-OG, B-OG, O, O]로 붙었다면?tokenized_dataset = klue_ner.map(tokenizer_function, batched=False)
from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
checkpoint
, num_labels=len(label_list)
, id2label = id2label
, label2id = label2id
)
!pip install -q seqeval
!pip install -q evaluate
# 평가 함수 정의
from evaluate import load
import numpy as np
metrics = load("seqeval")
def compute_metrics(eval_pred):
logits, labels = eval_pred
pred = np.argmax(logits, axis=2) # logit.shape → [batch_size, seq_length, num_labels]
# 예측 값, 실제 값 → BIO 형태의 문자열 담을 리스트
true_preds = []
true_labels = []
for pred_seq, label_seq in zip(pred, labels):
# -100 값을 제외한 결과를 담아줄 임시 리스트: 한 문장의 예측값, 실제값을 저장
temp_true_preds = []
temp_true_labels = []
# labels에서 -100 값은 평가에서 제외
for p, l in zip(pred_seq, label_seq):
if label != -100:
temp_true_preds.append(label_list[p])
temp_true_labels.append(label_list[l])
true_preds.append(temp_true_preds)
true_labels.append(temp_true_labels)
return metrics.compute(
predictions=true_preds
, references=true_labels
, zero_division=0
)
from transformers import TrainingArguments, Trainer
args = TrainingArguments(
output_dir="./results/klue-ner-koelectra" # 로컬에 저장된 걸 기준으로 허깅페이스에 업로드하기 때문에, 로컬 저장은 필수
, learning_rate=2e-5
, per_device_train_batch_size=8
, per_device_eval_batch_size=8
, num_train_epochs=20
, weight_decay=0.01
)
trainer = Trainer(
model=model
, args=args
, train_dataset=tokenized_dataset["train"]
, eval_dataset=tokenized_dataset["validation"]
, compute_metrics=compute_metrics
, tokenizer=tokenizer
)
trainer.train()

%cd /content/drive/MyDrive/Colab Notebooks/NLP
# 허깅페이스 로그인
from huggingface_hub import login
# 파일 형태의 api_key 불러오기
with open("./key/huggingface_api_key", 'r') as f:
api_key = f.read().strip()
login(token=api_key)
# 허깅페이스 업로드
repo_id = "사용자명/klue-ner-koelectra"
trainer.save_model(repo_id)
model.save_pretrained(repo_id)
tokenizer.save_pretrained(repo_id)
trainer.push_to_hub(repo_id)
# pipeline 사용하여 결과 확인
text = "이몽룡과 성춘향은 남원 광한루에서 자주 만났다"
# task: token-classification
# 안 쓰면 자동으로 설정됨 → 정확성을 위해 직접 작성해주기
from transformers import pipeline
checkpoint_mymodel = "be2be2/klue-ner-koelectra"
my_model = pipeline(task="token-classification", model=checkpoint_mymodel)
result = my_model(text)
result
[{'entity': 'B-PS',
'score': np.float32(0.33359873),
'index': 1,
'word': '이',
'start': 0,
'end': 1},
{'entity': 'I-PS',
'score': np.float32(0.3832988),
'index': 2,
'word': '##몽',
'start': 1,
'end': 2},
{'entity': 'I-PS',
'score': np.float32(0.3448925),
'index': 3,
'word': '##룡',
'start': 2,
'end': 3},
{'entity': 'B-PS',
'score': np.float32(0.30707076),
'index': 5,
'word': '성',
'start': 5,
'end': 6},
{'entity': 'I-PS',
'score': np.float32(0.34509343),
'index': 6,
'word': '##춘',
'start': 6,
'end': 7},
{'entity': 'I-PS',
'score': np.float32(0.32579824),
'index': 7,
'word': '##향',
'start': 7,
'end': 8},
{'entity': 'I-PS',
'score': np.float32(0.110316806),
'index': 9,
'word': '남원',
'start': 10,
'end': 12},
{'entity': 'I-OG',
'score': np.float32(0.13733202),
'index': 10,
'word': '광',
'start': 13,
'end': 14},
{'entity': 'I-OG',
'score': np.float32(0.17323461),
'index': 11,
'word': '##한',
'start': 14,
'end': 15},
{'entity': 'I-OG',
'score': np.float32(0.23224503),
'index': 12,
'word': '##루',
'start': 15,
'end': 16}]

추출적 질의응답에서 정답의 시작과 끝 위치를 찾는 이유는?
QA 전처리 과정에서 질문과 본문을 하나로 묶어 토크나이징 하는 이유는?
오프셋 매핑(offset mapping)이 역할은?
# 데이터 불러오기
from datasets import load_dataset
dataset = load_dataset("klue", "mrc", split="train[:1000]")
print(dataset)
Dataset({
features: ['title', 'context', 'news_category', 'source', 'guid', 'is_impossible', 'question_type', 'question', 'answers'],
num_rows: 1000
})
# 훈련용, 평가용(테스트용) 분리
dataset = dataset.train_test_split(test_size=0.2, seed = 6)
dataset
DatasetDict({
train: Dataset({
features: ['title', 'context', 'news_category', 'source', 'guid', 'is_impossible', 'question_type', 'question', 'answers'],
num_rows: 800
})
test: Dataset({
features: ['title', 'context', 'news_category', 'source', 'guid', 'is_impossible', 'question_type', 'question', 'answers'],
num_rows: 200
})
})
dataset["train"][0]
{'title': '서울 강북 재개발부터 광주·울산까지 ...불붙은 청약 1순위 마감 행렬',
'context': '청약 1순위 자격 완화 속에 저금리로 마땅한 투자처를 찾지 못한 시중의 부동자금이 분양시장에 몰리면서 청약 열기가 한층 뜨거워지고 있다. 기존 주택시장이 활기를 띠고 있는 지방부터 회복 속도가 더뎠던 서울 강북권 재개발 아파트까지 내 집 마련에 나선 실수요자와 분양권 매매 차익을 기대한 투자자들의 청약이 늘어난 결과라는 분석이다.26일 금융결제원에 따르면 서울 왕십리 뉴타운 3구역 센트라스 아파트는 지난 25일 1순위 청약접수 결과 1029가구 모집에 1만804명이 몰려 평균 10.5 대 1의 경쟁률로 모든 주택형이 마감됐다. 지하철 2호선 상왕십리역이 단지와 연결돼 도심은 물론 강남권 출퇴근이 쉬워 실수요 청약자들이 많았다는 지적이다. 2013년 분양 당시 중대형 아파트 미분양으로 몸살을 앓았던 인접 1구역 텐즈힐 단지가 최근 대부분 팔린 것도 긍정적인 요인으로 작용했다고 인근 중개사무소 관계자들은 설명했다.같은 날 경기 화성 동탄2신도시 A34블록에서 분양한 동탄2신도시 ‘에일린의 뜰’도 1순위 청약에서 443가구 공급에 5714명이 접수해 평균 12.8 대 1의 경쟁률을 보였다. 전용 74㎡는 청약 경쟁률이 최고 109.6 대 1에 달했다.지방에서도 1순위 마감 행진이 이어지고 있다. 전세가율(매매가 대비 전세금 비율)이 80%에 육박할 정도로 전셋값 상승세가 가파른 광주광역시에서 공급된 이안 광주 첨단 아파트는 292가구 모집에 광주 청약 1순위자만 1만6494명이 신청해 평균 경쟁률이 56.5 대 1에 달했다. 1가구를 모집한 전용 84㎡는 236명이 몰렸다. 울산 신정동 신정지웰도 2.7 대 1의 청약 경쟁률로 모든 주택형이 1순위에서 마감됐다.분양 계약 성적도 좋은 편이다. 지난 16일 13.7 대 1에 달하는 청약 경쟁률을 기록한 서울 마포 한강2차 푸르지오 오피스텔은 분양 계약 시작 1주일 만에 448실 모두 주인을 찾았다. 박원갑 국민은행 수석부동산전문위원은 “치솟는 전셋값과 주택 거래 증가, 저금리가 동시에 맞물리면서 청약 경쟁이 치열해지고 있다”면서도 “공급 물량이 크게 늘어난 만큼 입지와 분양가 등에 따라 청약 쏠림 현상이 나타날 것”으로 내다봤다.',
'news_category': '부동산',
'source': 'hankyung',
'guid': 'klue-mrc-v1_train_15439',
'is_impossible': False,
'question_type': 1,
'question': '에일린의 뜰 청약시 109명중 1명이 당첨된 전용 면적은 얼마인가?',
'answers': {'answer_start': [562], 'text': ['74㎡']}}
start_positions, end_position| 항목 | 설명 |
|---|---|
| 🎯 목적 | 질문 + 지문(context)을 토큰화하고, 정답의 시작/끝 위치를 토큰 인덱스로 변환 |
| 📎 입력 구성 | [CLS] question [SEP] context [SEP] 형태 |
| 🧭 위치 변환 | 정답의 문자 위치 → 토큰 위치 변환 (offset_mapping 사용) |
| ⚠️ 주의점 | context가 잘릴 경우 정답이 사라질 수 있음 → 이때 start=0, end=0 처리 |
offset_mapping: 현재 단어가 본문에서 몇 번째 문자 범위인지 알려주는 정보from transformers import AutoTokenizer
checkpoint = "monologg/koelectra-small-discriminator"
tokenizer = AutoTokenizer.from_pretrained(
checkpoint
)
6교시 정리
- 핵심 개념
- 추출적 질의응답은 문맥 내에서 질문에 대한 답변의 시작과 끝 위치를 찾아내는 과정
- 요약
- 추출적 질의응답 태스크 개념
- SQuAD
- 전처리 과정에서 질문과 본문을 하나로 묶어 토크나이징하고 정답 위치를 토큰 단위로 변환