Ⅰ. 오전 수업
A. 1교시
1. 지난 시간 복습
2. Public 폴더 사용해보기
B. 2교시
1. Public 폴더 사용해보기 (cont.)
2. GET 방식 통신 처리
C. 3교시
1. 작성한 코드 문제 찾기
Ⅱ. 오후 수업
A. 4교시
1. 지난 시간 복습
2. LangServe
B. 5교시
1. LangServe (cont.)
C. 6교시
1. LangServe (cont.)
Ⅲ. CAREER UP
현직자 특강
Ⅳ. 하루 돌아보기
npm i expressTIP: MPA vs. SPA
Express → MPA
REACT, VUE → SPA → views 폴더가 필요 없음!
driver = wb.Chrome())__dirname 키워드(환경변수) 사용__dirname: 현재 작업 중인 파일을 포함하는 폴더까지가 기준복습
a 태그 → href 속성
img 태그 → src 속성
link 태그 → href 속성 (CSS 연결)
script 태그 → src 속성 (JS 연결)


"로컬 PC 절대경로는 브라우저에서 직접 접근할 수 없어 웹상에서는 직접적인 사용이 불가능합니다."
Web browsers are designed with security in mind and, by default, prevent websites from directly accessing your local file system. This is a fundamental security measure to protect user privacy and prevent malicious websites from reading, modifying, or deleting your personal files without your explicit permission.
☞ Here's why this restriction exists:
Security:
Without this restriction, a malicious website could potentially access sensitive information on your computer, such as documents, passwords, or personal data, and transmit it to an attacker.
Privacy:
It prevents websites from tracking your activities or gathering information about your local environment without your consent.
Malware Prevention:
Direct file system access could be exploited to install or execute malware on your system.
☞ How websites can interact with local files (with user permission):
While direct access is blocked, websites can interact with local files through specific user-initiated actions and browser APIs:
File Input Element (<input type="file">):
This HTML element allows users to select files from their local system, which can then be uploaded to a server or processed by client-side JavaScript (e.g., using FileReader to read file content).
File System Access API:
Modern browsers offer APIs like the File System Access API, which allows web applications to request permission from the user to read or write specific files or directories on their local system. This access is granted on a per-file/directory basis and requires explicit user consent.
Drag and Drop:
Users can drag and drop files from their local system into a web application, triggering events that allow the website to process the dropped files.
.use() 사용 → 기능 설정, 제한 설정, 룰 설정, …RULE 1. 모든 경로는 반드시 절대경로를 활용: __dirname
RULE 2. 정적인 파일들은 무조건 public에 경유하게 세팅: app.use(express.static("public"));
<img src="img.jpg">
cf. sendFile은 참조(or경로 전달) 아닌 파일을 보내주는 거라 그냥 절대경로 씀
tip: HTML 파일에서
<html lang="en">표시하는 이유
SEO(검색 엔진 최적화) → lang="en"이면 검색 엔진이 외국 사이트라고 판단함
lang="ko"로 하면 한국 검색 엔진에 잘 잡힘
const express = require("express");
const app = express();
app.use(express.static("public")); // 지금 실습에서 필요하진 않지만 기본 구조라 생각하고 무조건 적기
app.get("/",(req,res)=>{
res.sendFile("/로그인.html")
});

sendFile()의 특징 때문!res.sendFile(__dirname+"/public/로그인.html")

action="http://localhost:3000/" → "3000번 포트에 누가 왔어요!" (app.listen(3000);) → 사용자가 어디로 들어왔나요? → 포트 번호 뒤에 아무것도 없는 메인으로 왔어요: app.get("/",(req,res)=>{res.sendFile("/로그인.html")}); 친구가 계속 메인 페이지를 다시 돌려줌 (값에 대한 처리가 없음) → "3000번 포트에 누가 왔어요!" → 메인 이동 → … → 계속 빙글빙글 돌고만 있음값을 "어디로" 보내느냐가 중요!
👩💻: 저희 데이터 보내려고 하는데 어디에다 보내야 해요?
👨💻: (아직 준비가 안 되어 있는데…)
👩💻: 저희 "http://localhost:3000/getLogin" 경로에 데이터 보낼게요~
👨💻: (getLogin 경로로 보낸 정보를 받아줄 새로운 업무를 서버에 추가해야겠다!)


res.sendFile() (파일 전송): 특정 파일을 클라이언트로 전송하여 브라우저에 직접 보여주는 방식res.redirect() (리디렉션): 클라이언트의 브라우저에게 다른 URL로 재요청하라는 응답(HTTP 30x 리디렉션)을 보내는 방식| 구분 | res.sendFile() | res.redirect() |
|---|---|---|
| 기능 | 파일 전송 | URL 이동 (재요청) |
| 클라이언트 동작 | 파일 내용 표시/다운로드 | 새로운 URL로 재요청 |
| URL 유지 여부 | 유지됨 | 변경됨 |
| 사용 예시 | index.html 파일 제공 | 게시글 등록 후 상세 페이지 이동 |
res.redirect("/");app.get("/",(req,res)=>{…}) 로직이 동작함res.redirect("/index.html");http://localhost:3000/index.html 파일로 바로 이동app.get("/",(req,res)=>{…}) 로직이 동작하지 않음 (코드 재사용이 안 됨)res.redirect("/");를 사용해 기존 코드를 사용 & 역할군을 확실히 나눠야 함!
const express = require("express");
const app = express();
app.use(express.static("public"));
app.get("/",(req,res)=>{
res.sendFile(__dirname+"/public/login.html")
});
app.get("/getLogin",(req,res)=>{
let data = req.query;
if(data.id=="test"&&data.pw=="1234"){
res.redirect("/");
}else{
res.redirect("/fail.html");
}
});
app.listen(3000);

action="http://localhost:3000/getLogin" http://localhost:3000/getLogin 뒤에 붙어서 전달됨: http://localhost:3000/getLogin?id=test&pw=1234app.get("/getLogin",(req,res)=>{…})로컬 환경에서 LLM 사용하기: LangServe, Ollama
# FastAPI, uvivcorn 설치
!pip install -q fastapi uvicorn
!pip install -q openai langchain-openai langchain langchain_community
!pip install -q langchain-teddynote
!pip install -q langserve[all]
# 파일에서 API 키 읽기
with open("./key/.openai_api_key", "r") as f:
api_key = f.read().strip()
os.environ["OPENAI_API_KEY"] = api_key
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
def langchain_fun(query):
# LLM 모델 생성
llm = ChatOpenAI(
model="gpt-4o-mini"
, temperature=0
, max_tokens=500
)
# 프롬프트 작성
prompt=ChatPromptTemplate.from_template("{input}")
# 체인 구성
chain=prompt | llm | StrOutputParser()
# 체인 실행
answer = chain.invoke(query)
# 결과값 반환
return answer
from fastapi import FastAPI # Python에서 API 서버를 쉽게 만드는 프레임워크.
import nest_asyncio # 주피터노트북 같은 환경에서 이벤트 루프 충돌을 막아주는 패키지.
import uvicorn # FastAPI 앱을 실행시켜주는 서버 엔진(ASGI 서버).
app = FastAPI() # app: 서버의 본체 역할 → API routes를 붙여 나감
nest_asyncio.apply() # 충돌 방지
@app.get("/{query}")
# LangChain 구동 함수
async def index(query):
return langchain_fun(query)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5000) # 서버 실행 (로컬 5000번 포트)
INFO: Started server process [17956]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:5000 (Press CTRL+C to quit)
INFO: 127.0.0.1:64474 - "GET / HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:64474 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 127.0.0.1:65314 - "GET /%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94 HTTP/1.1" 200 OK
INFO: 127.0.0.1:65314 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 127.0.0.1:60672 - "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:62460 - "GET /%EB%8B%B4%EA%B3%B0%EC%9D%B4%20%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94 HTTP/1.1" 200 OK
INFO: 127.0.0.1:62460 - "GET /.well-known/appspecific/com.chrome.devtools.json HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:62460 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [17956]

# FastAPI에 라우터를 추가하는 함수
from langserve import add_routes
# FastAPI 객체 생성
app = FastAPI()
nest_asyncio.apply()
# LLM 모델 생성
llm = ChatOpenAI(
model="gpt-4o-mini"
, temperature=0
, max_tokens=1000
)
# 목적에 맞는 chain 2개 생성하기: Prompt 별도 정의
prompt_1 = ChatPromptTemplate.from_template("나에게 {topic}에 대해서 농담해 줘")
prompt_2 = ChatPromptTemplate.from_messages([
("system","너는 여행 전문가이다")
, ("human", "{region}의 {target} 3군데를 알려주세요")
, ("ai", """출력은 다음과 같은 양식을 사용해주세요
[명칭1] 설명
[명칭2] 설명
[명칭3] 설명
""")
])
# LangChain 서버 라우터 설정
# 방법 1: 모델만 정의하고 프롬프트는 클라이언트에서 재정의하여 사용하기 위함
add_routes(app,llm,path="/llm_model")
# 방법 2: 서버에서 prompt_1과 LLM으로 이어지는 체인 연결
# 클라이언트가 topic(입력값)만 작성하면 서버에 고정된 프롬프트가 적용
add_routes(app,prompt_1|llm,path="/chain_1")
add_routes(app,prompt_2|llm,path="/chain_2")
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=5000)
/docs 확인해보기

from langchain.prompts import ChatPromptTemplate
from langchain.schema.runnable import RunnableMap
from langserve import RemoteRunnable
RemoteRunnable, RunnableMap
RemoteRunnable("http://127.0.0.1:5000/llm_model/") → 로컬 서버에 있는 llm 모델 가져오기RunnableMap({"llm_model": llm_model}) → 서버에서 가져온 모델을 연결해 사용하겠다는 의미llm_model = RemoteRunnable("http://127.0.0.1:5000/llm_model/")
# 사용자가 재정의한 프롬프트 작업 작업 수행
prompt = ChatPromptTemplate.from_messages(
[("system", "너는 동요 작사가이다"),
("user", "나에게 {topic}에 대한 동요에 대해 알려줘")]
)
# 체인 재정의
chain = prompt | RunnableMap({"llm_model": llm_model})
answer = chain.invoke({"topic": "거북이"})
print(answer["llm_model"].content)
물론이죠! 거북이에 대한 동요를 만들어볼게요. 아래는 간단한 동요의 예시입니다.
---
**거북이의 느린 걸음**
(1절)
거북이, 거북이, 느리게 가요,
작은 발로 뚜벅뚜벅, 길을 가요.
햇살 아래서, 풀밭을 지나,
천천히 걸어, 꿈을 찾아가요.
(후렴)
거북이, 거북이, 느리게 가도,
소중한 걸음, 멈추지 않아요.
작은 세상 속, 큰 꿈을 안고,
거북이처럼, 나도 나아가요.
(2절)
거북이, 거북이, 친구가 되어,
서로의 이야기를 나누어요.
바람이 불어, 나뭇잎 흔들려,
함께 노래해, 즐거운 하루예요.
(후렴)
거북이, 거북이, 느리게 가도,
소중한 걸음, 멈추지 않아요.
작은 세상 속, 큰 꿈을 안고,
거북이처럼, 나도 나아가요.
---
이런 식으로 거북이의 느린 걸음과 소중한 친구 관계를 담아보았습니다. 아이들이 쉽게 따라 부를 수 있도록 리듬감 있게 만들어 보았어요!
chain.invoke({"topic": "거북이"}) 하면 chain으로 딕셔너리 값이 들어감 → 맨 처음에 있는 prompt 안에 topic으로 거북이가 들어감 → 거북이가 들어간 prompt가 RunnableMap을 통해 llm_model로 들어감 → 로컬 서버 위치로 이동해 모델 실행# prompt_1, chain_1
chain_1=RemoteRunnable("http://127.0.0.1:5000/chain_1")
# 비동기 방식 진행
answer = await chain_1.ainvoke({"topic":"거북이"})
print(answer.content)
물론이죠! 여기 거북이에 대한 농담 하나 있어요:
왜 거북이는 항상 컴퓨터를 잘 못 다룰까요?
너무 느려서 마우스를 잡을 수가 없거든요! 🐢💻
재미있으셨나요?
# prompt_2, chain_2 직접 구동해보기 → 비동기 방식으로 진행
chain_2=RemoteRunnable("http://127.0.0.1:5000/chain_2")
# 비동기 방식 진행
answer = await chain_2.ainvoke({"region":"싱가포르", "target":"맛집"})
print(answer.content)
[치킨 라이스] - 싱가포르의 대표적인 요리로, 부드러운 닭고기와 향긋한 쌀밥이 함께 제공됩니다. 보통 고수와 함께 간장 소스를 곁들여 먹으며, 현지의 다양한 식당에서 맛볼 수 있습니다.
[락사] - 매콤한 코코넛 밀크 국물에 쌀국수와 해산물, 두부가 들어간 인기 있는 국수 요리입니다. 다양한 종류의 락사가 있지만, 특히 카통 락사가 유명합니다.
[칠리 크랩] - 싱가포르의 대표 해산물 요리로, 신선한 꽃게를 매콤한 칠리 소스와 함께 조리한 요리입니다. 빵과 함께 소스를 찍어 먹는 것이 일반적이며, 현지 식당에서 꼭 시도해볼 만한 메뉴입니다.