문서화 기반 개발이란 말 그대로 문서를 중심으로 요구사항 도출, 디자인, 개발, 테스트를 하나의 피드백 루프로 통합하는 접근 방식입니다. 기존의 코드 중심 개발(Code-Driven Development)과 달리 개발의 진입점(entry point)이 문서입니다. 팀은 문서를 작성하며 시스템의 동작을 정의하고, 이 문서가 곧바로 설계 명세서가 되며, 나아가 테스트의 기준선 역할까지 수행합니다.
문서가 곧 팀의 공통 언어이자 설계서이고, 또 코드에 대한 검증 기준이 되는 셈입니다. 잘 정립된 문서는 개발자, 디자이너, 기획자 모두가 바라보는 하나의 진실된 소스(source of truth)로 기능하며, 문서에 쓰인 대로 시스템이 동작하도록 코드를 구현하고 테스트까지 진행하게 됩니다. 이런 사이클에서는 문서와 코드의 괴리가 줄어들고, 문서만 읽어도 현재 시스템이 무엇을 하는지 이해할 수 있게 됩니다.
문서화가 어려운 이유
그렇다면 왜 현실에서는 문서화 기반 개발이 잘 이루어지지 않을까요? 문서화가 어려운 대표적인 이유 몇 가지를 짚어보겠습니다:
문서도 유지보수가 필요한 “소프트웨어”다 – 코드에 리팩터링이 필요하듯 문서에도 지속적인 업데이트와 구조 개선이 필요합니다. 하지만 많은 조직에서는 코드 리팩터링에는 공을 들이면서도 문서 리팩터링에는 거의 리소스를 할애하지 않습니다. 시간이 지날수록 코드와 문서의 불일치가 커지고, 문서는 신뢰를 잃게 됩니다.
문서는 이타적인 행위다 – 문서 작성은 당장 개인의 성과로 드러나기보다는 팀 전체를 위한 공유 행위입니다. 업무에 쫓기는 환경에서는 개발자가 기능 구현이나 버그 수정처럼 눈앞의 일에 집중하게 되고, 문서 작성은 우선순위에서 밀려나기 쉽습니다. 특히 빠른 결과를 요구하는 비즈니스 환경에서는 문서가 쉽게 뒷전으로 밀려나 결국 내용이 오래되기 쉽습니다.
문서보다 비즈니스가 더 빠르다 – 요즘 같은 빠른 실험과 MVP 중심 개발 문화에서는 문서화 속도가 비즈니스 변화를 따라가기 어렵습니다. 제품은 수시로 피벗하고 기능은 계속 바뀌는데, 그때마다 문서를 모두 수정하기란 현실적으로 버겁습니다. 결국 문서는 실제 제품보다 항상 한 발 뒤처지게 되죠.
이러한 문제를 해결하려면, 사람의 수동적인 노력만으로는 한계가 있고 자동화된 문서화 시스템이 필수적입니다.
MCP를 통한 문서 자동화 시스템
이 자동화된 문서화 시스템의 한 예로 떠오르는 것이 바로 MCP(Multi-Context Protocol)입니다. MCP는 Anthropic이 2024년에 제안한 새로운 표준으로, 요약하면 다양한 소프트웨어 툴과 AI를 연결해주는 프로토콜입니다
Notion, Jira, Google Sheets, Figma처럼 서로 다른 플랫폼들의 문서를 AI 에이전트가 읽고 요약하거나 서로 연결할 수 있게 해주는 통합 규약이라고 볼 수 있습니다. 한마디로 여러 곳에 흩어진 문서와 데이터를 한곳에 모아 활용할 수 있게 만드는 문맥 통합 도구입니다.
각각의 도구(Figma, Notion, Jira 등)가 자신만의 MCP 서버로 열려 있다고 가정하면,
AI 에이전트나 통합 스크립트가 이들을 차례차례 호출하여 요구사항 → 티켓 → 코드 -> 테스트 코드를 자동으로 엮어낼 수 있습니다. 곧 요구사항 문서가 디자인과 코드 생성으로 이어지고, 작업 완료 시 Jira 티켓이나 노션 페이지의 상태가 자동 갱신되는 등 문서→개발→피드백의 루프가 자동화될 수 있음을 보여줍니다.
문서 → 디자인 → 개발 → 테스트 → 피드백 사이클
앞서 언급한 자동화 루프를 조금 더 구체적으로 살펴보겠습니다. 문서화 기반 개발 사이클에서는 문서 작성부터 디자인 시안, 코드 구현, 테스트, 그리고 다시 문서 갱신까지가 하나의 흐름으로 이어집니다. 이를 단계별로 나타내면 다음과 같습니다
요구사항 문서가 변경되면, 그 내용이 Figma에 반영되고, 디자인이 확정되면 코드 저장소에 해당 컴포넌트나 코드 스켈레톤이 생성됩니다.
개발자가 그 코드를 구현하면 CI 상에서 테스트 코드가 돌아가고, 여기에는 MSW(Mock Service Worker)로 구현한 가짜 API 서버까지 포함하여 통합 테스트가 실행됩니다. 모든 테스트가 통과하면 그 결과가 다시 노션 문서나 Jira 티켓에 자동으로 댓글로 남는다든지 상태를 “완료”로 변경한다든지 하는 식으로 문서와 티켓이 최신화됩니다.
이렇게 함으로써 문서의 변경 → 코드 변경 → 테스트 → 문서 갱신의 순환 고리가 자동으로 돌아가게 됩니다. 결국 문서가 변경되면 코드와 테스트 결과로 증명되고, 테스트가 실패하면 문서나 코드 중 어딘가 잘못된 부분을 알려주며, 코드 변경도 문서에 자동 기록되는 생태계가 만들어지는 것입니다.
이 순환 구조의 핵심은, 문서와 코드, 테스트가 동등한 1급 시민으로 취급된다는 점입니다. 어느 하나라도 변경되면 나머지 둘이 이를 검증하고 보완해주는 삼각형 구조라고 볼 수 있습니다. 이러한 체계에서는 문서도 항상 최신의 기능을 반영하게 되고, 팀원 누구든 문서를 보면 현재 시스템의 동작과 일치하는 내용을 접하게 됩니다.
테스트 코드도 문서다
테스트 코드는 개발자가 기대한 시스템의 동작을 코드로 서술한 것으로, 일종의 “실행 가능한 문서”입니다. 예를 들어 사용자의 로그인 후 동작을 다음과 같이 테스트 코드로 명세할 수 있습니다. 또한, 테스트 코드는 개발자가 기대한 시스템의 동작을 코드로 서술한 문서이며 AI로 만든 산출물에게 바로 강력한 피드백을 줄 수 있는 방법입니다. 즉, 테스트는 “읽히는 문서”이면서 동시에 “실행 가능한 명세(Executable Specification)”입니다.
위 테스트는 자연어 문장(테스트 설명)과 코드 구현이 결합되어 시스템의 요구사항을 스스로 검증합니다. 테스트 설명 부분을 보면 마치 사양서의 한 문장처럼 “사용자는 로그인 후 대시보드로 이동해야 한다”라고 적혀 있습니다. 이 문장은 해당 기능의 요구사항을 나타내는 문서 역할을 하고, 아래의 코드 구현부는 실제 로그인 함수 login(user)의 반환값(response.redirect)이 기대한 대로 “/dashboard”인지 확인함으로써 그 문서의 내용을 검증합니다.
문서가 “무엇을 해야 하는지” 설명한다면, 테스트는 “정말 그렇게 되는지”를 증명합니다 다시 말해, 문서가 코드의 그림자라면 테스트 코드는 그 그림자의 진위를 증명하는 거울과도 같습니다. 테스트가 통과하지 못한다는 것은 곧 해당 기능에 대한 문서(또는 코드)가 실제 동작과 어긋남을 의미하므로, 테스트 실패를 통해 우리는 문서가 낡았거나 코드에 결함이 있음을 즉시 알아낼 수 있습니다. 이처럼 잘 작성된 테스트 코드는 팀에게 시스템의 신뢰성을 제공함과 동시에, 현재 시스템이 문서대로 작동하고 있음을 보여주는 살아있는 증거가 됩니다.
🧪 MSW를 통한 통합 테스트 기반 피드백
그렇다면 앞서 예로 든 테스트 코드에서 실제 API 호출이나 외부 서비스와의 통신은 어떻게 다룰까요?
여기서 등장하는 것이 바로 MSW(Mock Service Worker)입니다.
MSW는 브라우저나 Node 환경에서 서비스 워커 API를 활용하여 실제 네트워크 요청을 가로채고, 개발자가 정의한 모의 응답(Mock response)을 반환해주는 라이브러리입니다. 이를 이용하면 별도의 테스트용 서버를 띄우지 않고도 네트워크 계층을 포함한 통합 테스트를 손쉽게 구현할 수 있습니다. 다시 말해, 프론트엔드 관점에서 클라이언트-서버 간의 상호작용 전체를 테스트 코드에서 재현할 수 있게 됩니다.
예를 들어 “사용자가 로그인에 성공하면 토큰을 로컬스토리지에 저장한다”는 시나리오를 테스트하려 한다면, MSW로 로그인 API의 응답을 가로채어 성공 케이스를 미리 정의해둘 수 있습니다. 아래 코드는 MSW로 가짜 로그인 API 핸들러를 만들고, 이를 테스트에서 사용하는 예시입니다:
// 로그인 API 성공 응답 핸들러 정의
export const createLoginSuccessHandler = (mockTokenResponse: TokenResponse) => {
return http.post(`${API_BASE_URL}${AUTH_ROUTES.LOGIN}`, async ({ request }) => {
const body = await request.json();
// ... 요청 바디 검증 로직 등이 들어갈 수도 있음
return HttpResponse.json<ApiResponse<TokenResponse>>({
code: API_RESPONSE_CODES.OK,
message: "정상적으로 처리되었습니다.",
data: mockTokenResponse,
});
});
};
// 테스트 시나리오 구현 (Jest + Testing Library 예시)
describe("when 유효한 이메일을 입력하고 로그인 버튼을 클릭할 때", () => {
it("to be: 로그인이 성공하고, 토큰이 localStorage에 저장되어야 함", async () => {
const mockTokenResponse = { memberId: 1, token: "test-token" };
const testServer = createTestServer([createLoginSuccessHandler(mockTokenResponse)]);
// 테스트용 서버(MSW) 시작
testServer.listen();
renderWithProviders(<LoginForm />);
// 사용자 입력 시뮬레이션
await userEvent.type(screen.getByLabelText("이메일"), "test@example.com");
await userEvent.click(screen.getByRole("button", { name: "로그인" }));
// 결과 확인: 로컬스토리지에 토큰 저장됐는지 검증
await waitFor(() => {
expect(authApi.getToken()).toBe("test-token");
});
// 테스트용 MSW 서버 종료
testServer.close();
});
});
위 테스트는 실제로 백엔드 서버를 띄운 적은 없지만, MSW를 통해 마치 진짜 서버와 통신하듯 로그인 과정을 재현했습니다.
Testing Library를 사용해 사용자의 입력 이벤트를 흉내내고, MSW가 준비한 가짜 응답(test-token)을 받아 로컬스토리지가 업데이트되었는지를 검증함으로써, 전체 로그인 흐름이 문서 요구사항대로 이루어지는지를 자동 테스트한 것입니다. MSW 덕분에 테스트 환경의 리액트 컴포넌트는 실제 API와 대화한다고 믿고 있지만, 뒤에서는 우리의 가짜 서버가 응답을 주고 있기에 네트워크 의존성 없이도 신뢰성 있는 통합 테스트가 가능합니다.
중요한 점은, 이러한 테스트가 단순히 기능이 동작하는지를 넘어 문서에 정의된 시나리오가 제대로 구현되었는지”를 검증한다는 것입니다. 즉, 문서 → 코드 → 테스트의 일관성이 확보되는 것이죠.
MSW를 통한 테스트는 실패할 수도 있습니다(예를 들어, 문서에 따른 시나리오대로 동작하지 않으면 테스트가 실패). 그 실패는 개발팀에게 곧바로 피드백으로 돌아옵니다. “무엇이 틀렸는가? 문서의 시나리오가 잘못됐는가, 아니면 구현된 코드에 버그가 있는가?”와 같은 질문을 던지게 함으로써, AI는 문서와 코드의 불일치를 조기에 발견하고 수정할 수 있습니다.
이처럼 MSW + 테스트 코드 조합은 문서로부터 출발한 기대 동작을 실제 코드에서 자동으로 검증해주는 피드백 루프를 실현합니다.
결론 – 문서와 코드, 테스트의 삼각 루프
MCP와 같은 기술을 통해 문서와 디자인, 데이터가 연결되고, MSW와 테스트 코드를 통해 문서의 내용이 자동으로 검증될 수 있습니다. 이러한 환경에서는 문서가 곧 실행 가능한 스펙이 되고, 코드와 테스트는 그 스펙을 구현하고 확인하는 역할을 합니다. 문서가 바뀌면 코드가 바뀌고, 코드가 바뀌면 테스트가 이를 잡아내고, 테스트 결과는 다시 문서에 반영되는 선순환이 자리잡게 됩니다.
결국 “문서가 살아 있으면 팀도 살아 있다.” 문서가 최신 상태로 실행되고 있다면, 그 팀은 공유된 이해를 바탕으로 빠르게 움직일 수 있습니다. 그리고 “테스트는 문서의 진실을 증명하는 거울”이 되어, 우리의 문서(요구사항)가 정말 현실에서 지켜지고 있는지를 항상 비춰줍니다. 문서-코드-테스트가 삼위일체가 된 개발 문화야말로 빠른 변화 속에서도 품질과 이해도를 높이는 길일 것입니다
P.S) 1~2주동안 써봤는데, 문서 기록, jira 연동, 개발이 빠르게 연동되어 개발 외 일의 생산성이 매우 높아져 아주 만족도가 높습니다..!
요즘 MCP(Model Context Protocol)를 활용해 실무에 어떤 기능들을 적용할 수 있을지 살펴보고 있습니다. 그중에서도 특히 Cursor 에디터와 Figma를 MCP로 연동하여 디자인-to-코드 퍼블리싱 워크플로우를 자동화하는 방법을 테스트해보았는데요. 이번 글에서는 그 과정을 정리해서 공유드리고자 합니다.
개인적으로는 GAN(생성적 적대 신경망) 구조에서 영감을 받아, 마치 Generator(코드 생성)와 Discriminator(코드 리뷰)가 서로 경쟁하며 결과물을 개선하는 형태로 구현해 보았습니다.
MCP(Model Context Protocol)는 AI 코드 에디터(예: Cursor, Claude Desktop 등)가 로컬 리소스나 외부 API에 접근할 수 있도록 해주는 표준 프로토콜입니다. 쉽게 말해 “AI가 Figma API, GitLab, Notion, DB 등 외부 도구를 직접 불러와 사용할 수 있게 하는 인터페이스"라고 보면 됩니다.
조금 더 풀어서 설명하자면, MCP 환경에서는 MCP 서버가 어댑터(Adapter) 역할을 하고, Cursor와 같은 에디터는 클라이언트(Client) 역할을 합니다. 예를 들어 MCP 서버가 Figma API와 통신하여 디자인 정보를 가져오면, Cursor는 그 정보를 받아와 코드 생성 등에 활용할 수 있게 되죠. MCP는 프로토콜이기 때문에 Cursor뿐만 아니라 Codex, Claude Code 등 여러 AI 코딩 플랫폼에 동일하게 연결할 수 있습니다. (이 글에서는 주로 Cursor와 codex를 기반으로 설명드릴게요.)
⚙️ 2. 사전 준비
본격적인 자동화 실험을 하기 전에 몇 가지 준비 사항이 있었습니다. 아래 항목들을 미리 갖춰야 했어요:
Cursor 에디터: MCP 클라이언트를 지원하는 최신 버전이 필요합니다 (2024.10 이후 버전). 저는 Cursor를 사용했고, MCP 설정을 할 수 있는지 꼭 확인해야 합니다. 꼭 cursor가 필요한건 아닙니다만, 전 cursor에서 진행했습니다.
Codex(혹은 claude code 등): 실제 리뷰를 진행할 에이전트입니다. MCP 내부에서 코드 리뷰 AI 서버로 이용하며, 로그인이 되어 있어야합니다.openai API를 사용해도 되긴하는데, 해당 방법시 사용 토큰당 과금 요소가 필요합니다.
Node.js: v18 이상이 설치되어 있어야 합니다 (MCP 서버 실행용으로 사용).
Figma Personal Access Token: Figma API에 접근하기 위한 개인 액세스 토큰이 필요합니다. Figma 계정의 설정(Security 탭)에서 생성할 수 있어요. 참고: 발급은 Figma 개발자 사이트에서 할 수 있으며, Dev Mode 활성화 등 권한이 필요합니다.
Figma File Key: MCP로 연동하려는 Figma 파일의 고유 키 값입니다. 해당 파일의 URL에서 확인할 수 있습니다 (파일 URL 중 file/{파일Key} 형태로 포함되어 있어요).
위 준비를 마쳤다면, 이제 Cursor에 MCP 서버를 연결해야 합니다. Figma MCP 서버로는 오픈소스인 Figma-Context-MCP 프로젝트를 활용했습니다. Cursor 설정(Settings)에서 MCP 섹션을 열어, 공식 가이드에 따라 Figma MCP를 추가로 등록합니다. (자세한 설정 방법은 GLips/Figma-Context-MCP 깃허브와 Framelink의 Figma MCP Quickstart 문서를 참고했습니다.)
Cursor 설정 파일에 MCP 서버를 등록할 때 아래와 같은 JSON 구성을 넣어주면 됩니다:
위와 같이 설정하고 MCP 서버(Figma MCP)를 실행하면, Cursor와 Figma 간 연동 준비가 완료됩니다. 이제 Cursor에서 Figma 디자인 정보를 불러와서 코드 생성을 시작할 수 있어요! 😃
이제부터는 실제로 Figma 디자인을 가져와 코드로 변환하고, AI 코드 리뷰 에이전트를 통해 자동으로 개선하는 과정을 단계별로 설명하겠습니다. 전체 흐름을 요약하면 “Figma 디자인 -> 코드 생성 -> AI 코드 리뷰 -> 수정 반영”의 반복 루프라고 볼 수 있는데요, 이는 마치 앞서 언급한 GAN의 제너레이터-디스크리미네이터가 서로 경쟁하며 결과물을 개선하는 것과 비슷합니다.
실제 workflow, 해당 스크린샷은 figma 자체 제공MCP를 활용한 화면
그럼 1단계부터 차근차근 살펴보겠습니다.
1.1 Figma MCP를 통한 디자인 데이터 추출
우선 Figma에서 구현하려는 UI 컴포넌트를 하나 선택했습니다. 예를 들어, 아래와 같은 Figma 디자인 파일의 특정 컴포넌트를 가져온다고 해볼게요:
jira의 Calender component
@Figma 디자인 링크 위 디자인 컴포넌트를 shared 폴더 내에 구현해주세요. React + Tailwind를 사용하여 UI를 만들고, 코드의 응집도(cohesion)를 높이는 한편 결합도(coupling)는 최소화해주세요. 비즈니스 로직과 UI는 hook 또는 props를 활용해서 분리하도록 합니다.
위와 같은 프롬프트를 Cursor에 입력하면, MCP를 통해 해당 Figma 노드(컴포넌트)의 정보가 AI에게 전달됩니다. 그러면 Cursor의 코드 생성 AI가 지시에 따라 React + Tailwind CSS 조합으로 해당 컴포넌트의 코드를 생성합니다. 저는 프롬프트에 디자인 구현 시 “응집도를 높이고 결합도를 줄여라”, “로직과 UI 분리” 등의 베스트 프랙티스도 함께 써주어, 생성된 코드 품질을 높이려고 했어요. 😀
1.3 Compound Pattern 적용 (컴파운드 패턴)
코드를 생성할 때 고려한 중요한 아키텍처 원칙 중 하나는 Compound Component Pattern의 적용이었습니다. 복잡한 UI를 구현하다 보면 Props drilling(상위 컴포넌트의 props를 여러 단계 하위로 전달)이 많이 발생할 수 있는데요, 이를 줄이기 위해 컴파운드 패턴으로 컴포넌트를 설계했습니다. 즉, 하나의 컴포넌트를 여러 하위 구성요소로 쪼개어 구성하는 방식입니다.
예를 들어 Card 컴포넌트를 Compound Pattern으로 구성하면 다음과 같은 형태가 됩니다:
위처럼 컴포넌트의 계층을 명확히 분리하면, 필요한 데이터나 함수를 하위 컴포넌트에 전달할 때 중간 단계의 불필요한 props 전달을 줄일 수 있습니다. 또한 각 부분을 독립적인 컴포넌트로 재사용할 수도 있어 응집도를 높이면서도 결합도는 낮게 유지할 수 있죠.
1.4 필요시 Provider 패턴 사용
컴파운드 패턴으로 대부분의 UI 상태 전달 문제를 해결했지만, 만약 그것만으로 부족한 복잡한 상태 관리가 필요할 경우에는 Context API를 활용한 Provider 패턴을 적용했습니다. 예를 들어 전역적인 상태 공유나 깊은 트리 구조에서의 데이터 전달이 필요한 경우 Context를 사용하면 유용합니다:
// Context Provider 패턴 예시
const AppContext = createContext();
const AppProvider = ({ children }) => {
// 공유할 상태와 로직 정의
const value = { /* 상태 값들 */ };
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
위처럼 Context를 쓰면 컴포넌트 트리 어디서든 AppContext를 통해 값에 접근할 수 있으므로, props drilling 없이도 필요한 데이터를 주고받을 수 있어요. 다만 Context 사용은 전역으로 상태를 공유하는 것이므로, 정말 필요한 경우에만 제한적으로 사용하고 기본적으로는 컴파운드 패턴 내에서 해결하는 것을 우선으로 했습니다.
이렇게 해서 1단계에서는 Figma 디자인 -> 초안 코드 생성까지 완료되었습니다. 이제 생성된 코드를 AI 에이전트를 통해 리뷰하고 개선하는 단계로 넘어가겠습니다.
2단계: 코드 리뷰 및 개선 (반복 루프)
2단계에서는 앞서 생성된 코드를 다양한 관점에서 AI로 리뷰하고, 피드백을 반영하여 코드를 개선하는 과정을 거쳤습니다. 저는 이를 세 가지 종류의 리뷰 에이전트를 통해 여러 번 반복적으로 수행했는데요, 첫 번째는 일반 코드 품질 리뷰, 두 번째는 웹 접근성(WCAG) 관점 리뷰, 세 번째는 아키텍처 및 코드 구조 리뷰였습니다.
이 부분에서 저는 MCP를 이용해 직접 만든 코드 리뷰 에이전트를 활용했습니다. 이 MCP 서버는 OpenAI의 Codex(프로그래밍 특화 모델)나 기타 LLM을 활용해 코드의 개선점을 알려주는 역할을 합니다. 쉽게 말해, 제 코드 리뷰 에이전트(MCP 서버)는 AI에게 코드를 보여주고 “문제점을 찾아줘”라고 물어보면 여러 가지 분석 결과를 주는 비서 같은 존재입니다. 이를 통해 사람 대신 AI가 1차로 리뷰를 해주니 상당히 편하더군요!
각 리뷰를 진행한 이휴, 리뷰 결과를 바로 100% 신뢰하기보다는 제가 한 번 더 정리하고 핵심만 추출하는 과정을 거쳤습니다. AI가 장황하게 피드백을 주기도 하고, 가끔 맥락에 맞지 않는 제안을 할 때도 있어서, 필터링과 우선순위화를 사람이 도와주는 셈이죠. 그럼 각 리뷰 단계별로 어떤 작업을 했는지 살펴보겠습니다.
로컬에 띄워놓고, codex를 사용하여 리뷰 및 검증후 MCP 프로토콜이 넘기는 형태로 구현했습니다.
2.1 Codex 기반 일반 코드 리뷰
먼저 Codex 기반의 일반 코드 품질 리뷰를 수행했습니다. 이 리뷰는 생성된 코드의 전반적인 품질, 가독성, 버그 여부 등을 점검해주는 역할입니다. 제 코드 리뷰 MCP 에이전트에서 이 기능을 호출하는 명령은 다음과 같았습니다:
`Analyze the following TypeScript code and provide a comprehensive code review focusing on:
1. **Context**: What does this code do?
2. **Security Issues**: Any security vulnerabilities or concerns
3. **Performance Issues**: Performance bottlenecks or inefficiencies
4. **Architecture Issues**: Design patterns, coupling, separation of concerns
5. **Logic Issues**: Potential bugs, edge cases, logical errors
Note: Do not include barrel export related content in your analysis.`
피드백 정제 과정: Codex로부터 분석 결과(JSON 형태 혹은 텍스트 리포트)가 오면, 다음과 같은 과정을 거쳐서 피드백을 정리했습니다.
AI 리뷰 결과 해석: Codex가 내놓은 리뷰 내용을 쭉 읽어봅니다. 보통 코드 스타일 지적, 잠재 버그, 최적화 제안 등이 섞여 나오는데, 일단 빠르게 훑어보죠.
핵심 개선사항 추출: 리뷰 결과에서 정말로 중요한 지적사항이나 코드 품질에 큰 영향을 미치는 포인트를 뽑아냅니다. (예: "함수 A의 복잡도가 높다", "불필요한 변수 선언이 있다" 등)
우선순위 결정: 추출된 개선사항들에 우선순위를 매깁니다. 바로 고치지 않으면 위험한 버그 -> 리팩토링하면 좋은 부분 -> 스타일 사항 순으로 중요한 것부터 순위를 정했어요.
구체적인 수정 방향 계획: 각 중요한 지적에 대해 어떻게 수정할지 방향을 간략히 정리합니다. 예를 들어 "함수 A 복잡도 개선"이라면, 함수 쪼개기를 고려한다든지, "변수 최소화"라면 불필요한 변수를 삭제하거나 로직 단순화 계획을 세우는 식이죠.
이렇게 핵심만 추린 후에, 우선순위 높은 항목부터 코드를 개선했습니다. Codex의 제안을 맹신하기보다는 참고하는 수준으로 활용하고, 제가 수용할 부분은 수용하고 아닌 부분은 걸러냈습니다.
2.2 웹 접근성 체크 (Accessibility Agent 리뷰)
다음으로, 웹 접근성(WCAG) 관점에서의 리뷰를 진행했습니다. 아무리 UI를 잘 구현해도, 시멘틱 마크업이나 접근성이 떨어지면 실제 서비스에서 품질이 낮아질 수 있으니까요. 이를 위해 MCP 리뷰 에이전트의 accessibility 모드를 활용했습니다. 명령어는 아래와 같습니다:
`You are a Senior Frontend Publisher with 10+ years of experience in web accessibility and semantic web development. Analyze the following React/TypeScript code focusing specifically on:
1. **Web Accessibility (WCAG 2.1/2.2)**:
- ARIA attributes usage and correctness
- Keyboard navigation support
- Screen reader compatibility
- Color contrast and visual accessibility
- Focus management and tab order
- Semantic HTML structure
2. **Semantic Web Standards**:
- Proper HTML5 semantic elements usage
- Microdata or JSON-LD structured data
- Meta tags and SEO optimization
- Progressive enhancement principles
3. **React Accessibility Best Practices**:
- Proper use of React Aria hooks
- Accessible form controls and validation
- Error message accessibility
- Component accessibility patterns
4. **Performance & UX for Accessibility**:
- Loading states and skeleton screens
- Error boundaries and fallbacks
- Responsive design considerations
- Animation and motion preferences`
피드백 정제 과정: 접근성 리뷰 결과에 대해서도 다음과 같이 정리했습니다.
WCAG 준수 여부 확인: AI가 지적한 내용 중 웹 콘텐츠 접근성 가이드라인(WCAG)에 비춰 심각하게 위반되는 사항이 있는지 체크했습니다. (예: 컨트라스트 비율 부족, 폼 레이블 누락 등)
시맨틱 HTML 사용 검토: <div>로 떼운 부분이 없는지, 적절한 시맨틱 태그(<header>, <main>, <button> 등)를 썼는지 지적된 부분을 살폈습니다. 혹시 빼먹은 시맨틱 태그가 있다면 수정 계획에 포함했습니다.
키보드 내비게이션/포커스: 키보드만으로 UI 조작이 가능한지, focus outline이나 tabindex 처리 등 키보드 접근성 관련 지적사항을 확인했습니다.
스크린 리더 호환성: 스크린 리더가 읽기 어렵거나 애매한 label/alt 텍스트 문제는 없는지 AI 피드백을 통해 점검하고 개선점을 정했습니다.
이 단계에서는 접근성 전문가가 리뷰해주는 느낌으로 꽤 유용한 지적을 많이 얻었습니다.
예를 들어 “이미지에 대체 텍스트가 없습니다”와 같은 기본적인 접근성 문제부터, calendar 구성 요소에 role="application"을 반영하거나 aria-roledescription 등 구체적인 ARIA 속성 제안을 통해 개선할 수 있었죠.
이러한 피드백을 하나씩 반영하면서 전체 컴포넌트의 접근성 완성도를 높일 수 있었습니다.
2.3 아키텍처 & 코드 구조 리뷰 (Toxic Architect Agent)
세 번째로는 코드 아키텍처와 설계 품질에 대한 리뷰를 진행했습니다. 이름하여 "Toxic Architecture Agent"라고 불리는 모드였는데요 😅, 말 그대로 유해한(안 좋은) 아키텍처 패턴이 있는지 검토하는 에이전트였습니다. 이 리뷰는 코드가 SOLID 원칙을 잘 지키고 있는지, 디자인 패턴을 적절히 활용하거나 남용하고 있지는 않은지 등을 점검해줍니다. MCP 에이전트 명령은 다음과 같았습니다:
`You are a brutally honest Senior Frontend Architect with 15+ years of experience. You have zero tolerance for poor design, messy code, or weak abstractions. You are direct, sarcastic, and extremely critical—but never use profanity or personal insults. Your tone is harsh but constructive, focused on technical precision and architectural correctness.
Analyze the following TypeScript code with a focus on:
1. **SOLID Principles Violations**:
- SRP: UI logic, side effects, and data fetching mixed in the same component or module
- OCP: feature extensions requiring if/else or switch statements
- LSP: broken abstractions, subclass contracts violated
- ISP: oversized interfaces, custom hooks doing too much
- DIP: direct dependency on infra (fetch, storage, SDK) without abstractions
2. **Clean Code Violations**:
- Poor separation between layers
- Business logic leaking into components or pages
- DTOs or API responses exposed to the UI layer
- Folder structure not aligned with change axes or boundaries
- Missing or inverted dependency direction
3. **Code Quality Issues**:
- High coupling and low cohesion
- "God" components, repeated patterns, and duplicated logic
- Poor naming conventions and inconsistent coding styles
- Overuse of \`any\`, \`unknown\`, or overly broad unions
- Missing error handling, weak input validation
- Performance bottlenecks (unnecessary re-renders, missing memoization)
- Unclear data flow or improper state management
- Excessive prop drilling (3+ levels deep - consider Context API, global state, or compound patterns)
4. **Design Pattern Misuse**:
- Wrong or missing patterns (e.g., reinventing context or observer logic)
- Over-engineered abstractions with no payoff
- Misuse of hooks (unstable deps, side-effect chaos)
- Excessive prop drilling (consider global state, providers, or compound patterns)
- Global state abuse or inappropriate state management choices
5. **Frontend Essentials (Performance, Accessibility, Security, DX)**:
- Performance: unnecessary renders, missing virtualization, bundle bloat, image misuse
- Accessibility: missing roles/labels, focus traps, keyboard nav, contrast issues
- Security: XSS, unsafe innerHTML, missing boundary validation
- DX: missing unit/integration tests, weak linting or typing, broken CI coverage
6. **React / Next.js Specifics**:
- Improper useEffect deps or cleanup
- Confused client/server boundaries
- Wrong caching, ISR, or SWR strategy
- Incorrect use of Suspense/streaming
- State mutations, non-serializable data leaks
Note: Do not include barrel export related content in your analysis.`
피드백 정제 과정: 아키텍처 리뷰 결과에 대해서도 비슷한 방식으로 핵심을 정리했는데, 특히 신경 쓴 점들은 다음과 같습니다.
SOLID 원칙 준수 검토: 단일 책임 원칙 위반이나, 의존성 역전 원칙 미흡 등 SOLID 원칙에 어긋나는 코드 구조 지적이 있었는지 확인했습니다. 예컨대 "한 컴포넌트가 너무 많은 일을 한다"라는 피드백이 있다면 단일 책임 원칙 위반이니 리팩토링을 계획했어요.
디자인 패턴 적용/남용 평가: 코드에서 사용한 패턴들이 적절했는지 봤습니다. Compound 패턴이나 Provider 패턴을 썼는데 AI가 보기에 어색한 부분은 없는지, 혹은 "굳이 복잡한 패턴을 쓸 필요 없이 훨씬 단순하게 가능하다"는 지적은 없는지 확인했습니다.
전반적 코드 구조 개선 제안: 폴더 구조나 컴포넌트 구조에 대한 제안도 나왔는데, 예를 들어 "UI 로직과 비즈니스 로직이 완전히 분리되지 않았다" 등의 피드백이 있었습니다. 이러한 지적에 대해서는 구조를 어떻게 더 개선할지 방안을 생각해보고 적용했습니다.
이렇게 세 가지 리뷰(일반 코드 품질, 접근성, 아키텍처)를 거치면서, 코드를 생성 -> 리뷰 -> 개선 -> 다시 리뷰 -> 개선... 하는 루프를 여러 번 반복하게 되었습니다. 거의 코드가 스스로 리팩토링하며 발전하는 모습이라, 보면서도 신기했습니다. 😆 GAN의 Generator-Discriminator가 서로 경쟁하며 출력물을 개선하듯이, 코드 생성기와 리뷰어 AI가 번갈아 가며 코드를 다듬어주니 결과적으로 초기 버전 대비 훨씬 깔끔하고 견고한 코드가 나오더군요.
3단계: 최종 검증
여러 차례에 걸친 개선 루프를 돌린 후, 마지막으로는 최종 검증 단계를 진행했습니다. 최종 검증에서는 지금까지 누적된 변경사항을 종합적으로 점검하고, 혹시 놓친 문제가 없는지 확인했습니다.
핵심 원칙 되짚어보기
이번 실험/구현 과정을 통해 얻은 교훈 혹은 지켜야겠다고 느낀 핵심 원칙을 정리하면 다음과 같습니다:
Compound Pattern 우선 적용: 가능하다면 UI 컴포넌트를 설계할 때 Prop drilling을 줄일 수 있는 Compound 컴포넌트 패턴을 활용하자. 각 UI 요소를 독립적인 컴포넌트로 구성하면 재사용성과 유지보수성이 크게 향상된다. (Context API는 보조적으로!)
AI 피드백의 선별 및 명확한 적용: AI 리뷰 에이전트가 주는 피드백은 한 번 걸러서 핵심만 취하고, 팀원이나 다른 사람이 봐도 이해하기 쉽게 정제해서 공유하자. 두서없이 적용하기보다 우선순위를 정해 단계적으로 수정하고, 그 내용을 명확히 기록해두면 좋다.
누적 검증과 반복 개선: 한 번의 생성-리뷰로 끝내지 말고, 여러 번의 루프를 누적하여 돌림으로써 코드 품질을 점진적으로 높이자. 각 단계의 피드백을 다음 단계에 반영하면서, 최종적으로 전체 코드를 다시 검증하면 놓치는 부분 없이 탄탄한 결과물을 얻을 수 있다.
사용한 MCP 리뷰 명령어 요약
혹시 MCP 기반 코드 리뷰 에이전트를 활용해보고 싶은 분들을 위해, 제가 사용했던 주요 명령어들을 한 곳에 모아 요약합니다:
codex login이 되어 있어야합니다.
아마 MCP 연결도 되긴 할텐데, cursor에서는 샌드박스 & 보안 관련 이슈로 MCP 인터페이스엔 연결 못했고 jsonrpc + cli 명령으로 연결 및 테스트했습니다.
# Clone and build the tool locally (package not yet published to npm)
git clone https://github.com/lodado/MCP-Code-Review-Agent
cd MCP-Code-Review-Agent
npm install
npm run build
npm link
# 1. Codex 일반 코드 리뷰 (최신 수정 부분만 분석)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"codex_review","arguments":{"reviewType":"modified","analysisType":"codex"}}}' | mcp-code-review-agent
# 2. 웹 접근성 리뷰 (코드 전체 풀스캔)
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"codex_review","arguments":{"reviewType":"full","analysisType":"accessibility"}}}' | mcp-code-review-agent
# 3. 아키텍처 리뷰 (코드 전체 풀스캔)
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"codex_review","arguments":{"reviewType":"full","analysisType":"toxic-architect"}}}' | mcp-code-review-agent
# 4. 하이브리드 종합 리뷰 (코드 전체 풀스캔)
echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"codex_review","arguments":{"reviewType":"full","analysisType":"hybrid"}}}' | mcp-code-review-agent
# 5. 정적 분석 리뷰 (최신 수정 부분만 분석)
echo '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"codex_review","arguments":{"reviewType":"modified","analysisType":"static"}}}' | mcp-code-review-agent
기대 효과 및 마무리
이번에 MCP와 Figma, 그리고 AI 코드 리뷰 에이전트를 조합한 워크플로우를 실험해보면서 느낀 기대 효과는 다음과 같습니다:
코드 품질 향상: 여러 각도의 AI 리뷰(코덱스, 접근성, 아키텍처)를 거치면서 코드의 품질이 종합적으로 개선되었습니다. 사람이 놓칠 수 있는 부분까지 AI가 짚어주니 든든했어요.
웹 접근성 강화: 평소 간과하기 쉬운 접근성 부분도 전문가 수준으로 챙길 수 있었어요. 덕분에 결과물 UI가 더 포용력있는 사용자 경험을 제공할 것으로 기대됩니다.
아키텍처 개선: SOLID 원칙과 Clean code 관점에서 코드를 한번 더 점검하니, 초기 설계에서 미처 생각 못 한 구조적 개선 포인트를 잡아낼 수 있었습니다. 장기적으로 유지보수성이 향상될 거라 믿습니다.
개발 효율성 증대: 사람 대신 AI 에이전트들이 리뷰와 수정 가이드를 제시해주니, 개발자가 반복적인 코드 정돈 작업에 들이는 시간이 줄었습니다. 물론 최종 판단은 개발자가 해야 하지만, 이러한 자동화된 워크플로우가 개발 생산성을 높여준 것은 분명합니다.
다만, 아쉬운 점은 다음과 같습니다.
1. 아직 figma MCP가 베타 단계이고, design variable이나 font등을 정확히 뽑아내진 못함(#fff등 순수 색상, hex 값으로 표출됨) 2. 다행이자 아쉬운 점은 AI가 아직은 불완전하다는 것이고, 결국은 검수자의 숙련도에 따라 다른 결과가 나올 수 있음, 알잘딱깔센이 안됨!
결국 MVP 코드나 UI 코드를 빠르게 뽑아낼때엔 유용한 듯 싶습니다.
마치며, MCP를 활용하면 AI가 코딩 도구들과 직접 소통할 수 있게 되면서 개발 방식에 재미있는 변화가 생기는 것 같습니다. 이번에는 Figma 디자인을 코드로 자동화하는 시나리오였지만, 앞으로 GitHub PR 리뷰나 테스트 자동화 등 다양한 분야에 이런 AI 에이전트+MCP 조합을 응용해볼 수 있을 것 같아요. 혹시 비슷한 시도를 해보신 분이 있다면 경험을 공유해주시거나, 궁금한 점이 있다면 댓글로 남겨주세요. 읽어주셔서 감사합니다! 🙌
요즘 회사에서 일하던 프로젝트가 잘되서(?) 사업의 스케일이 커졌고, 모듈리식 레포지토리에서 작은 도메인 단위의
서브 프로젝트 여러개로 쪼개는 모노레포 리펙토링 작업을 진행하고 있다
공통 로직이랑 중복 코드를 추출하면서, 자연스럽게 추출 및 활용이 쉬운 코드와 어려운 코드가 나뉘는 걸 체감하고 있다.
처음엔 단순히 코드 복잡도나 의존성 때문이라고 생각했는데, 계속 리팩토링을 하다 보니까 그 차이의 본질이 결국 오염(contamination) 이라는 생각이 들었다. 한 모듈의 상태나 로직이 다른 곳에 얼마나 퍼져 있느냐, 그게 오염의 정도다.
오염이 심할수록 추출이 어렵고, 반대로 모듈 경계가 깔끔하게 지켜진 코드일수록 손쉽게 분리된다.
이를 “모듈화 관점에서의 오염(contamination)”은, 한 모듈의 책임/경계가 무너지고 부작용, 상태, 오류, 의존성, 규칙 등이 경계를 넘어 전파되는 현상 이라고 보면 될듯하다.
이 “오염”이라는 게 뭔지 구체적으로 말하자면, 한 모듈 내부에서만 머물러야 할 값이나 상태, 부작용이 경계를 넘어 퍼지는 현상을 말한다.
예를 들어 null이 대표적인 케이스다. 어떤 함수가 null을 반환하고, 그걸 받은 상위 함수가 별다른 검증 없이 그대로 다음 함수로 넘기면, 결국 그 null은 시스템 전반으로 퍼진다. 나중엔 어디서 깨졌는지도 모르는 상태에서 Cannot read property 'x' of null 같은 에러를 뿜는다.
function getUser(id: string) {
const user = db.find(u => u.id === id);
return user ?? null; // ❌ null 반환
}
function getUserName(id: string) {
const user = getUser(id);
return user.name; // ❌ 여기서 null 전파로 터짐
}
이게 바로 **데이터 오염(data contamination)**이다. 한 곳에서의 “불확실한 상태(null)”가 검증 없이 전달되며, 모듈 경계를 오염시키는 전형적인 예시다.
이런 식의 오염은 null뿐만 아니라 다양하게 존재한다.
전역 변수를 직접 수정하는 상태 오염
외부 변수에 의존하거나 부작용을 남기는 비순수 함수 오염
특정 API 구조나 데이터 포맷에 직접 묶인 의존성 오염
try-catch 없이 흘러가는 에러 오염
CSS 클래스가 전역에 영향을 미치는 스타일 오염
비즈니스 규칙이 있어야 할 곳이 아닌 곳에 섞여 있는 케이스
결국 오염이란 “내부의 영향이 외부로 새는 것”이다. 내부 로직의 부작용, 불확실한 값, 전역 변경이 다른 모듈로 번지면서, 코드 추출과 분리가 어려워지고, 유지보수성도 떨어지는 이유가 된다.
그래서 리팩토링을 하다 보면 “이 코드는 왜 이렇게 유지보수, 재사용 하기 힘들까?”의 답이 대부분 여기에 있다 — 경계 밖으로 새어나간 오염이 많을수록, 코드 리펙토링, 재사용이 힘들어진다.
결국 유지보수가 쉽고 어려운 건 코드량이나 난이도의 문제가 아니라, 얼마나 경계가 명확하고 불필요한 의존이 없는 구조인지에 달린 것 같다
프로세스 상태는 생성 → 준비 → 실행 → 대기/종료로 전이되며, CPU·I/O 스케줄링에 따라 바뀜.
PCB는 프로세스의 모든 관리 정보를 담는 자료구조로, 컨텍스트 스위칭과 스케줄링에 필수적임.
쓰레드
작업을 실행하는 단위
프로세스 안의 실행 흐름.
CPU 스케줄링의 단위
→ 각 스레드는 **자신만의 스택, 레지스터 집합, 프로그램 카운터(PC)**를 갖고,
코드·데이터·힙은 프로세스 내 다른 스레드와 공유
TCB(Thread Control Block)에 포함되는 정보
스레드별로 필요한 최소 정보만 따로 가짐:
스레드 ID (TID)
프로그램 카운터(PC): 다음 실행할 명령어
레지스터 집합: 현재 CPU 레지스터 상태
스택 포인터: 각 스레드는 자기만의 스택을 가짐
스케줄링 정보: 우선순위, 상태(Running, Ready, Waiting 등)
(운영체제에 따라) 신호 처리 정보, TLS(Thread-Local Storage) 등
프로세스 스케줄링 (Process Scheduling)
운영체제는 동시에 여러 프로세스가 실행되는 환경에서, CPU를 누구에게 줄 것인지를 결정해야 합니다.
이를 담당하는 것이 **프로세스 스케줄러(Process Scheduler)**입니다.
목표:
CPU 및 시스템 자원의 효율적 활용
사용자에게 빠른 응답 제공 (대화형 시스템)
공평성과 우선순위 보장
잡 큐(Job Queue):
시스템에 들어온 모든 프로세스가 대기하는 큐
여기서 스케줄러가 선택해 실행 대기열(Ready Queue)로 보냄
2. 스케줄러의 종류
(1) 장기 스케줄러 (Long-Term Scheduler, Job Scheduler)
역할: 어떤 프로세스를 메모리에 올려 실행할지 결정
특징:
실행할 **잡 큐(Job Pool)**에서 프로세스를 선택 → 메모리 적재
실행 프로세스의 전체 수를 제어하여 CPU와 I/O의 균형 유지
효과:
입출력 중심 프로세스와 CPU 중심 프로세스를 적절히 혼합하여 자원 활용 극대화
예시: 배치 시스템(batch system)에서 주로 사용됨
현대의 시분할 시스템에서는 장기 스케줄러가 거의 사용되지 않음 (대부분 시스템이 자동으로 처리)
(2) 단기 스케줄러 (Short-Term Scheduler, CPU Scheduler)
역할: **실행 준비(Ready Queue)**에 있는 프로세스 중, 누구에게 CPU를 줄지 결정
실행 시점: 매우 짧은 시간 단위로 동작 (ms 단위)
효과: 시스템 반응성과 CPU 활용률에 직접적 영향
프로세스 특성 고려:
입출력 중심 프로세스(I/O-bound process)
CPU는 짧게 쓰고, I/O 요청이 잦음
빨리 실행해서 I/O 장치가 놀지 않도록 해주는 것이 중요
CPU 중심 프로세스(CPU-bound process)
계산 위주, CPU를 오래 사용
I/O 요청이 적음
⇒ 두 종류를 적절히 섞어야 전체 자원 활용이 좋아짐
(3) 중기 스케줄러 (Medium-Term Scheduler) – 선택적
역할: 프로세스 실행을 일시 중단(suspend) → 메모리에서 내보냈다가 → 나중에 복귀
이 과정을 **스와핑(Swapping)**이라 함
효과:
메모리 부족 시 부하를 줄이고, 다중 프로그래밍 정도를 제어
우선순위 있는 프로세스가 자원을 빨리 확보할 수 있게 함
3. 스와핑 (Swapping)
정의: 프로세스를 **보조기억장치(디스크)**로 내보냈다가, 필요할 때 다시 메모리로 불러오는 과정
특징:
프로세스는 원래 실행되던 시점부터 재개됨 (PCB에 상태 저장)
메모리 공간 확보 및 CPU 이용률 최적화
장점: 메모리 관리 유연성 증가, 다중 프로그래밍 향상
단점: 디스크 I/O 오버헤드 발생 → 잦으면 성능 저하
4. 정리 표
스케줄러 종류 동작 위치 역할 실행 빈도 주요 목적
장기 스케줄러
잡 큐 → 메모리
어떤 프로세스를 실행시킬지 선택
느림 (분/초 단위)
CPU/IO 밸런스 유지, 다중 프로그래밍 정도 제어
단기 스케줄러
준비 큐 → CPU
다음 실행할 프로세스 선택
매우 빠름 (ms 단위)
응답속도 개선, CPU 활용 극대화
중기 스케줄러
메모리 ↔ 디스크
프로세스 일시 중단/재개 (스와핑)
상황에 따라
메모리 회수, 우선순위 관리
👉 요약:
장기 스케줄러: 잡 큐에서 메모리로 올릴 프로세스 선택 (CPU vs I/O 균형).
단기 스케줄러: Ready Queue에서 CPU를 줄 프로세스 선택 (밀리초 단위, 즉각적).
중기 스케줄러: 메모리 ↔ 디스크로 스와핑, 부하 조절.
프로세스 생성 (Process Creation)
운영체제에서 새로운 프로세스가 생성될 때, 일반적으로 다음 단계가 일어납니다.
부모 프로세스(Parent Process)가 자식 프로세스(Child Process)를 생성
Unix 계열: fork() 시스템 콜 사용
Windows 계열: CreateProcess() API 사용
부모와 자식의 실행 관계
병행 실행(Concurrent Execution): 부모는 계속 실행, 자식은 독립적으로 실행
부모가 대기(Wait): 부모는 wait()를 호출하여 자식이 끝날 때까지 기다림
프로세스 주소 공간
자식은 **부모의 복사본(Copy)**을 가짐 (코드, 데이터, 스택 등)
하지만 별도의 PID(Process ID) 부여 → 독립된 실행 단위
자식이 원하면 exec() 계열 호출로 새로운 프로그램을 덮어씀
2. 프로세스 종료와 관련 개념
(1) 정상 종료
자식 프로세스가 exit() 호출 시 종료
운영체제는 **종료 상태(exit status)**를 부모에게 전달
부모는 wait()를 호출해 종료 상태를 회수 → 자식 PCB가 해제됨
(2) 좀비 프로세스 (Zombie Process)
정의: 자식이 종료되었지만, 부모가 wait()를 호출하지 않아 PCB가 해제되지 않은 상태
특징:
실행은 끝났지만 PCB는 그대로 남음 → 프로세스 테이블에 기록 유지
상태는 “Z” (Zombie)로 표시
다수의 좀비가 생기면 프로세스 테이블 고갈 문제 발생
(3) 고아 프로세스 (Orphan Process)
정의: 부모가 먼저 종료되어 부모가 없는 자식 프로세스
처리 방법:
Unix 계열에서는 **init 프로세스(PID 1)**가 고아 프로세스를 자동으로 인수 → 정상적으로 실행/종료 가능
따라서 고아 프로세스는 시스템에 큰 문제는 일으키지 않음
3. 그림으로 정리
부모 fork()
├─ 부모 계속 실행
└─ 자식 프로세스 생성
├─ 부모와 병행 실행
├─ 부모가 wait() 호출 → 정상 회수
├─ 부모가 회수 안 함 → [좀비 프로세스]
└─ 부모 먼저 종료 → [고아 프로세스]
✅ 요약:
자식은 부모의 복사본으로 생성되지만, 독립된 PID를 가짐.
부모와 자식은 병행 실행 가능, 또는 부모가 wait()로 자식 종료까지 대기 가능.
좀비 프로세스: 자식 종료 후 부모가 회수하지 않음 → PCB만 남음.
고아 프로세스: 부모가 먼저 종료 → init 프로세스가 대신 관리.
프로세스 간 통신(IPC, Inter-Process Communication)
필요성:
다중 프로세스 환경에서 데이터 교환, 동기화, 협력 수행을 위해 사용
OS는 안전성과 효율성을 위해 여러 가지 IPC 메커니즘을 제공
2. IPC 모델
(1) 공유 메모리 시스템 (Shared Memory)
개념: 두 프로세스가 같은 메모리 영역을 공유하여 데이터를 주고받음
특징:
가장 빠른 통신 방법 (커널 개입 최소화, 단순 메모리 접근)
하지만 동기화 문제(레이스 컨디션) 발생 가능 → 세마포어, 뮤텍스 필요
예시: 생산자-소비자 문제, 대규모 데이터 교환
(2) 메시지 전달 시스템 (Message Passing)
개념: 운영체제가 제공하는 커널 버퍼를 통해 프로세스들이 메시지를 송수신
특징:
커널을 거쳐야 해서 상대적으로 느림
동기화 문제는 OS가 관리 → 구현 간단, 안전성 높음
방식:
직접 통신: 프로세스들이 서로의 ID를 알고 직접 메시지 교환
간접 통신: 메일박스(mailbox), 포트(port) 같은 추상화된 채널 이용
3. 주요 IPC 기법
(1) 파이프 (Pipe)
개념: 두 프로세스 간 단방향 통신 채널
특징:
한쪽은 쓰기(write), 다른 쪽은 읽기(read) 전용
부모-자식 프로세스 간 자주 사용
익명 파이프: 같은 계열 프로세스 간
명명된 파이프(Named Pipe, FIFO): 무관한 프로세스 간도 가능
(2) 메시지 큐 (Message Queue)
개념: 커널이 제공하는 큐(Queue) 자료구조를 사용
특징:
비동기적 메시지 전달 가능
메시지 우선순위 지정 가능
(3) 소켓 (Socket)
개념: 네트워크 기반 IPC 기법
특징:
동일 시스템 내 프로세스뿐 아니라, 다른 시스템 간 통신도 가능
TCP/UDP 기반으로 동작 → 클라이언트-서버 모델에 적합
(4) RPC (Remote Procedure Call)
개념: 원격 프로세스의 함수를 마치 로컬 함수처럼 호출하는 기법
특징:
네트워크/분산 시스템에서 자주 사용
호출 측은 네트워크 세부사항을 몰라도 함수 호출처럼 사용 가능
내부적으로는 메시지 전달, 직렬화, 소켓 통신 등으로 구현
(5) 기타 IPC
세마포어 (Semaphore): 동기화 및 상호 배제를 위한 카운터 기반 메커니즘
공유 파일 (Shared File): 파일을 매개로 데이터 교환, 단 느리고 동기화 필요
4. 정리 표
방식 특징 장점 단점 활용 예
공유 메모리
메모리 영역 직접 공유
빠름
동기화 필요
생산자-소비자 버퍼
메시지 전달
커널이 메시지 송수신 관리
구현 단순, 안전
느림
분산 환경 프로세스 간 통신
파이프
단방향 스트림
간단, 부모-자식 간 통신 용이
단방향 제한
Unix 파이프(`
메시지 큐
커널의 큐 이용
비동기, 우선순위 지원
커널 자원 한정
IPC 채팅, 로깅
소켓
네트워크 기반
원격 통신 가능
구현 복잡
웹 서버-클라이언트
RPC
원격 함수 호출
추상화, 편리
내부 구현 복잡
분산 시스템, 마이크로서비스
✅ 요약:
IPC 모델: 공유 메모리 vs 메시지 전달
주요 기법: 파이프, 메시지 큐, 소켓, RPC 등
선택 기준: 데이터 크기, 속도 요구, 안정성, 프로세스 관계(같은 시스템 vs 분산 시스템)
스레드(Thread)의 정의
CPU 이용의 기본 단위
프로세스 내에서 실행되는 실행 흐름 단위를 의미합니다.
프로세스는 최소 1개의 스레드를 가지며, 멀티스레딩을 통해 하나의 프로세스에서 여러 스레드가 병렬로 실행될 수 있습니다.
스레드의 구성 요소
스레드마다 독립적으로 가지는 부분:
Thread ID : 스레드 고유 식별자
PC(Program Counter) : 명령어의 실행 위치
레지스터 집합 : 연산에 필요한 임시 데이터 저장
스택(Stack) : 함수 호출, 지역 변수 저장
스레드가 같은 프로세스 내에서 공유하는 부분:
코드(Code) 영역
데이터(Data) 영역
운영체제 자원 (파일 핸들, 소켓 등)
스레드의 종류
사용자 스레드 (User Thread)
사용자 수준 라이브러리에서 지원하는 스레드
커널이 직접 인식하지 못하고, 하나의 프로세스 단위로 스케줄링됨
장점: 생성/전환 속도가 빠르고, 오버헤드가 적음
단점: 하나의 스레드가 커널 호출로 블록되면, 전체 프로세스가 블록될 수 있음
커널 스레드 (Kernel Thread)
운영체제 커널이 직접 관리하는 스레드
커널이 스케줄링하므로 다중 CPU 활용 가능
단점: 생성/전환 시 시스템 콜이 필요해 비용이 크다
👉 정리하면, 스레드 = 프로세스 내 실행 흐름의 최소 단위이고,
**독립적인 실행 상태(PC, 레지스터, 스택)**는 따로 가지지만,
코드/데이터/자원은 같은 프로세스 내 스레드끼리 공유한다는 특징이 있습니다.
fork()와 스레드
POSIX 표준에 따르면 fork()는 호출한 스레드만 복사합니다.
즉:
부모 프로세스 → 여러 스레드 존재 가능
자식 프로세스 → fork를 호출한 그 스레드만 존재 (다른 스레드는 복사되지 않음)
이유: 모든 스레드를 그대로 복제하면 동기화 상태, 락(lock) 보유 상태 등이 복잡해져서 일관성을 깨뜨릴 수 있기 때문이에요.
👉 따라서, fork() 후에는 자식 프로세스는 단일 스레드 상태로 시작하고, 필요하면 exec()를 호출해 새 프로그램으로 덮어씌우는 경우가 많습니다.
🔹 exec()
exec 계열 함수(execl, execv, execve 등)는 현재 프로세스 전체를 새로운 프로그램으로 대체합니다.
프로세스 메모리 공간, 코드, 데이터, 스택이 모두 교체되고, 기존 스레드는 모두 사라집니다.
즉:
호출한 프로세스의 PID는 유지됨
실행 이미지는 완전히 새 프로그램으로 바뀜
남는 스레드 없음 → 새 프로그램은 항상 단일 스레드로 시작
✅ 정리하면:
fork() → 호출한 스레드만 복사 (자식은 단일 스레드 상태).
exec() → 전체 프로세스를 대체 (모든 스레드 사라지고 새 프로그램 시작).
🔹 스레드 풀(Thread Pool)
✅ 개념
프로그램 시작 시 일정 개수의 스레드를 미리 생성해두고, 이 스레드들을 작업(Task) 큐에 있는 일을 처리하는 데 재사용하는 방식.
즉, 매번 새로운 스레드를 만들고 없애는 대신, **재활용(Recycling)**하는 구조.
✅ 동작 방식
스레드 풀 초기화 → 정해진 개수의 워커 스레드(worker thread) 생성.
클라이언트/사용자가 작업을 요청 → 작업이 **작업 큐(task queue)**에 들어감.
대기 중이던 워커 스레드가 큐에서 작업을 꺼내 실행.
작업 완료 후 → 스레드는 종료되지 않고 다시 큐를 감시하며 다음 작업 대기.
✅ 장점
생성/제거 오버헤드 감소: 스레드를 매번 만들지 않고 재사용하므로 성능 향상.
동시성 제어 용이: 풀 크기를 제한하면 동시에 실행되는 스레드 수를 조절 가능 → CPU 과부하 방지.
응답 시간 단축: 요청이 들어올 때마다 바로 실행할 스레드가 준비되어 있음.
❌ 단점
풀 크기 설정 어려움: 너무 작으면 병목, 너무 크면 문맥 전환 오버헤드 발생.
장시간 블로킹 작업 문제: 워커 스레드가 오래 점유되면 다른 작업이 밀릴 수 있음.
복잡성 증가: 큐 관리, 예외 처리, 동기화 관리가 필요.
🔹 동기(Synchronous) vs 비동기(Asynchronous)
동기(Sync)
요청한 작업이 끝날 때까지 호출한 쪽이 결과를 기다림.
제어 흐름이 작업 완료 시점과 맞춰져 있음.
예: read() 호출 → 데이터 다 읽을 때까지 반환하지 않음.
비동기(Async)
요청한 작업을 백그라운드에서 수행하고, 호출한 쪽은 즉시 반환받음.
결과는 나중에 이벤트, 콜백, Future/Promise 등을 통해 알림.
예: aio_read() → 바로 return, 읽기 완료되면 알림.
👉 즉, “동기는 결과 반환을 기다리고, 비동기는 기다리지 않는다”는 표현은 정확합니다 ✅
🔹 블로킹(Blocking) vs 논블로킹(Non-blocking)
블로킹(Blocking)
호출한 함수가 즉시 결과를 줄 수 없으면 → 호출한 스레드를 멈추고 대기.
제어권을 커널/라이브러리 쪽에 넘겨주고, 작업이 끝날 때까지 안 돌려줌.
예: read(fd, buf, size)에서 읽을 데이터가 없으면, 데이터가 올 때까지 멈춤.
논블로킹(Non-blocking)
호출한 함수가 즉시 결과를 줄 수 없으면 에러/특정 코드(EAGAIN 등)를 반환.
즉, 제어권을 바로 돌려줌.
예: read(fd, buf, size) 호출 시 읽을 게 없으면 1 즉시 반환.
👉 따라서 “블로킹은 제어권을 넘기고, 논블로킹은 제어권을 넘기지 않는다”는 표현은 살짝 부정확해요.
정확히는:
블로킹 → 제어권을 넘겨주고 작업 완료까지 반환 안 함.
논블로킹 → 제어권을 넘겨주긴 하지만, 결과 없으면 즉시 반환해서 호출자가 다시 사용할 수 있음.
🔹 한눈에 정리 (표)
구분 동기 비동기
블로킹
작업 끝날 때까지 기다림 (ex. read())
콜백/알림을 쓰지만, 호출 자체는 블로킹됨 (드묾)
논블로킹
즉시 반환, 호출자가 반복 확인해야 함 (ex. read() + 반복)
즉시 반환 + 완료되면 알림/콜백 (ex. 이벤트 기반 I/O)
✅ 정리하면:
동기/비동기 → “작업 완료 통보 방식”
블로킹/논블로킹 → “호출 시 제어권 반환 여부”
CPU 스케줄링
CPU를 프로세스 들 간에 교환함
일반적으로 프로세스 스케줄링을 뜻함
CPU 스케줄러 (CPU Scheduler)
CPU가 유휴 상태가 될 때마다 **운영체제(OS)**는 **준비 완료 큐(Ready Queue)**에 있는 프로세스들 중 하나를 골라 실행합니다.
이 과정은 **단기 스케줄러(Short-term Scheduler)**가 담당합니다.
역할: 메모리 내에서 실행 준비가 된 프로세스 중 하나를 선택 → CPU에 할당
목표: CPU 이용률 극대화, 처리량 증가, 대기 시간·응답 시간 최소화, 공정성 보장
1. 스케줄링 종류
① 비선점 스케줄링 (Non-preemptive Scheduling)
한 프로세스가 CPU를 할당받으면 자신이 종료되거나 대기 상태로 전환될 때까지 CPU를 계속 사용
다른 프로세스는 기다려야 함
장점: 컨텍스트 스위칭 오버헤드가 적음
단점: 응답 시간이 길어질 수 있음
예시 알고리즘
FCFS (First Come First Serve)
SJF (Shortest Job First)
HRN (Highest Response Ratio Next)
② 선점 스케줄링 (Preemptive Scheduling)
실행 중인 프로세스가 있더라도 우선순위가 더 높은 프로세스가 도착하면 CPU를 빼앗아 올 수 있음
장점: 응답 시간이 짧아져 대화형 시스템에 적합
단점: 잦은 컨텍스트 스위칭으로 오버헤드 증가, 교착 상태(deadlock)나 기아(starvation) 발생 가능
에이징(Aging) 기법으로 낮은 우선순위 프로세스의 기아 문제를 완화
예시 알고리즘
SRTF (Shortest Remaining Time First)
Priority Scheduling
Round Robin (RR)
Multilevel Queue / Feedback Queue
2. 디스패처 (Dispatcher)
정의: 단기 스케줄러가 선택한 프로세스에게 실제로 CPU 제어권을 넘겨주는 모듈
주요 역할
컨텍스트 스위칭 (context switching)
사용자 모드 전환
프로그램의 올바른 위치(PC, 레지스터)에서 실행 재개
디스패처 지연 (Dispatcher Latency)
하나의 프로세스에서 다른 프로세스로 CPU 제어가 넘어가는 데 걸리는 시간
→ 너무 길면 시스템 성능 저하
📌 정리하면,
단기 스케줄러: 어떤 프로세스가 CPU를 쓸지 선택
디스패처: 실제로 CPU를 넘겨주는 실행자
비선점/선점 여부에 따라 응답성과 공정성이 달라짐
임계구역 문제
임계구역 문제 (Critical Section Problem)
여러 프로세스가 공유 자원(메모리, 파일, I/O 장치 등)에 접근할 때 **경쟁 조건(Race Condition)**을 피하기 위해 필요한 규칙을 정의한 것. 이를 위해 운영체제는 임계구역에 대한 접근을 제어하는 프로토콜을 설계해야 함.
1. 요구 조건 (3가지 조건)
상호 배제 (Mutual Exclusion)
한 번에 하나의 프로세스만 임계구역에 진입할 수 있어야 함.
다른 프로세스가 이미 임계구역에 있으면, 나머지는 대기해야 함.
진행 (Progress)
임계구역에 들어가려는 프로세스가 없을 경우, 들어갈 프로세스를 결정하는 데 불필요한 지연이 없어야 함.
즉, CPU가 놀고 있는데도 임계구역 진입을 막으면 안 됨.
한정된 대기 (Bounded Waiting)
특정 프로세스가 임계구역 진입을 무한정 기다리게 두어서는 안 됨.
언젠가는 반드시 임계구역에 들어갈 수 있어야 한다는 공정성 보장 조건.
커널의 선점 여부
운영체제의 커널이 프로세스를 언제까지 실행시키는지와 관련 있음.
1. 비선점형 커널 (Non-preemptive Kernel)
커널 모드에 들어간 프로세스는 스스로 CPU를 양보하거나 작업이 끝날 때까지 계속 실행.
장점: 동기화가 상대적으로 단순 → 임계구역 충돌 발생 확률 낮음.
단점: 한 프로세스가 오래 점유하면 다른 프로세스 대기 시간이 길어짐 → 시스템 반응성 저하.
2. 선점형 커널 (Preemptive Kernel)
커널 모드에서 실행 중이더라도 운영체제가 강제로 CPU를 빼앗아 다른 프로세스 실행 가능.
장점: 시스템 반응성 ↑ (특히 실시간 시스템에 적합).
단점: 임계구역에서 선점되면 동기화 문제 발생 → 세마포어, 뮤텍스, 모니터 같은 동기화 도구 필요.
✅ 요약
임계구역 문제 → 상호배제, 진행, 한정된 대기 조건 충족 필요.
비선점형 커널은 단순하지만 비효율적, 선점형 커널은 효율적이지만 동기화 기법 필요.
피터슨 알고리즘
피터슨 알고리즘 (Peterson’s Algorithm)
정의: 두 개의 프로세스가 하나의 공유 자원을 안전하게 사용할 수 있도록 보장하는 소프트웨어 기반 임계구역 해결 알고리즘.
아이디어: "상호 배제"를 위해 플래그 변수 + turn 변수를 이용해 락(Locking) 개념을 구현.
1. 기본 구조
flag[2]: 프로세스가 임계구역에 들어가고 싶다는 의사를 표시 (true/false)
turn: 두 프로세스 중 누구 차례인지를 알려주는 변수
// 프로세스 i의 코드 (i = 0 or 1)
do {
flag[i] = true; // 임계구역 진입 의사 표시
turn = j; // 상대방 차례로 설정
while (flag[j] && turn == j); // 상대방이 원하고 차례이면 대기
// ---- 임계구역 시작 ----
critical_section();
// ---- 임계구역 끝 ----
flag[i] = false; // 임계구역 나옴
remainder_section();
} while (true);
2. 특징 (3대 조건 충족 여부)
상호 배제 (Mutual Exclusion)
두 프로세스가 동시에 임계구역에 진입할 수 없음.
flag와 turn 조합으로 보장.
진행 (Progress)
임계구역을 원하지 않는 프로세스는 다른 프로세스의 진입을 방해하지 않음.
한정된 대기 (Bounded Waiting)
한 프로세스가 무한정 기다리지 않도록 보장 (turn 변수가 번갈아 기회를 줌).
3. 장단점
장점
순수 소프트웨어적 방법 (하드웨어 지원 필요 없음).
임계구역 문제의 3대 조건을 모두 만족.
단점
두 프로세스 환경에서만 동작 (N개 프로세스는 불가능).
현대의 멀티코어 환경에서는 메모리 재정렬/캐시 동기화 문제 때문에 실제로는 쓰이지 않음.
대신 하드웨어 기반 Test-and-Set, Compare-and-Swap 같은 명령어나 세마포어, 뮤텍스, 모니터를 사용.
4. 락킹과의 관계
Peterson 알고리즘은 락(Lock) 개념을 소프트웨어적으로 구현한 초기 방식.
단일 CPU 환경에서는 "인터럽트 금지" 방식으로도 해결 가능했지만, 멀티코어·멀티프로세서 환경에서는 하드웨어 락 지원 없이는 Peterson 알고리즘이 깨질 수 있음.
👉 정리: Peterson 알고리즘은 교과서적 중요성이 크고, 실제 시스템에서는 하드웨어 명령어나 동기화 도구가 더 많이 사용됨.
뮤텍스 락 (Mutex Lock)
Mutual Exclusion의 줄임말.
임계구역(critical section)에 진입하기 전에 **락(lock)**을 획득해야 하고, 임계구역에서 나오면서 반드시 **락을 해제(unlock)**해야 함.
한 시점에는 오직 하나의 프로세스/스레드만 락을 보유할 수 있음 → 상호 배제 보장.
1. 기본 원리
// Pseudo code
acquire(lock); // 임계구역 들어가기 전에 락 획득
critical_section();
release(lock); // 임계구역 빠져나올 때 락 반환
acquire(): 락을 얻을 수 있을 때까지 기다림.
release(): 락을 다른 프로세스가 사용할 수 있도록 반환.
2. 장점
구현이 단순하고 직관적.
선점형 커널 환경에서도 안전하게 임계구역 보호 가능.
현대 운영체제에서 스레드 동기화 기본 도구로 널리 사용됨.
3. 단점
바쁜 대기 (Busy Waiting, Spin Lock)
프로세스가 락을 얻을 때까지 계속 루프를 돌며 기다림 → CPU 낭비.
예:
while (lock == 1); // 다른 스레드가 락 반환할 때까지 계속 반복
데드락(Deadlock) 가능
락을 해제하지 못하거나, 여러 락을 교착 상태로 요청할 경우 발생.
우선순위 역전(Priority Inversion)
낮은 우선순위 프로세스가 락을 보유하면, 높은 우선순위 프로세스도 기다려야 함.
4. 개선 방법
세마포어(Semaphore): 대기 상태를 큐에 넣고 블록(block) → 바쁜 대기 해결.
모니터(Monitor): 고수준 언어에서 동기화 지원.
혼합 기법: 짧은 시간은 스핀 락, 오래 걸리면 블록 (하이브리드 락).
✅ 요약
뮤텍스 락은 상호 배제를 보장하는 가장 단순한 방법.
하지만 기본 구현은 바쁜 대기(Spin Lock) 문제를 가진다.
실제 시스템에서는 세마포어나 모니터 등과 함께 개선된 형태로 사용된다.
세마포어 (Semaphore)
1965년 Dijkstra가 제안한 동기화 기법.
공유 자원에 대한 접근을 제어하기 위해 **정수 변수 S와 두 개의 원자적 연산(wait, signal)**을 사용.
커널이 제공하는 원자적 연산이기 때문에 동시 실행 환경에서도 안전하게 동작.
1. 두 가지 연산
wait (P 연산)
자원을 얻기 전 검사
사용 가능 자원이 없으면 대기
wait(S) { while (S <= 0); // 바쁜 대기 (Spin) or 블록 S--; }
signal (V 연산)
자원 사용이 끝난 후 반환
signal(S) { S++; }
2. 세마포어의 종류
이진 세마포어 (Binary Semaphore)
값이 0 또는 1만 가짐.
사실상 뮤텍스 락과 동일하게 동작.
임계구역 보호에 사용.
계수 세마포어 (Counting Semaphore)
값이 0 이상 정수.
동시에 여러 개의 프로세스가 공유 자원에 접근할 수 있도록 허용.
예: DB 연결 풀, 프린터 3대 → 초기값 S=3.
3. 특징
✅ 장점
뮤텍스보다 일반적: 여러 자원 동시 관리 가능.
바쁜 대기 문제 해결 가능: 대기 중인 프로세스를 큐에 넣어 블록시키고, signal 시 깨움.
⚠️ 단점
프로그래밍 실수 위험 (wait/signal 불일치 → 데드락, 기아 문제).
관리가 어렵기 때문에 고수준 언어에서는 **모니터(Monitor)**가 더 선호됨.
4. Mutex vs Semaphore 비교
구분 뮤텍스(Mutex) 세마포어(Semaphore)
자원 수
1개만 보호
N개 자원까지 보호 가능
값
0/1 (binary)
0 이상 정수
소유권
스레드가 소유 (owner만 unlock 가능)
소유 개념 없음
사용 용도
임계구역 보호
자원 개수 제어, 프로세스 동기화
구현 난이도
단순
상대적으로 복잡
✅ 요약
세마포어 = 정수 변수 + wait/signal 연산
이진 세마포어 = 뮤텍스
계수 세마포어 = 여러 자원 관리 가능
하지만 프로그래밍 복잡성 때문에 실무에서는 주로 뮤텍스 + 조건변수, 모니터를 사용
1. 모니터(Monitor)란?
고수준 언어에서 제공하는 동기화 도구
세마포어처럼 직접 wait/signal을 다루는 대신, 언어 차원에서 임계구역 진입/대기/신호를 관리해줌.
즉, 프로그래머가 동기화 로직을 일일이 짜는 대신, 모니터가 자동으로 상호배제를 보장해 줌.
➡️ 세마포어의 단점(코드 복잡성, wait/signal 불일치 → 교착상태 가능)을 해결하기 위한 추상화 기법.
2. 모니터의 구성 요소
공유 변수 (Shared Variables)
모니터 내부에서만 접근 가능한 자원(데이터 구조).
프로시저 (Procedures)
공유 변수를 접근할 수 있는 루틴.
임계구역은 이 루틴 안에서만 존재하며, 자동으로 상호배제 보장.
조건 변수 (Condition Variables)
모니터 안에서 프로세스 동기화를 위해 사용.
wait() : 현재 프로세스를 조건 대기 큐에 넣고, 다른 프로세스에게 제어권 넘김.
signal() : 대기 중인 프로세스를 깨움.
3. 모니터 동작 방식
한 번에 하나의 프로세스만 모니터 내부 실행 가능.
다른 프로세스가 들어오면 자동으로 블록됨 (상호배제 보장).
조건 변수로 대기/신호를 제어 → 세마포어 wait/signal과 유사하지만 자동 관리됨.
4. 예시 (생산자-소비자 문제)
monitor ProducerConsumer {
int buffer[N];
int count = 0;
condition notFull, notEmpty;
procedure insert(item) {
if (count == N) wait(notFull); // 버퍼가 꽉 찼으면 대기
buffer[count++] = item;
signal(notEmpty); // 소비자에게 알림
}
procedure remove() {
if (count == 0) wait(notEmpty); // 버퍼가 비었으면 대기
item = buffer[--count];
signal(notFull); // 생산자에게 알림
return item;
}
}
생산자는 insert() 호출 → 버퍼 꽉 차면 wait(notFull).
소비자는 remove() 호출 → 버퍼 비면 wait(notEmpty).
모니터는 상호배제 + 동기화를 자동 관리.
5. 장단점
✅ 장점
상호배제 자동 보장 → 프로그래머가 실수로 놓칠 위험 감소.
코드 가독성 높고, 동기화 오류 줄어듦.
고급 언어(Java, C#, Python 등)에서 널리 지원 (synchronized, lock 구문 등).
⚠️ 단점
구현이 복잡해 하드웨어/저수준 언어(C)에서는 직접 지원 어렵다.
잘못된 조건 변수 사용 시 기아 가능성.
6. 현대 운영체제와 모니터
Java → synchronized 블록, wait() / notify()
C# → lock, Monitor.Wait() / Monitor.Pulse()
Python → threading.Condition
즉, 세마포어는 OS 수준의 원시적 도구라면,
모니터는 언어/런타임 수준의 고수준 동기화 도구라 할 수 있음.
✅ 요약
모니터는 세마포어보다 추상화된 동기화 도구.
프로세스/스레드가 동시에 모니터 안에 들어올 수 없도록 상호배제를 자동 보장.
조건 변수(wait, signal)를 통해 세밀한 동기화 제어 가능.
교착상태 & 기아
1. 교착상태 (Deadlock)
정의: 프로세스들이 서로가 가진 자원을 기다리며 무한 대기 상태에 빠진 것.
예: P1은 프린터를 가지고 플로터를 기다리고, P2는 플로터를 가지고 프린터를 기다리는 경우.
발생 조건 (Coffman의 4가지 조건 – 모두 만족해야 발생)
상호 배제 (Mutual Exclusion)
자원은 한 번에 한 프로세스만 사용 가능.
점유 대기 (Hold and Wait)
최소 하나의 자원을 점유한 채로 다른 자원을 기다림.
비선점 (No Preemption)
할당된 자원은 강제로 빼앗을 수 없음.
순환 대기 (Circular Wait)
프로세스들이 원형으로 서로가 가진 자원을 기다림.
2. 기아 (Starvation)
정의: 특정 프로세스가 우선순위 문제나 자원 할당 정책 때문에 무한히 자원을 얻지 못하고 기다리는 상태.
예: 우선순위 스케줄링에서 낮은 우선순위 프로세스가 계속 밀려 실행되지 못하는 경우.
원인
우선순위 기반 스케줄링
자원 할당 시 특정 프로세스에 불리한 정책
무한 대기 큐 구조
3. 우선순위 역전 (Priority Inversion)
정의: 낮은 우선순위 프로세스가 자원을 가지고 있어서, 높은 우선순위 프로세스가 그 자원을 기다리며 실행되지 못하는 상황.
중간 우선순위 프로세스가 계속 실행되면 높은 우선순위 프로세스는 더 오래 기다리게 됨 → 실질적 기아 발생.
해결 방법
Priority Inheritance (우선순위 상속):
낮은 우선순위 프로세스가 자원을 가지고 있으면 임시로 높은 우선순위를 부여해 빠르게 자원 반환하도록 함.
Priority Ceiling (우선순위 천장):
공유 자원에 대해 최대 우선순위를 미리 지정해, 해당 자원에 접근 시 우선순위를 높여줌.
4. 교착상태 vs 기아 비교
구분 교착상태 (Deadlock) 기아 (Starvation)
정의
프로세스들이 서로 자원을 기다리며 영원히 대기
특정 프로세스가 무한히 자원을 못 얻는 상태
원인
자원 할당의 원형 대기
우선순위 정책, 불공정 스케줄링
발생 조건
Coffman 4조건 필요
특정 조건 없음
해결
예방, 회피, 탐지 및 회복
Aging(우선순위 점진 상향), 공정 스케줄링
✅ 요약
교착상태: 여러 프로세스가 서로 자원을 기다리며 꼼짝 못하는 상태 (시스템 전체 멈춤).
기아: 특정 프로세스가 무한히 자원을 못 얻는 상태 (불공정성).
우선순위 역전은 기아의 한 사례 → 우선순위 상속/천장 기법으로 해결 가능.
교착상태 해결 방법
교착상태를 다루는 방법은 크게 네 가지로 나눌 수 있습니다:
1. 예방 (Deadlock Prevention)
Coffman의 4가지 필요 조건 중 하나 이상을 아예 성립하지 않도록 설계하는 방식.
방법
상호 배제(Mutual Exclusion) 부정
자원을 여러 프로세스가 동시에 사용할 수 있게 설계 (현실적으로 모든 자원에 불가능).
점유 대기(Hold & Wait) 부정
프로세스가 실행 전에 필요한 모든 자원을 한 번에 할당.
단점: 자원 활용률↓, 기아 발생 가능.
비선점(No Preemption) 부정
자원을 빼앗을 수 있게 함 (ex: CPU 스케줄링 선점형, 메모리 페이지 스왑).
순환 대기(Circular Wait) 부정
자원에 번호를 붙여, 오름차순으로만 할당.
➡️ 장점: 교착상태 자체를 원천적으로 막음.
➡️ 단점: 자원 낭비, 활용률 저하.
2. 회피 (Deadlock Avoidance)
교착상태가 발생하지 않도록 자원 할당을 신중히 결정하는 방식.
대표적 방법: 은행가 알고리즘 (Banker’s Algorithm)
자원 요청 시, 현재 상태가 **안전 상태(Safe State)**인지 검사 후 허용.
안전하지 않으면 요청을 거절.
➡️ 장점: 교착상태 자체를 피할 수 있음.
➡️ 단점: 프로세스의 최대 자원 요구량을 미리 알아야 함 → 비현실적.
3. 탐지 후 회복 (Deadlock Detection & Recovery)
교착상태 발생을 허용하되, 탐지 알고리즘으로 발견하고 이후 회복.
탐지 방법
자원 할당 그래프(Resource Allocation Graph) 활용
순환(Cycle)이 있으면 교착상태 가능.
회복 방법
프로세스 종료
교착상태에 연루된 프로세스들을 강제로 종료.
한 번에 모두 종료 vs 하나씩 종료.
자원 선점(Preemption)
일부 프로세스에서 자원을 빼앗아 다른 프로세스에 할당.
단점: 프로세스 상태 rollback 필요 → 오버헤드 발생.
4. 무시 (Deadlock Ignorance)
교착상태를 해결하는 비용이 너무 크기 때문에, 실제로는 무시하는 방법.
대부분의 범용 OS (Windows, Linux, macOS)는 이 방법 사용.
교착상태 발생 빈도가 낮고, 발생 시 시스템을 재부팅하면 됨.
➡️ 장점: 구현 단순, 오버헤드 없음.
➡️ 단점: 일부 프로세스가 멈출 수 있음.
✅ 요약
방법 특징 장점 단점
예방 (Prevention)
4조건 중 하나 제거
교착상태 절대 발생 X
자원 낭비, 비효율
회피 (Avoidance)
안전 상태 유지
교착상태 피할 수 있음
최대 자원 요구량 필요
탐지 & 회복 (Detection & Recovery)
교착상태 허용 후 탐지·복구
자원 활용률 ↑
탐지·복구 비용 큼
무시 (Ignore)
그냥 무시
단순, 효율 ↑
교착상태 시 시스템 멈춤
메모리 관리 전략
CPU는 PC(program counter)가 지시하는데로 메모리에서 다음 수행할 명령어를 가져옴
주 메모리 ↔ 프로세서 자체에 내장한 레지스터는 CPU의 유일한 범용 저장장치
주 메모리 접근시 속도 차이로 cpu클록 틱 사이클이 소요되고, 지연(stall) 현상이 발생함
논리, 물리 주소
1. CPU와 메모리 접근
CPU는 **프로그램 카운터(PC, Program Counter)**가 가리키는 메모리 주소에서 명령어를 가져와 실행.
CPU 내부에는 **레지스터(Register)**가 있어서 연산에 직접 사용되는 데이터를 저장.
*주 메모리(Main Memory, RAM)**는 CPU가 데이터를 읽고 쓰는 기본 저장 장치지만, **CPU 클록 대비 속도가 느려 지연(stall)**이 발생.
이를 줄이기 위해 **캐시 메모리(Cache)**가 등장 (CPU ↔ 캐시 ↔ 메모리 구조).
2. 논리 주소와 물리 주소
논리 주소(Logical Address, 가상주소)
CPU가 생성하는 주소 (프로그램 관점).
각 프로세스는 독립적인 주소 공간을 가진다고 "착각"할 수 있음.
물리 주소(Physical Address)
실제 메모리 하드웨어(RAM)가 갖는 주소.
주소 변환(Address Translation)
*MMU (Memory Management Unit)**가 논리 주소 → 물리 주소 변환을 담당.
보통 **재배치 레지스터(Relocation Register)**를 사용해서 시작 위치를 보정.
➡️ 이를 통해 다중 프로세스 환경에서도 서로 간섭하지 않고 메모리 사용 가능.
3. 동적 적재 (Dynamic Loading)
프로세스 전체를 메모리에 올리지 않고, 필요한 부분만 메모리에 적재.
장점: 메모리 사용 효율 ↑, 다중 프로그래밍에 유리.
예: 라이브러리를 호출할 때 실제 필요한 함수만 메모리에 로드.
4. 메모리 할당 기법 (연속 메모리 할당)
프로세스들을 메모리에 배치할 때, 빈 공간(free hole)을 어떻게 선택할지 결정하는 방식.
최초 적합(First Fit)
처음 발견한 충분히 큰 공간에 배치.
속도 빠름, 하지만 단편화(fragmentation) 발생 가능.
최적 적합(Best Fit)
크기가 가장 작은, 딱 맞는 공간에 배치.
메모리 낭비 최소화, 그러나 작은 조각(외부 단편화) 많이 생김.
최악 적합(Worst Fit)
가장 큰 공간에 배치.
큰 공간을 나눠 사용 → 큰 프로세스를 위한 공간 확보 가능.
하지만 실제 효율은 떨어짐.
✅ 요약
CPU ↔ 메모리 속도 차이를 줄이기 위해 캐시가 필요.
논리 주소는 CPU가 보는 주소, 물리 주소는 실제 메모리 주소.
MMU가 주소 변환 수행.
동적 적재로 메모리 효율성을 높임.
메모리 배치 기법: 최초 적합, 최적 적합, 최악 적합 → 각각 속도/효율/낭비 측면에서 장단점 다름.
*** 동적 할당 → 외부 단편화 발생
1. 단편화(Fragmentation)
메모리 할당 과정에서 생기는 사용하지 못하는 빈 공간 문제.
(1) 외부 단편화 (External Fragmentation)
여러 번의 메모리 할당/해제로 인해 자잘한 빈 공간이 여기저기 흩어져 전체적으로는 충분한 메모리가 있어도 큰 프로세스를 넣을 수 없는 상황.
예: 10KB 프로세스 필요 → 빈 공간이 2KB+3KB+5KB로 나뉘어 있으면 수용 불가.
해결 방법:
압축(Compaction): 메모리 내용을 옮겨서 빈 공간을 하나로 모음.
(2) 내부 단편화 (Internal Fragmentation)
할당된 블록이 실제 요구보다 큰 경우 발생 → 블록 내에 낭비된 공간 존재.
예: 12KB 요청 → 16KB 단위 블록 할당 → 4KB 낭비.
2. 세그멘테이션 (Segmentation)
*프로그래머가 논리적으로 프로그램을 나눈 단위(세그먼트)**를 메모리에 배치하는 기법.
세그먼트 = 코드, 데이터, 스택 등 가변 크기 블록.
CPU가 생성하는 주소 = (세그먼트 번호, 오프셋)
*세그먼트 테이블(Segment Table)**을 통해 물리 주소 변환.
✅ 장점:
프로그래머 관점 그대로 메모리 관리 가능 (논리적 단위 유지).
외부 단편화 발생하지만, 내부 단편화는 적음.
3. 페이징 (Paging)
메모리를 고정 크기 블록으로 나누는 기법.
프레임(Frame): 물리 메모리를 나눈 블록.
페이지(Page): 프로세스의 논리 주소 공간을 나눈 블록.
크기 동일 (예: 4KB).
➡️ CPU가 생성하는 주소 = (페이지 번호, 페이지 오프셋)
➡️ 페이지 테이블(Page Table): 페이지 번호 → 프레임 번호 변환.
✅ 장점:
외부 단편화 없음 (모두 같은 크기).
내부 단편화만 발생 (마지막 페이지 일부 낭비).
4. TLB (Translation Lookaside Buffer)
페이지 테이블 접근은 메모리 참조이므로 느림 → 매번 하면 2번 메모리 접근(페이지 테이블 + 실제 데이터) 필요.
이를 줄이기 위해 TLB라는 고속 캐시 사용.
최근 변환된 페이지 번호 ↔ 프레임 번호를 저장해 주소 변환 속도 향상.
✅ 요약
단편화
외부 단편화: 작은 조각 흩어짐 → 압축 or 페이징/세그멘테이션으로 해결.
내부 단편화: 블록 단위 때문에 남는 공간 발생.
세그멘테이션: 논리적 단위(코드, 데이터, 스택)를 가변 크기로 관리. → 프로그래머 친화적, 외부 단편화 존재.
페이징: 고정 크기 블록(Frame/Page)으로 관리. → 외부 단편화 없음, 내부 단편화 존재.
페이지 테이블 필요 → 성능 저하 → TLB로 보완.
. 가상 메모리(Virtual Memory)란?
프로세스 전체가 물리 메모리에 적재되지 않아도 실행 가능하게 하는 기법.
사용자 입장에서는 매우 큰 “연속적인 메모리 공간”을 쓰는 것처럼 보이지만, 실제로는 물리 메모리(RAM)와 보조기억장치(디스크, SSD 등)를 조합해서 구현.
*논리 주소(가상 주소)**와 물리 주소를 분리하여 관리.
2. 필요성
메모리 효율성 향상
전체 프로그램을 메모리에 올릴 필요 없이 필요한 부분만 적재 → 더 많은 프로세스를 동시에 실행 가능 (멀티프로그래밍).
보호(Protection)
프로세스마다 독립적인 주소 공간 제공 → 서로 침범 불가.
유연성
실제 메모리보다 큰 프로그램도 실행 가능.
1. 요구 페이징 (Demand Paging)
정의: 프로세스 전체를 메모리에 적재하지 않고, 실제로 필요할 때 해당 페이지를 메모리에 적재하는 기법.
프로그램 실행 시 처음에는 필요한 최소한의 페이지만 로드 → 나머지는 실행 도중 필요할 때 디스크에서 불러옴.
(1) 참조의 지역성(Locality of Reference)
시간 지역성(Temporal Locality): 최근 접근한 데이터는 곧 다시 접근될 가능성 ↑
공간 지역성(Spatial Locality): 접근한 주소 근처의 데이터가 곧 참조될 가능성 ↑
요구 페이징은 이 성질을 활용 → 성능이 실제로는 꽤 좋음.
(2) 페이지 부재(Page Fault)
CPU가 요청한 페이지가 메모리에 없을 때 발생.
처리 과정:
CPU → 페이지 없음 감지 (트랩 발생).
OS → 디스크에서 해당 페이지 적재.
페이지 테이블 갱신 후 재실행.
(3) 유효 접근 시간 (Effective Access Time, EAT)
실제 메모리 접근 시간은 **페이지 부재율(p)**에 비례.
EAT=(1−p)×메모리 접근 시간+p×페이지 폴트 처리 시간EAT = (1 - p) \times \text{메모리 접근 시간} + p \times \text{페이지 폴트 처리 시간}
EAT=(1−p)×메모리 접근 시간+p×페이지 폴트 처리 시간
페이지 폴트 처리 시간은 디스크 I/O가 포함되므로 매우 크다.
따라서 페이지 부재율은 극히 낮아야 성능 유지 가능.
2. 쓰기 시 복사 (Copy-on-Write, COW)
정의: 프로세스가 fork()나 exec()로 복제될 때, 모든 페이지를 처음부터 복사하지 않고 → 부모와 자식이 같은 물리 페이지를 공유하다가 실제로 쓰기(write) 연산이 발생하는 순간 복사하는 기법.
(1) 동작 원리
fork() 시 자식 프로세스는 부모의 페이지를 그대로 참조. (읽기 전용)
어느 한쪽이 해당 페이지를 쓰기(write) 시도 → 페이지 부재 발생.
OS가 그 시점에만 페이지를 새로 복사해서 분리.
(2) 장점
불필요한 페이지 복사를 방지 → 메모리 절약.
fork() 후 exec()가 곧바로 이어지는 경우(자식이 새로운 프로그램 실행) → 부모 메모리 복사는 거의 필요 없음.
✅ 요약
요구 페이징(Demand Paging)
필요한 페이지만 적재 → 메모리 효율 ↑
참조의 지역성 때문에 실제 성능이 만족스러움
하지만 페이지 부재율이 높아지면 성능 저하 (스래싱 위험)
쓰기 시 복사(Copy-on-Write, COW)
부모-자식이 페이지를 공유하다가 쓰기 시점에만 복사
fork() + exec() 최적화에 매우 효과적
1. 페이지 교체(Page Replacement)란?
가상 메모리 시스템에서, 새로운 페이지를 메모리에 불러와야 하는데 빈 프레임이 없을 때 → 기존에 있던 페이지 중 하나를 교체하는 과정.
어떤 페이지를 교체하느냐에 따라 **페이지 부재율(Page Fault Rate)**이 크게 달라짐.
목표: 페이지 부재율을 최소화하는 알고리즘 선택.
2. 프레임 할당 (Frame Allocation)
각 프로세스에 얼마나 많은 프레임을 줄 것인지 결정.
너무 적으면 → 페이지 폴트 잦음.
너무 많으면 → 다른 프로세스가 부족해짐.
3. 주요 페이지 교체 알고리즘
(1) 최적 교체 (Optimal Replacement, OPT)
앞으로 가장 오랫동안 사용하지 않을 페이지를 교체.
이론적으로 가장 좋은 성능 → 페이지 부재율 최소.
하지만 미래 참조를 알 수 없으므로 실제 구현 불가, 비교 기준으로 사용.
(2) FIFO (First-In First-Out)
메모리에 가장 오래 있던 페이지를 교체.
구현 단순하지만, 성능이 나쁠 수 있음.
Belady의 모순(Belady’s Anomaly): 프레임 수를 늘렸는데도 페이지 폴트가 증가할 수 있음.
(3) LRU (Least Recently Used)
가장 오랫동안 사용하지 않은 페이지를 교체.
과거 사용 이력이 미래 사용 가능성과 연관 있다는 "참조의 지역성(Locality)"에 기반.
일반적으로 가장 널리 쓰임.
구현 방식:
카운터 기반 (최근 접근 시간 기록)
스택 기반 (최근 사용된 페이지를 스택 상단에 유지)
(4) LFU (Least Frequently Used)
사용 빈도가 가장 낮은 페이지를 교체.
문제: 최근 집중적으로 쓰였지만 앞으로 필요 없는 페이지도 남아있을 수 있음.
(5) Clock 알고리즘 (Second Chance)
FIFO 변형: 교체 대상 페이지에 참조 비트(Reference Bit) 확인.
참조된 페이지는 한 번 기회를 주고 다음 후보로 넘김.
성능은 LRU에 근접하면서 구현은 단순.
4. 선택 기준
실제 운영체제에서는 LRU 또는 Clock 알고리즘이 주로 사용.
이유:
OPT는 이상적이지만 불가능.
FIFO는 성능 불안정.
LRU는 locality 가정 하에 안정적으로 좋은 성능.
Clock은 LRU 근사치로 구현 효율성 높음.
✅ 요약
페이지 교체는 빈 프레임이 없을 때 어떤 페이지를 제거할지 결정하는 문제.
목표: 페이지 부재율 최소화.
대표 알고리즘: OPT, FIFO, LRU, LFU, Clock.
실제 OS는 **LRU(또는 근사 알고리즘)**을 가장 많이 사용.
1. 쓰레싱(Thrashing)이란?
과도한 페이지 부재(Page Fault) 때문에 CPU가 실제 작업보다 페이징 처리에 더 많은 시간을 소모하는 현상.
결과적으로 CPU 이용률 급격히 저하, 시스템 성능이 심각하게 떨어짐.
즉, 프로세스가 필요한 페이지가 메모리에 거의 없어서 계속 디스크 ↔ 메모리 스왑이 일어나는 상태.
2. 원인
메모리 과다 할당 부족
프로세스들이 동시에 실행되면서 각 프로세스에 충분한 프레임을 배정하지 못한 경우.
지역성(Locality) 위반
요구 페이징은 참조의 지역성을 가정하는데, 프로그램이 메모리 전체를 자주 건드리면 페이지 폴트 ↑.
다중 프로그래밍 정도(Multiprogramming Degree) 과도
너무 많은 프로세스를 동시에 메모리에 올려두면 각 프로세스가 필요한 최소 프레임을 확보하지 못함.
3. 증상
페이지 부재율(Page Fault Rate) ↑
CPU 이용률(CPU Utilization) ↓
디스크 I/O 폭증
프로그램 실행 속도 급격히 저하
4. 해결 방법
(1) 최소 프레임 보장
각 프로세스에 필요한 최소 프레임 수를 보장해야 함.
예: 페이지 교체 알고리즘과 함께 최소 프레임 개수를 할당.
(2) 작업 집합 모델 (Working Set Model)
프로세스가 일정 시간 동안 자주 참조하는 페이지 집합 = 작업 집합(Working Set).
이 집합을 모두 메모리에 적재 → 페이지 폴트 감소.
(3) PFF (Page Fault Frequency) 방식
페이지 폴트율을 측정해 임계값 초과 시 → 프레임을 늘려주고, 낮으면 줄임.
동적으로 프레임 수를 조절해 thrashing 방지.
(4) 다중 프로그래밍 정도 조절
시스템에 동시에 실행되는 프로세스 수를 줄임.
즉, 일부 프로세스를 swap-out 해서 나머지 프로세스가 충분한 프레임 확보하도록 함.
✅ 요약
Thrashing = CPU가 일 못 하고 페이징만 하는 상태.
원인: 프레임 부족, 지역성 위반, 다중 프로그래밍 과도.
해결: 최소 프레임 보장, 작업 집합(Working Set), PFF(Page Fault Frequency), 다중 프로그래밍 정도 조절.
OPTIONS /api/data HTTP/1.1
Origin: <https://myapp.com>
Access-Control-Request-Method: PUT
서버 응답:
Access-Control-Allow-Origin: <https://myapp.com>
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Credential 요청 (쿠키/인증 포함)
fetch나 XHR에서 credentials: include 설정
서버가 반드시 다음 헤더 필요:
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: <https://myapp.com> (와일드카드 * 사용 불가)
🟦 OSI 7계층 구조
1. 물리 계층 (Physical Layer)
역할: 데이터의 비트(0/1) 신호를 실제 전송 매체(케이블, 전파 등)를 통해 송수신.
장비 예시: 허브, 리피터, 케이블, 커넥터
단위: 비트 (Bit)
2. 데이터 링크 계층 (Data Link Layer)
역할: 물리 계층에서 전송된 비트를 프레임(Frame) 단위로 관리. 에러 검출/수정, 흐름 제어 수행.
MAC 주소 사용 → 동일 네트워크 내에서 장치 식별.
장비 예시: 스위치, 브리지
단위: 프레임 (Frame)
3. 네트워크 계층 (Network Layer)
역할: 데이터의 목적지까지의 경로 선택(라우팅), 논리적 주소(IP) 부여.
IP 주소 사용 → 다른 네트워크 간 통신 가능.
프로토콜 예시: IP, ICMP, ARP, RARP
장비 예시: 라우터
단위: 패킷 (Packet)
4. 전송 계층 (Transport Layer)
역할: 종단 간(end-to-end) 통신 제공. 데이터의 신뢰성 보장.
주요 프로토콜:
TCP(연결지향, 신뢰성 보장, 흐름/혼잡 제어)
UDP(비연결성, 빠르지만 신뢰성 낮음)
단위: 세그먼트 (TCP) / 데이터그램 (UDP)
5. 세션 계층 (Session Layer)
역할: 통신 세션(대화)의 생성, 유지, 종료 관리.
예: 로그인 세션, 원격 프레젠테이션 연결
기능: 동기화, 체크포인트, 복구
6. 표현 계층 (Presentation Layer)
역할: 데이터의 표현 방식 통일. 암호화, 압축, 인코딩/디코딩.
예: JPEG, GIF, MP3, TLS/SSL 암호화
키워드: 번역기 역할
7. 응용 계층 (Application Layer)
역할: 최종 사용자와 직접 맞닿아 있는 계층. 네트워크 서비스 제공.
프로토콜 예시:
HTTP/HTTPS (웹)
FTP (파일 전송)
SMTP/IMAP/POP3 (이메일)
DNS (도메인 네임 변환)
🔹 정리 (계층별 단위 & 장비)
계층 단위 주요 장비/기술
7. 응용
데이터
웹 브라우저, 앱
6. 표현
데이터
암호화/압축
5. 세션
데이터
API 세션 관리
4. 전송
세그먼트/데이터그램
TCP, UDP
3. 네트워크
패킷
라우터
2. 데이터링크
프레임
스위치
1. 물리
비트
케이블, 허브
애플리케이션 구조
📌 애플리케이션 구조 (Application Architecture)
애플리케이션 구조는 개발자가 설계한 방식에 따라 다양한 종단 시스템(End System)에서 애플리케이션이 어떻게 조직되는지를 설명한다.
대표적으로 클라이언트-서버 구조와 P2P 구조가 있다.
🔹클라이언트-서버 구조 (Client–Server Architecture)
서버(Host): 항상 켜져 있고, 클라이언트의 요청을 처리하는 중심 시스템
클라이언트(Client): 다수의 호스트에서 실행되며, 서버에 요청을 보내는 역할
특징
중앙 집중형 구조
보안, 데이터 관리, 성능 최적화에 유리
서버 부하가 집중되면 확장 비용 증가
🔹 P2P 구조 (Peer-to-Peer Architecture)
특징
항상 켜져 있는 기반 서버에 최소한만 의존
피어(Peer): 간헐적으로 연결되는 호스트 쌍이 직접 서로 통신
자가 확장성(Self-Scalability): 네트워크에 참여하는 피어 수가 늘어날수록 자원도 함께 증가
탈 중앙화된 구조로 확장성과 유연성이 뛰어남
🔹 프로세스 (Process)
정의: 종단 시스템에서 실행되는 프로그램 단위
관심 대상: 프로세스 간 통신
같은 종단 시스템 내부 프로세스 간 통신
다른 종단 시스템 간 프로세스 간 통신
클라이언트 & 서버 개념
클라이언트(Client): 통신 세션을 초기화하는 프로세스
서버(Server): 통신 세션을 시작하기 위해 접속을 기다리는 프로세스
✅ 요약하면,
클라이언트-서버 구조는 중앙 집중형으로 관리가 용이하지만 서버 부하가 커짐
P2P 구조는 분산형으로 확장성이 뛰어나지만 관리와 보안이 상대적으로 복잡
프로세스 관점에서는 누가 먼저 접속을 시도하는지(클라이언트)와 기다리는지(서버)에 따라 역할이 나뉨
📌 프로세스와 컴퓨터 네트워크 사이의 인터페이스
🔹 소켓 (Socket)
정의: 애플리케이션과 네트워크 사이의 API
역할: 프로세스가 네트워크를 통해 메시지를 주고받을 수 있도록 하는 인터페이스
🔹 애플리케이션 계층 프로토콜
호스트 식별: 인터넷에서는 IP 주소로 호스트를 식별
주요 요구 사항
신뢰적 데이터 전송
손실 없는 데이터 보장을 원할 때: TCP
손실 허용 애플리케이션(예: 스트리밍): UDP
처리량(Throughput)
보안(Security): SSL/TLS 계층에서 제공
인터넷에서 대표적으로 사용되는 프로토콜
TCP, UDP (트랜스포트 계층)
HTTP (애플리케이션 계층)
📌 HTTP (HyperText Transfer Protocol)
특징
온-디맨드(On-Demand) 방식
웹에서 사용하는 대표적 애플리케이션 계층 프로토콜
기본적으로 TCP(HTTP/2.0 이하) 위에서 동작
비상태성(Stateless) → 쿠키(Cookie)로 상태 관리 보완
🔹 HTTP 버전 별 특징
✅ HTTP 1.0
비 지속 연결 (Non-Persistent Connection)
요청–응답마다 TCP 연결을 새로 설정
지속 연결 (Persistent Connection) 지원 시작
✅ HTTP 1.1
기본적으로 지속 연결(Persistent)
Keep-Alive 헤더를 통해 연결 유지
✅ HTTP 2.0
멀티플렉싱(Multiplexing): 하나의 연결에서 다중 요청/응답 처리
성능 개선: 지연 감소, 헤더 압축, 서버 푸시(Server Push) 지원
** 하나의 TCP 연결을 통해 여러 데이터 요청을 병렬로 전송
✅ 요약하면,
소켓은 프로세스와 네트워크의 인터페이스
TCP/UDP가 신뢰성과 속도의 기준을 결정
HTTP는 웹의 핵심 애플리케이션 프로토콜로, 버전 업그레이드마다 성능 최적화와 연결 효율성이 강화됨
쿠키로 Stateless 한 HTTP를 보완
📌 웹 캐싱 (Web Caching)
🔹 정의
웹 캐시(Web Cache, Proxy Server):
웹 서버 대신 클라이언트의 HTTP 요청을 처리하는 네트워크 개체
동작 원리:
브라우저가 요청한 객체(HTML, CSS, JS, 이미지 등)를 캐시에 저장
같은 요청이 다시 오면 원 서버(origin server)에 가지 않고 캐시에서 응답
🔹 특징
성능 향상: 지리적으로 가까운 캐시 서버에서 응답 → 지연 시간 감소
대역폭 절약: 동일 콘텐츠 반복 요청 시 원 서버로의 전송 감소
부하 분산: 원 서버의 트래픽을 줄여 부하 완화
운영 주체: 일반적으로 ISP(Internet Service Provider) 가 구입 및 설치
🔹 조건부 GET (Conditional GET)
웹 캐시가 저장된 객체가 여전히 최신인지 확인하기 위해 사용되는 HTTP 요청 방식
ETag(Entity Tag)
서버가 객체에 대해 생성하는 고유 식별자
요청 시 If-None-Match 헤더로 전달
서버는 ETag 비교 후 같으면 304 Not Modified 응답 → 캐시된 데이터 사용
Last-Modified
객체의 최종 수정 시간 정보를 제공
요청 시 If-Modified-Since 헤더로 전달
변경 없으면 304 Not Modified 응답
🔹 요청 흐름 요약
클라이언트 ──HTTP 요청──> 캐시 서버
│
├─ 캐시에 있으면 → 바로 응답
│
└─ 캐시에 없거나 만료되면 → 원 서버에 요청
│
└─ 최신 여부 확인 (조건부 GET: ETag, Last-Modified)
✅ 정리하면,
웹 캐시는 프록시처럼 동작하여 성능과 효율을 개선
조건부 GET은 캐시된 콘텐츠의 유효성을 확인하는 핵심 메커니즘
304 Not Modified 응답으로 불필요한 데이터 전송을 줄임
Cache-Control 헤더
🔹 정의
HTTP/1.1부터 도입된 캐싱 정책 제어 헤더
서버가 응답 시 Cache-Control 헤더를 포함하면, 브라우저나 프록시 캐시가 리소스를 얼마나, 어떤 방식으로 캐싱할지 결정
alwaysApply: true면 항상 컨텍스트에 포함 이 3개가 룰 동작의 핵심입니다. (Cursor UI의 “Rule Type” 드롭다운도 내부적으로 이 속성 조합을 바꿔줍니다.)
어떻게 활용할 수 있을까?
팀 컨벤션 문서화 기존에는 Notion이나 Wiki에만 적어두던 “팀 규칙”을 Cursor 룰로 옮겨 AI에게 직접 인식시킬 수 있습니다.
예: “새 페이지는 반드시 app/(dashboard)/ 구조를 따를 것”, “데이터 패칭에는 TanStack Query를 활용할 것”
AI 코드 생성 품질 향상 단순한 함수 작성 요청에도 AI가 미리 정의한 룰을 참고해 구조적이고 일관성 있는 코드를 제안합니다.
예: “API 호출 시 반드시 Result<T, E> 타입으로 감싸고, 실패 시 로깅 + 사용자 메시지 제공”
리뷰 비용 절감 코드 리뷰 단계에서 발생하는 반복적인 스타일 및 규칙 지적이 줄어들어, 리뷰어는 아키텍처 적합성이나 로직 검증에 집중할 수 있습니다.
지속적인 팀 학습 도구 신입 개발자나 새로운 팀원이 투입될 경우, 룰에 의해 생성되는 코드만 보더라도 팀이 어떤 방식을 선호하는지 자연스럽게 익힐 수 있습니다.
실제로 어떻게 활용하고 있는지?
1. PR 리뷰
---
description: "실무형 PR 템플릿"
alwaysApply: false
---
# 📑 실무형 PR 템플릿
## 목적
- PR 문서를 일관된 구조와 충분한 근거로 작성한다.
- 변경 이유(Why) → 설계(How) → 검증(Proof) 흐름을 따른다.
- 문서는 **마크다운** 형식으로 작성한다.
---
## 🟢 필수 섹션
### Description
- 변경 범위를 한 줄로 요약한다.
- 핵심 키워드를 포함한다.
### 배경
- 기존 동작의 한계나 문제점을 설명한다.
- 개선이 필요한 이유를 명확히 기술한다.
### 주요 변경사항
- 개선된 동작 방식을 요약한다.
- 데이터 구조, 처리 방식, 정책 등 변경된 규칙을 나열한다.
### 테스트
- 주요 시나리오 목록을 항목으로 나열한다.
- 실행 방법을 간단히 명시한다.
- 현재 테스트 결과를 요약한다.
### 성능 분석 (필수)
- **시간복잡도**와 **공간복잡도**를 명시한다.
- 계산 결과가 `O(n^2)` 이상일 경우, **주의가 필요하다는 경고 문구**를 반드시 포함한다.
- 예: `이 PR은 최악의 경우 O(n^2) 복잡도를 가질 수 있으므로, 대규모 데이터 환경에서는 성능 저하에 유의해야 함.`
---
## 🟡 선택 섹션 (대규모/구조 변경 PR 시 작성)
### 타입/구조 변경
- 변경 전/후 구조를 항목으로 정리한다.
- 호환성 영향과 범위를 명확히 기술한다.
### 동작 규칙
- 포함/제외 조건을 단계별로 기술한다.
- 결과 반영 조건과 불변성 원칙을 기록한다.
### 구현 파일
- 변경된 파일 경로를 나열한다.
- 각 파일의 역할과 변경 이유를 요약한다.
### 검증 방법
- 테스트 통과 확인 절차를 적는다.
- 샘플 데이터나 실제 UI 확인 절차를 체크리스트로 작성한다.
### 변경 파일 요약
- 파일별 변경 의도를 불릿 형태로 정리한다.
---
## 작성 규칙 (Do/Don’t)
- **Do**
- 필수 섹션은 항상 작성한다.
- 선택 섹션은 변경 규모·영향에 따라 추가한다.
- 구조 변경은 호환성 영향까지 반드시 기록한다.
- 불변성 원칙은 필요 시 명시한다.
- 시간·공간복잡도는 항상 기록하고, `O(n^2)` 이상일 경우 반드시 경고 문구를 추가한다.
- **Don’t**
- 근거 없는 성능 수치를 기재하지 않는다.
- 불필요하게 긴 코드 조각을 문서에 직접 삽입하지 않는다(파일 경로나 링크로 대체).
---
## 리뷰어 체크리스트
- [ ] 필수 섹션이 모두 포함되었는가?
- [ ] 변경 이유와 개선 방법이 명확하게 연결되는가?
- [ ] 구조 변경 시 영향 범위가 기술되었는가?
- [ ] 테스트 시나리오가 주요 변경사항을 커버하는가?
- [ ] 시간/공간복잡도가 기록되었는가?
- [ ] 복잡도가 `O(n^2)` 이상일 경우, 경고 문구가 포함되었는가?
- [ ] 성능 및 추후 과제가 현실적으로 기술되었는가?
AI가 가장 강력한 부분이죠. 분석엔 강력하지만 상대적으로 창조엔 약합니다. 가장 만족스러운 부분인데
cursor는 문서화에 큰 강점이 있습니다.
기획 문서 내용과 "지금까지 작업한 내용을 main 브랜치와 지금 브랜치의 차이를 기반으로 분석해서 PR 문서를 마크다운으로 만들어줘" 라고 위 룰과 함께 명령을 내리면, 작업 컨택스트가 없는 리뷰어도 손쉽게 맥락 파악이 가능해집니다.
그리고 리뷰어 혹은 개발자가 성능 병목사항을 일일히 짚어내기 어려운데, 시간복잡도 공간복잡도 측면에서 코드를 분석해주니
코드 품질도 높일 수 있고 주니어/신입 개발자에게는 어떻게 개발해야하는지 피드백을 AI한테 받아서 개발이 가능해집니다.
2. Test 코드 작성
---
description: Documentation and testing guidelines, available on demand
alwaysApply: false
---
## Documentation
- After defining any function, method, or component:
- Write a JSDoc comment explaining its purpose, parameters, and return value.
- Keep it concise but informative.
## Testing
- Test code with various inputs, including edge cases.
- use **vitest** for testing, not jest
- Include both positive and negative scenarios.
- Use a clear "as is → when → to be/should" structure:
- `describe('<Unit>')`
- `describe('as is: <current state>')`
- `describe('when <condition>')`
- `it('to be: <expected>, should <assertions>')`
- Avoid testing implementation details — focus on public behavior.
AI가 가장 강력한 부분 2죠.
잘개 쪼개져서 격리된 모듈과 순수함수로 코드를 구현하고, 작은 context 토큰 범위와 기획 문서를 제공한다면 비교적 정확한 테스트케이스를 뽑아낼 수 있습니다.
3. 컨벤션 강제
---
description: Always apply modularization and abstraction principles for maintainable frontend architecture
alwaysApply: true
---
## Core Principles
### 1. Side Effect Isolation
- Treat code that depends on environment (time, network, browser API) as **side effects**.
- Isolate side effects into separate modules or inject them as parameters.
- In tests, mock side-effect modules to avoid flaky results.
- Keep core functions as pure as possible.
### 2. Component Layer Separation
- Split each component into the following logical layers:
1. **UI (Render)** – Pure presentational JSX, no business logic.
2. **State** – State management hooks, selectors, store.
3. **Domain Logic** – Business rules and computations.
4. **Network (API)** – Data fetching/mutations, isolated into API modules.
- Ensure that these layers communicate via explicit interfaces.
### 3. Strategy Pattern & Dependency Injection
- For business policies or algorithms that may change (e.g., payment calculation, discount rules):
- Define them as strategy interfaces.
- Implement strategies separately.
- Inject the strategy into components/hooks instead of hardcoding.
- Components should depend only on the interface, not concrete implementations.
### 4. Domain Policy Ownership
- Avoid embedding business policy data directly in frontend code.
- Fetch policies from backend whenever possible.
- Frontend focuses on **how** to display/use the data, backend controls **what** policy applies.
기존 코드들이 규칙대로 잘 짜여졌다면 더욱 강력해집니다.
코드를 짤때, 팀이 원하는 스타일 및 컨밴션으로 코드를 구현하도록 권장합니다.
AI가 마구잡이로 코드를 짜는걸 방지해주죠
마무리
정리하자면, Cursor 룰은 “AI에게 우리 팀의 개발 문화를 주입하는 장치"라고 볼 수 있습니다. 단순한 스타일 교정이 아니라, 아키텍처, 성능, 보안, 접근성, 테스트 등 다양한 영역의 원칙을 미리 정의해두고 이를 코드 생성 단계에 반영할 수 있다는 점이 큰 장점입니다.
덕분에 개발팀은 규칙을 반복적으로 확인하거나 리뷰에서 같은 지적을 주고받을 필요가 줄어들고, 진짜 중요한 설계와 문제 해결에 집중할 수 있습니다.
개인적으론 문서화가 제일 마음에 드네요. 코드 리뷰 전 코드 품질을 높이기 위한 하나의 셀프 피드백 단계를 추가한 느낌입니다.
그리고 이 글에서는 "i18n을 어떻게 적용하는지"까지 내용에 넣으면 글이 너무 길어지기 때문에 다루지 않습니다.
next-intl등 여러 좋은 방식을 찾아보길 추천드립니다.
SEO란?
SEO(Search Engine Optimization, 검색엔진 최적화)는 우리가 작성한 블로그 글이 구글 같은 검색 엔진 결과에서 더 잘 보이도록 만들어주는 과정입니다. 처음에는 다소 복잡해 보일 수 있지만, 하나씩 따라 해 보시면 분명 홈페이지 노출에 큰 도움이 될 거예요. 이 글에서는 SEO의 기초 개념부터 온페이지/테크니컬/오프페이지 SEO, 구조화 데이터 활용, 유용한 도구, 그리고 배포 전 체크리스트까지 차근차근 설명해 하겠습니다.
1. SEO 기본 용어 이해하기
먼저 SEO를 공부할 때 자주 접하는 핵심 용어들을 알아보겠습니다. 어려운 영어 용어도 많지만, 최대한 쉽게 풀어서 설명해 드릴게요.
크롤링 (Crawling): 검색 엔진의 로봇 프로그램인 크롤러(crawler)가 웹사이트의 페이지들을 찾아다니며 콘텐츠를 수집하는 과정입니다. 예를 들어 구글의 크롤러인 Googlebot이 여러분의 블로그 글을 발견하고 내용을 긁어가는 작업이죠. 크롤링이 잘 되어야 내 블로그 글이 검색엔진에 알려지게 됩니다.
인덱싱 (Indexing): 크롤링한 페이지들을 검색 엔진의 데이터베이스에 저장하고 정리하는 과정입니다. 검색 엔진은 수집된 정보를 분류하고 색인(인덱스)을 만들어 두는데, 도서관에서 책을 분류해 두는 것과 비슷합니다. 페이지의 키워드나 내용에 따라 잘 정리되어야 사용자가 검색할 때 해당 페이지가 나타날 수 있어요.
검색 결과 페이지 (SERP): Search Engine Results Page의 줄임말로, 말 그대로 사용자가 어떤 키워드로 검색했을 때 나오는 검색 엔진 결과 페이지를 뜻합니다. 특히 첫 번째 페이지 결과를 SERP라고 부르는 경우가 많습니다. 대부분의 사용자들은 검색 결과 첫 페이지까지만 보고 끝내기 때문에(75% 이상이 첫 페이지에서 검색을 마친다고 합니다). 내 블로그 글이 SERP 상위에 노출되는 것이 매우 중요합니다.
메타 태그 (Meta Tag): 웹페이지의 <head> 영역에 들어가는 메타데이터(정보)로서, 검색 엔진과 브라우저에게 페이지 정보를 제공합니다. 대표적으로 제목(title)과 설명(description) 메타태그가 중요합니다. 메타 태그는 방문자 눈에는 직접 보이지 않지만, 검색 결과 목록에 표시되는 제목과 설명을 결정합니다.
캐노니컬 태그 (Canonical Tag): 동일하거나 매우 비슷한 콘텐츠가 여러 URL로 중복돼 있을 때, 검색 엔진에 "이 페이지의 원본은 이것이에요!" 하고 알려주는 태그입니다. <head> 안에 <link rel="canonical" href="정규 URL"> 형태로 넣습니다. 이 태그를 지정하면 검색 엔진은 중복 페이지들 중 canonical로 지정된 URL을 대표 페이지로 간주하여 랭킹 신호를 집중시킵니다. 예를 들어, example.com?page=1이나 example.com/index.html 등 여러 주소로 접속되는 같은 내용의 페이지가 있다면 하나를 canonical로 지정해야 불필요한 중복 색인을 막을 수 있습니다.
** 찾아본 결과, 다국어 홈페이지는 각각 번역한 페이지가 자신의 canoinical Tag 주인이 되는게 유리합니다.!
사이트맵 (sitemap.xml): 웹사이트의 모든 페이지 목록을 모아둔 파일로, 책의 목차 같은 역할을 합니다. 주로 XML 형식으로 작성되며 사이트의 최상위 경로(예: https://내블로그/sitemap.xml)에 위치합니다. 사이트맵을 검색 엔진에 제출해 두면 크롤러가 사이트 구조를 한눈에 파악하여, 평소 찾기 어려운 페이지도 빠짐없이 크롤링하고 인덱싱할 수 있게 됩니다 (※ 티스토리의 경우 기본적으로 RSS 피드가 사이트맵 역할을 하지만, 필요에 따라 직접 sitemap.xml을 제작해 Search Console에 제출할 수도 있습니다.)
robots.txt: 웹사이트 최상위 경로(루트 디렉터리)에 두는 텍스트 파일로, 검색 엔진 크롤러의 접근을 제어하는 지침을 적어둡니다. 예를 들어 "이러이러한 곳은 크롤링하지 마!" 혹은 "이 사이트맵 파일 위치는 여기야!" 하는 내용을 담습니다. robots.txt 파일이 없으면 크롤러는 웹사이트에서 접근할 수 있는 모든 페이지를 자유롭게 크롤링하며 특정 페이지를 검색 결과에 나오지 않게 막고 싶다면 robots.txt에 경로를 지정해 차단할 수 있습니다. 또한 robots.txt 맨 아래에 Sitemap: 지시어를 넣어 사이트맵 위치도 알려줄 수 있어요.
구조화 데이터 (Structured Data): 페이지의 내용을 검색 엔진이 더 체계적으로 이해할 수 있도록 추가하는 특별한 형식의 데이터입니다. 예를 들어 블로그 글이 어떤 주제의 기사인지, 작성자는 누구인지, 게시 날짜는 언제인지 등을 구조화된 형식으로 마크업하면, 검색 엔진이 그것을 읽고 이해하여 검색 결과에 풍부한 정보(리치 결과)를 보여줄 수 있습니다. 구조화 데이터는 주로 스키마.org 형태로 표현하며, HTML <script> 태그 안에 JSON-LD 형식으로 넣는 방법이 권장됩니다. 이를 적용하면 검색 결과에서 별점, 자주 묻는 질문(FAQ), 빵부스러기 경로 등 추가 정보가 표시될 수 있어 클릭률을
높이는 데 도움이 됩니다. (실제로 리치 결과로 표시된 페이지는 일반 결과보다 클릭률이 82% 높았다는 사례도 있습니다)
각 용어가 뭘 뜻하는지 글 아래에서 차근차근 알려드릴게요.
2. 온페이지 SEO 최적화
온페이지 SEO는 말 그대로 페이지 내부에서 할 수 있는 최적화 작업입니다. 블로그 글의 콘텐츠와 HTML 구조를 잘 정돈하여 검색 엔진 친화적으로 만드는 것을 의미합니다. 특히 티스토리 블로그 글을 작성할 때 아래 요소들을 신경 써 보세요.
메타 태그 최적화: 제목(title)과 설명(description)
온페이지 SEO의 첫 걸음은 메타태그 설정입니다. 검색 엔진은 페이지의 <title> 태그와 <meta name="description"> 태그를 읽어 해당 페이지의 제목과 요약을 이해합니다. 이 두 가지를 잘 써주는 것만으로도 SEO 효과를 크게 볼 수 있습니다:
제목 태그: 블로그 글 제목은 <title>에 해당하며, 검색 결과에 파란색 큰 제목으로 표시됩니다. 글 내용과 밀접하며 핵심 키워드를 포함한 제목을 작성하세요. 예를 들어 이 글의 제목이 <title>티스토리 초보 개발자를 위한 블로그 SEO 가이드</title>라면, "티스토리", "개발자", "SEO 가이드" 같은 키워드가 담겨 있죠. 너무 길지 않게 (권장 50~60자 이내) 명확한 제목을 정하는 것이 좋습니다.
메타 설명: <meta name="description" content="..."> 부분에 글 요약을 넣을 수 있습니다. 이 내용은 검색 결과의 회색 설명문구로 나타나며, 사용자들이 이 글을 클릭할지 결정하는 데 큰 영향을 줍니다. 간결하면서도 흥미를 끄는 요약을 1~2문장 정도 작성해 보세요. (티스토리에서는 글 작성 시 본문 요약 또는 설명 입력란이 있다면 활용하시면 됩니다.) 만약 메타 설명을 직접 작성하지 않으면, 검색 엔진이 본문 일부를 자동으로 발췌해 보여줍니다.
예시로 HTML 메타태그를 작성하면 다음과 같습니다
<head>
<title>티스토리 초보 개발자를 위한 블로그 SEO 가이드</title>
<meta name="description" content="티스토리에서 기술 블로그를 시작한 초심자 개발자를 위한 SEO 최적화 방법을 소개합니다.">
</head>
위와 같이 제목과 설명을 명확히 해 두면, 검색 엔진에 내 콘텐츠를 잘 소개하고 사용자도 클릭하기 전에 내용을 파악하기 쉬워집니다. 티스토리도 잘 찾아보시면 위 태그가 존재해요.
2-1) 시멘틱 태그 및 헤딩 태그 활용: H1~H3 구조 잡기
블로그 글을 작성할 때 시멘틱 태그를 체계적으로 사용하는 것도 중요합니다. 예시 한가지로 헤딩(heading)태그를 들어볼게요.
헤딩은 글의 제목과 소제목들을 나타내는 HTML 태그인데요:
<h1>: 한 페이지에 단 한 번만 사용하는 최상위 제목입니다. 일반적으로 블로그 글의 제목이 h1에 해당합니다 (티스토리에서는 글 제목을 자동으로 h1으로 처리해줍니다).
<h2>, <h3>: 글의 주요 섹션 제목(h2)과 그 하위 소제목(h3)에 사용합니다. 글의 흐름을 논리적으로 나누어 주며, 중요한 키워드를 적절히 포함해 주는 게 좋습니다. 예를 들어 지금 보고 계신 글도 각 큰 챕터 제목이 h2, 그 안의 작은 주제가 h3 태그로 구성되어 있습니다.
검색 엔진은 헤딩 구조를 통해 이 페이지가 어떤 주요 주제와 하위 주제들로 이루어져 있는지 파악합니다. 따라서 헤딩을 계층적으로 잘 쓰면 검색엔진이 콘텐츠를 이해하기 쉽고, 독자들도 글을 한눈에 보기 편해집니다. 아래는 간단한 예시입니다:
<h1>블로그 글 제목 (예: 웹 접근성 가이드)</h1>
<p>여는 문단...</p>
<h2>1. 접근성이란 무엇인가?</h2>
<p>...설명...</p>
<h3>접근성의 중요 요소</h3>
<p>...설명...</p>
<h2>2. 접근성을 향상시키는 방법</h2>
<p>...설명...</p>
이처럼 h1 -> h2 -> h3 순으로 논리적인 아웃라인을 만들고, 단순히 글자 크기를 키우는 용도로 쓰지 않도록 주의하세요. 디자인을 위해 글자 크기를 키울 때는 CSS를 사용하고, 의미적인 구조는 헤딩 태그로 표현하는 것이 SEO에 좋습니다
2-2) 내부 링크 구성하기: 사이트 내 유기적 연결
내부 링크란 내 블로그의 다른 글이나 페이지를 본문 중에 연결하는 것입니다. 예를 들어 이전 글이나 관련 글이 있다면 본문에서 해당 키워드에 하이퍼링크를 걸어두는 식이죠. 내부 링크 최적화를 위한 팁은 다음과 같습니다:
관련 글 연결: 현재 작성 중인 글과 주제가 비슷하거나 참고하면 좋을 이전 글이 있다면 링크를 거세요. 예를 들어 "지난 번에 HTTP와 HTTPS 차이에 대해 다뤘는데, 자세한 내용은 이 글에서 확인하세요."처럼요.
키워드 앵커텍스트: 링크를 걸 때 클릭 here 같은 문구 대신 해당 페이지를 잘 나타내는 키워드로 링크를 거는 것이 좋습니다. 예를 들어 데이터베이스 성능 튜닝 글이라면 "데이터베이스 인덱싱 기법에 대해서는 이전 포스팅 인덱싱 최적화 방법을 참고하세요." 이렇게 굵은 키워드 부분에 링크를 거는 것이 SEO에 도움이 됩니다.
사이트 구조 파악 도움: 내부 링크가 잘 연결되어 있으면, 방문자가 내 블로그 안에서 더 많은 페이지를 탐색하게 되어 체류 시간도 늘어나고 이탈률이 줄어듭니다. 또한 크롤러도 링크를 따라가며 사이트의 콘텐츠를 효율적으로 발견하므로 색인 구축에 유리해요.
내부 링크를 너무 과하게 걸 필요는 없지만, 자연스럽게 연결될 수 있는 부분이 있으면 적극 활용해 보세요. 티스토리에서는 글 작성 시 편집기에서 링크 아이콘을 눌러 쉽게 본문 링크를 추가할 수 있습니다.
참고: 온페이지 SEO에서는 이 외에도 이미지의 대체 텍스트(alt 속성)를 넣어주는 것, 모바일 친화적 디자인을 사용하는 것, 페이지 로딩 속도를 높이는 것 등이 모두 포함됩니다. 하지만 이러한 부분은 아래 테크니컬 SEO에서 추가로 다루겠습니다.
** 글을 잘 쓰면 자발적으로 사람들이 레퍼런스로 참조하겠죠? 글 품질이 중요한 부분입니다.
3. 테크니컬 SEO (Technical SEO) 고려사항
이제 테크니컬 SEO 분야로 넘어가 보겠습니다. 테크니컬 SEO는 말 그대로 사이트 기술적 요소들을 최적화하여 검색 엔진이 사이트를 더 잘 크롤링/인덱싱하고, 사용자에게도 더 나은 경험을 제공하도록 하는 작업들입니다. 초심자 개발자 분들도 알아두면 좋은 주요 항목을 살펴보겠습니다.
3-1) Canonical URL 설정하기
앞서 용어 설명에서 다룬 캐노니컬 태그를 다시 한 번 강조합니다. 만약 티스토리 블로그에서 동일한 콘텐츠가 여러 경로로 접근될 수 있다면 canonical 설정을 고려해야 합니다. 예를 들어, "http://"로도 열리고 "https://"로도 열리거나, ?category=... 같은 파라미터에 따라 같은 글이 다른 URL로 보이는 경우가 있을 수 있습니다. 이런 상황에서는 페이지 <head>에 다음과 같이 원본 URL을 지정하는 캐노니컬 태그를 넣습니다:
이렇게 하면 검색 엔진은 중복된 여러 URL 중 canonical로 지정된 주소만 대표로 취급하고 나머지는 중복으로 판단하여 랭킹에 불이익이 없도록 처리해줍니다.
티스토리 기본 도메인을 사용하는 경우에는 큰 문제는 없지만, 개인 도메인을 연결한 경우 yourblog.tistory.com와 www.yourdomain.com 두 가지 주소로 접속되는 이슈가 생길 수 있으니 이럴 때 canonical 태그로 하나로 정해주시면 좋습니다.
사이드 프로젝트에서 기본주소/{언어} 로 각 언어로 번역한 동일한 페이지를 만들어놓고, 각 페이지 별로 각각 카노니컬 태그를 설정해줬습니다. 예를 들어 /ko(한국어) 페이지에는 /ko가 canonical Tag 주인으로 설정되어 있고 다른 언어를 alternate로 설정해두었고, /ja(일본어) 페이지에선 /ja가 canonical Tag로 지정되어 있고 다른 언어들이 alternate로 지정되어 있습니다.
3-2) 사이트맵 파일과 robots.txt 설정하기
sitemap.xml과 robots.txt 파일은 테크니컬 SEO의 기본이라 불립니다. 티스토리 블로그도 마찬가지로 이 개념을 이해하고 활용하면 검색 엔진 크롤러 친화적인 사이트가 됩니다.
사이트맵(sitemap.xml): 내 블로그의 글 목록(및 페이지 목록)을 모두 나열한 XML 파일입니다. 직접 작성하기 어렵다면 검색하면 사이트맵 자동 생성 도구도 많으니 활용하시면 돼요. 완성된 sitemap.xml 파일은 Search Console에 제출해서 구글에 알려줄 수 있습니다. 사이트맵을 제출해 두면 구글이 내 블로그의 새 글도 빠르게 발견할 수 있고, 혹시 내부 링크가 부족해 못 찾는 페이지도 색인될 수 있습니다.
위 예시는 사이트의 홈 페이지와 어떤 글 하나를 목록에 넣은 것입니다. <lastmod>는 마지막 수정 날짜, <priority>는 페이지 중요도를 표시하는데 필수는 아니라서 없어도 괜찮습니다. 티스토리의 경우 기본 제공되는 RSS 피드를 이용해 Search Console에서 사이트맵으로 등록할 수 있습니다. (티스토리 RSS가 글 목록을 제공하므로 사실상 사이트맵 역할을 일부 합니다.)
robots.txt: 사이트의 루트(최상위 경로)에 robots.txt라는 텍스트 파일을 두면 검색 로봇들이 먼저 이를 확인합니다. 이 파일 안에는 크롤링 허용/비허용 규칙과 사이트맵 위치 등을 기술할 수 있습니다 티스토리 공식 도메인은 기본적인 robots.txt가 자동으로 설정되어 있어 수정할 순 없지만, 개인 도메인을 사용하면서 직접 서버를 운영한다면 robots.txt를 직접 만들어야 합니다.
위 내용은 모든 크롤러(User-agent: *) 에게 /admin/ 경로는 크롤링하지 말라고 지시하고, 그 외는 모두 허용(Allow)하는 설정입니다. 마지막 줄에는 사이트맵 URL을 명시했습니다. 대다수의 경우 특별히 숨기고 싶은 페이지가 없다면 Disallow 지시자 없이 모두 허용하는 것이 좋습니다. 참고로 robots.txt를 잘못 설정해서 전체 사이트를 Disallow해버리면 검색 유입에 치명적이니, 항상 조심해서 다뤄주세요.
3-3) Core Web Vitals 지표 개선하기 (LCP, INP, CLS)
구글은 사용자 경험을 중시하기 때문에, 웹 성능 지표들도 검색 순위에 반영하고 있습니다. 특히 Core Web Vitals(코어 웹 바이탈)라는 세 가지 주요 지표를 정의하여 사이트 품질을 평가하고 있는데요, 각각 다음을 의미합니다.
LCP (Largest Contentful Paint): 최대 콘텐츠 표시 시간으로, 페이지 로드 시작 후 가장 큰 콘텐츠 요소(이미지 또는 큰 텍스트 블록 등)가 화면에 나타나는 데 걸리는 시간입니다. 쉽게 말해 본격적인 내용이 로딩되는 속도라고 볼 수 있어요. 2.5초 이내에 LCP가 발생하면 좋은 것으로 평가됩니다.
INP (Interaction to Next Paint): 다음 페인트까지의 상호작용으로, 사용자가 페이지에서 어떤 동작(클릭 등)을 했을 때 다음 화면이 그 동작에 반응하여 렌더링되기까지의 시간입니다. 이는 인터랙션 반응성을 측정하는 지표입니다. 0.2초(200ms) 미만이면 우수한 것으로 간주됩니다. (기존에는 FID(첫 입력 지연) 지표를 사용했지만, INP가 보다 포괄적인 지표로 새롭게 도입되었습니다.)
CLS (Cumulative Layout Shift): 누적 레이아웃 이동 점수로, 페이지 로딩 중 예상치 못한 레이아웃 변경이 얼마나 발생하는지를 수치화한 것입니다. 예를 들어 이미지가 늦게 로드되어 텍스트가 밀려난다거나, 광고 삽입으로 화면 요소들이 튀는 경우 CLS 점수가 높아집니다. 0.1 이하이면 안정적인 페이지로 평가합니다
이 세 가지 지표는 사용자에게 쾌적한 페이지 경험을 제공하는지를 나타내며, 구글 검색에서도 중요한 참고 신호로 사용되고 있습니다. 그렇다면, 각 지표를 어떻게 개선할 수 있을까요? 몇 가지 팁을 소개합니다:
LCP 개선: 가장 큰 요소가 빨리 나타나도록 사이트를 최적화하세요. 예를 들어 첫 화면에 보이는 이미지 용량을 줄이고, 필요한 경우 지연 로드(lazy-load)는 아래쪽 이미지에만 적용합니다. 또, 서버 응답속도를 높이거나 CDN을 이용해 콘텐츠 전송을 빠르게 하는 것도 LCP 향상에 도움이 됩니다. 한마디로 사용자가 첫 콘텐츠를 빨리 볼 수 있게 해주는 작업들입니다.
INP 개선: 자바스크립트 등의 대응 속도를 높이기 위해 노력합니다. 너무 무거운 스크립트는 비동기로 로드하거나 필요한 부분만 실행하고, 사용자가 상호작용할 때 메인 쓰레드를 오랫동안 블로킹하지 않도록 코드를 최적화합니다. 복잡한 작업은 Web Worker 등으로 분산하거나, 애니메이션 및 이벤트 핸들러를 효율적으로 작성하는 등의 방법이 있습니다. 결과적으로 사용자 조작에 즉각 반응하는 페이지를 만들자는 것이죠.
CLS 개선: 로딩 중 레이아웃이 튀지 않도록 미리 대비합니다. 예를 들어 이미지에는 width/height 속성을 지정하여 자리가 미리 확보되게 하고, 광고나 동적 콘텐츠가 삽입되는 공간도 최소한의 높이를 미리 예약해 둡니다. 글꼴이 로드되며 갑자기 텍스트가 커지는 경우도 있으니, 웹폰트 사용 시 FOIT/FOUT 현상도 신경 쓰는 것이 좋습니다. 핵심은 사용자 스크롤 중에 내용이 밀려서 불편하지 않게 만드는 것입니다.
위 개선 사항들은 모두 개발 단계에서 조금씩 신경 써야 하는 부분이지만, 초심자 분들도 이미지 크기 최적화나 불필요한 스크립트 최소화, 그리고 티스토리에서 제공하는 반응형 스킨 등을 활용하는 것으로도 충분히 시작할 수 있습니다.
3-4) HTTPS 보안 적용의 필요성
마지막 테크니컬 요소로 HTTPS에 대해 짚고 넘어가겠습니다. HTTPS는 HTTP에 보안 프로토콜(TLS)이 추가된 것으로, 사용자와 서버 사이의 통신을 암호화하여 도청이나 위변조를 방지합니다. 오늘날 웹 환경에서 HTTPS는 사실상 기본이며, SEO 측면에서도 매우 중요합니다.
구글은 이미 2014년에 HTTPS 사용 여부를 랭킹 신호로 활용하기 시작했다고 발표한 바 있습니다. 비록 영향력은 작다고 했지만 시간이 지날수록 중요도가 커지고 있고, 현재는 HTTPS가 기본이 된 사이트들이 검색에서 유리한 것은 확실합니다. 사용자의 신뢰도 측면에서도 주소창에 자물쇠 아이콘이 보이지 않는 사이트는 클릭을 꺼리기 때문에 방문자 유치에도 불리해요.
티스토리 자체는 https:// 프로토콜을 지원하므로, 개인 도메인을 연결한 경우에도 SSL 설정을 꼭 하셔야 합니다. (티스토리 관리자 설정 > 블로그에서 HTTPS 사용 여부를 확인할 수 있습니다. 또는 Cloudflare 같은 서비스를 이용해 개인 도메인에 무료 SSL을 적용할 수도 있습니다.) 사이트 전체가 HTTPS로 제공되지 않고 일부만 HTTP라면 혼합된 콘텐츠 경고가 뜨면서 사용자에게 불안감을 줄 수도 있으니, 블로그 자산(이미지 등)도 가급적 https 주소를 사용하세요.
요약하면, 속도/보안 등 기술적인 부분도 SEO의 일부입니다. 검색엔진은 양질의 콘텐츠와 함께 빠르고 안전한 사이트를 사용자에게 제공하고자 하므로, 우리 블로그도 이에 맞게 기술적인 토대를 다져두면 좋겠습니다.
3-5) 구조화 데이터(JSON-LD) 적용하기
앞서 구조화 데이터에 대해 간단히 설명드렸는데요, 이번에는 왜 구조화 데이터를 써야 하는지와 어떻게 적용하는지 조금 더 살펴보겠습니다.
구조화 데이터를 사용하는 이유
일반적인 웹페이지는 사람이 읽기 위한 HTML 콘텐츠로 작성되지만, 구조화 데이터는 검색엔진이 기계적으로 이해하기 쉽도록 추가로 제공하는 데이터입니다. 이 데이터를 추가하면 다음과 같은 이점이 있습니다:
콘텐츠 명확한 이해: 예를 들어 블로그 글에 구조화 데이터를 넣으면, 이 글의 제목, 저자, 발행일, 본문 요약 등이 어떤 것인지 명시적으로 표시할 수 있습니다. 검색 엔진은 이를 참고하여 해당 정보를 보다 정확하게 파악합니다.
리치 결과(Rich Results): 구조화된 데이터를 활용하면, 구글 검색 결과에 별점, 썸네일 이미지, 방문자 리뷰, 레시피 조리 시간, FAQ 항목 등 다양한 추가 정보를 노출시킬 수 있습니다. 이런 풍부한 결과는 사용자 눈길을 끌어 CTR(클릭률) 상승에 도움이 됩니다. (실제로 앞서 언급했듯이, 리치 결과가 일반 결과보다 클릭률이 82% 높다는 분석도 있습니다.)
음성 검색 및 AI 활용: 구조화 데이터로 콘텐츠를 잘 정리해 두면, 구글 어시스턴트 같은 음성 검색이나 향후 AI 검색 기능에서도 내 콘텐츠를 적절한 답변으로 활용할 확률이 높아집니다. 이는 미래를 대비한 포인트이긴 하지만 알아두면 좋겠죠.
그렇다면, 이렇게 좋은 구조화 데이터를 어떻게 추가할까요? 구글에서는 JSON-LD 형식을 사용할 것을 권장하고 있습니다. JSON-LD는 자바스크립트 객체 표기 형태로 데이터를 표현하기 때문에, 따로 페이지의 화면에 보이지 않고 <script> 태그 안에 정보를 넣어두기만 하면 됩니다.
구조화 데이터 간단 예제
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"applicationCategory": "EntertainmentApplication",
"name": "Simmey - 우리의 얼굴 매칭 AI를 사용하여 부모님과 얼마나 닮았는지 확인해보세요!",
"description": "우리의 얼굴 매칭 AI를 사용하여 부모님과 얼마나 닮았는지 확인해보세요! 재미있고 흥미로운 가족 닮은꼴 비교를 시작할 수 있는 메인 페이지입니다.",
"author": {
"@type": "Person",
"name": "lodado"
},
"keywords": "face matching, ai, fun, family",
"url": "https://mamapapa.vercel.app/",
"datePublished": "2025-05-25T08:11:55.646Z",
"isAccessibleForFree": true,
"publisher": {
"@type": "Organization",
"name": "lodado",
"logo": {
"@type": "ImageObject",
"url": "https://mamapapa.vercel.app/Logo.svg"
}
},
"operatingSystem": "ALL",
"browserRequirements": "A modern browser with JavaScript enabled",
"offers": {
"@type": "Offer",
"price": "0.00",
"priceCurrency": "USD"
}
}
</script>
위는 제가 실제로 사이드프로젝트에 적용한 JSON-LD 태그입니다.
Tip: 구조화 데이터를 추가한 후에는 구글 리치 결과 테스트 도구를 사용해서 올바르게 구현되었는지 검증해 보세요. 오류가 있다면 검색엔진에 반영되지 않을 수 있으니, 반드시 확인하는 습관을 들이는 것이 좋습니다.
3-6) 메타태그 -Open Graph 태그
twitter meta tag
이외에도 twitter, facebook등 여러 소셜 미디어에서 사용하는 메타 태그 - Open Graph 태그 (og:title, og:description, og:image 등) 를 추가하면 SEO 향상에 도움이 됩니다.
4) 오프페이지 SEO 개념과 실천 방법
지금까지는 내 블로그 내부에서 할 수 있는 SEO에 집중했다면, 이제 오프페이지 SEO에 대해 이야기해보겠습니다. 오프페이지 SEO란 내 사이트 외부의 요소들로 검색 순위에 영향을 주는 것들을 말합니다. 그중 가장 대표적인 것이 백링크(Backlink) 입니다.
오프페이지 SEO란?
간단히 말해, 다른 사이트로부터 내 사이트로 연결되는 링크와 외부에서의 평판을 관리하는 것입니다. 구글 같은 검색 엔진은 어떤 사이트에 좋은 콘텐츠가 많으면, 다른 사이트들도 그 사이트를 많이 링크한다고 판단합니다. 그래서 품질 좋은 백링크가 많을수록 신뢰도와 권위가 높다고 보고 랭킹을 올려주는 것이죠. 반대로 출처가 의심스러운 스팸 링크가 많으면 오히려 패널티를 받을 수도 있습니다.
거창한 링크 빌딩 전략을 펼치기는 어렵겠지만, 쉬운 방법 몇 가지를 실천해 볼 수 있습니다:
콘텐츠 공유: 내가 쓴 유용한 글이 있다면 SNS에 직접 공유하세요. 트위터, 페이스북, 링크드인, 개발자 커뮤니티 등 관련된 곳에 포스팅해서 사람들의 관심을 끌면 자연스럽게 방문자도 늘고, 누군가 내 글을 인용해갈 가능성도 생깁니다. 처음부터 큰 영향은 아니어도 노출 기회를 넓힌다는 측면에서 중요합니다.
커뮤니티 참여 및 링크: 개발자라면 Stack Overflow나 Hashnode, Reddit, 또는 국내 개발자 커뮤니티(예: OKky, Dev.to, Velog 등)에 활동하면서 관련 질문에 답변을 달거나 글을 쓰며 자연스럽게 내 블로그 글 링크를 남길 수도 있습니다. 단, 무턱대고 홍보 링크만 남기는 것은 스팸으로 보일 수 있으니 주의하세요. 정말 해당 질문에 도움이 되는 상세한 답변을 제공하면서 참고 링크로 내 글을 언급하는 식이 바람직합니다.
게스트 포스팅 & 협업: 기회가 된다면 다른 블로그에 게스트 포스트를 기고하고 내 블로그를 소개하거나, 지인들의 블로그와 교차로 링크 교환을 할 수도 있습니다. 예를 들어 다른 분의 블로그에 내가 쓴 글을 올리면서 "원문: 내 블로그 링크"를 남기는 방식입니다. 이는 어느 정도 컨텐츠 신뢰를 쌓은 후에 가능하겠지만, 하나의 방법이 될 수 있어요.
디렉토리 및 프로필 활용: 기술 블로그들의 모음 디렉토리에 내 블로그를 등록하거나(국내에 SEO 디렉토리는 흔치 않지만, 해외엔 alltop 같은 사이트들이 있음), 개발 관련 프로필(GitHub, LinkedIn)에 블로그 주소를 넣어두는 것도 링크 노출을 늘리는 소소한 팁입니다.
무엇보다 중요한 것은 고퀄리티 콘텐츠 생산입니다. 콘텐츠가 좋아야 사람들이 자발적으로 공유하고 링크해 주기 때문에, 이것이 가장 이상적인 오프페이지 SEO라 할 수 있습니다. 처음부터 백링크 숫자에 집착하기보다는 블로그에 유용한 글을 꾸준히 쌓고, 커뮤니티에서 신뢰를 얻는 것이 장기적으로 SEO에 큰 도움이 될 거예요.
이 고퀼리티 콘텐츠 생산을 어떻게 하냐고요? 가장 어려운 부분일꺼 같은데 운칠기삼이라고 봅니다. 제 사이드 프로젝트도 이유는 모르겠지만 러시아 검색엔진쪽에서 많이 유입되는거 같네요..
배포 전 SEO 체크리스트
마지막으로, 이제 홈페이지를 발행하기 전에 중요 요소들을 하나씩 체크해 봅시다. 아래 SEO 체크리스트를 통해 빠뜨린 부분은 없는지 확인해보세요:
제목 태그와 메타 설명 작성: 글의 주제를 명확히 담은 제목(title)을 정했고, 매력적인 메타 설명(description)을 추가하셨나요? (검색 결과에 표시될 문구입니다.)
URL 주소 확인: 티스토리에서 자동 생성된 제 글 URL(슬러그)이 의미있게 잘 설정되어 있나요? (가능하다면 영문으로 간단히 키워드를 포함하는 게 좋지만, 티스토리는 자동으로 생성됩니다. 예: https://myblog.tistory.com/15보다는 .../seo-guide 처럼 식별 가능하면 좋습니다.)
헤딩 구조 점검: 본문에 h1은 하나만 있고, h2/h3 등의 소제목이 논리적으로 계층을 잘 이루고 있나요? 혹시 글씨 크기를 키우려고 헤딩을 남용하지 않았는지 확인하세요.
키워드 최적화: 글 내용 중에 주요 키워드가 자연스럽게 포함되어 있나요? 제목, 첫 문단, 헤딩 등에 핵심 단어가 들어가면 좋습니다. 다만 키워드 과다 남용은 금물입니다. 독자를 위한 글쓰기가 우선이에요.
내부 링크 추가: 관련있는 다른 포스팅이 있다면 서로 링크를 연결해 두었나요? 새로운 글에서는 이전 관련 글을, 그리고 나중에라도 이전 글에 이 새로운 글의 링크를 추가하면 더욱 좋습니다.
이미지 대체 텍스트(Alt): 글에 이미지나 그림을 넣었다면 <img alt="..."> 속성에 이미지 설명을 넣었는지 확인합니다. 이는 시각장애인용 보조기기에도 필요하고 SEO에도 유용합니다.
모바일 환경 확인: 모바일에서 내 블로그가 읽기 편한지 확인해 보셨나요? 화면 크기에 따라 레이아웃이 깨지거나 글씨가 너무 작지 않은지 점검하세요. 모바일 친화적(모바일 프렌들리) 사이트는 검색 순위에도 유리합니다.
페이지 속도 확인: PageSpeed Insights 등으로 페이지 점수가 너무 낮게 나오지는 않았나요? 이미지 최적화나 필요없는 스크립트 지우기 등을 통해 속도를 개선해 보세요. LCP/CLS 지표가 나쁘게 나오면 해당 요소를 수정하는 것도 잊지 마세요.
HTTPS 적용 여부: 블로그 주소가 http://가 아닌 https://로 접속되고 있는지 확인합니다. 티스토리 기본 도메인은 HTTPS이지만, 개인 도메인은 SSL 설정을 해야 합니다. 브라우저 주소창에 자물쇠가 뜨는지 꼭 확인하세요.
구조화 데이터 추가: 가능하다면 JSON-LD 형태의 구조화 데이터 스크립트를 페이지에 넣었는지 확인합니다. (티스토리에서는 스킨 편집이나 개별 글 HTML 모드에서 추가 가능) 나중에라도 한 번 도전해보세요.
Search Console 색인 요청: 글을 발행했다면 Google Search Console에 가서 새 URL을 색인 요청 해보세요. 이렇게 하면 크롤러가 비교적 빨리 방문해서 색인해갈 확率이 높아집니다.
SNS 공유 준비: 마지막으로, 소셜 미디어에 공유할 준비가 되었나요? Open Graph 태그 (og:title, og:description, og:image 등)도 자동 설정되도록 스킨을 구성하면 좋습니다. 공유했을 때 미리보기 카드가 깔끔히 나오면 클릭률 상승에 도움이 되니까요.
내용이 다소 많았지만, 요약하자면 검색엔진이 내 블로그를 잘 찾아가도록 돕고, 방문자에게 좋은 경험을 주는 것이 SEO의 핵심입니다.
worker 적용한 cavas(좌측)와 미적용된 canvas(우측), 상단의 count 올라가는 속도가 차이가 난다.
웹 성능 최적화를 위해 브라우저의 메인 스레드(Main Thread) 부담을 줄이는 것은 매우 중요합니다. 이번 글에서는 웹 초보자도 이해하기 쉽게 Web Worker, OffscreenCanvas, Comlink를 활용해 메인 스레드 작업을 분산하고 성능을 향상시키는 방법을 소개합니다.
Web Worker란? 왜 메인 스레드 작업을 분산해야 하나?
Web Worker는 브라우저에서 메인 스레드와 별도로 동작하는 백그라운드 스레드입니다. 쉽게 말해 웹 페이지의 UI와 독립적으로 실행되는 자바스크립트 환경입니다. Web Worker를 사용하면 메인 스레드가 해야 할 무거운 작업을 워커로 위임할 수 있어요
이로써 메인 스레드는 보다 중요한 UI 렌더링이나 사용자 입력 처리 등에 집중하고, 무거운 연산으로 인한 프리즈(멈춤) 현상을 줄일 수 있습니다.
웹 성능이 저하되는 흔한 이유는 메인 스레드가 과부하되어 UI가 응답하지 않는 상태가 되는 것입니다. 예를 들어 복잡한 계산이나 큰 이미지 처리 등을 메인 스레드에서 하면, 그동안 버튼 클릭이나 화면 렌더링이 멈출 수 있죠. Web Worker로 이러한 작업을 옮기면 브라우저가 다중 스레드처럼 작업하여 UI를 매끄럽게 유지할 수 있습니다. 요약하면, Web Worker는 웹 앱에 멀티스레딩 효과를 주어 성능을 높이는 도구입니다.
** 성능상 우월한 방법이 아닙니다! 병렬 처리를 위할때 유용합니다.
OffscreenCanvas: 워커에서 캔버스를 그리기 위한 비밀병기
Canvas를 활용한 그래픽 연산은 웹에서 많이 사용되지만, 기존에는 이 <canvas> 요소가 메인 스레드의 DOM과 연결되어 있어 워커에서 직접 조작할 수 없었습니다. OffscreenCanvas(오프스크린 캔버스)는 이런 제약을 해결해 주는 기술입니다. OffscreenCanvas는 화면에 보이지 않는 캔버스를 의미하며, 캔버스를 DOM과 분리하여 화면 밖(off-screen)에서 렌더링할 수 있게 해줍니다. 덕분에 Web Worker 내부에서도 캔버스에 그림을 그릴 수 있죠.
오프스크린 캔버스를 쓰는 이유는 간단합니다. 메인 스레드의 부담을 줄이기 위해서입니다. 예를 들어 복잡한 애니메이션이나 물리 시뮬레이션을 Canvas로 구현한다면, 메인 스레드에서 매 프레임 그림을 그리느라 다른 작업을 못할 수 있습니다.
OffscreenCanvas를 사용하면 이러한 캔버스 그리기 연산을 워커로 넘겨 병렬 처리할 수 있습니다. 메인 스레드는 UI 업데이트나 이벤트 처리에 집중하고, 워커는 OffscreenCanvas에 그림을 그린 다음 그 결과만 메인 스레드로 보내 화면에 표시하는 식이죠. 이렇게 하면 캔버스 애니메이션도 부드럽게 돌리고, UI도 끊김 없이 반응하도록 만들 수 있습니다.
Comlink: Web Worker와의 통신을 쉽게 해주는 라이브러리
Web Worker를 직접 사용하다 보면, postMessage와 onmessage로 메시지를 주고받는 코드가 다소 번거롭습니다.
Comlink(컴링크)는 이를 간단하게 만들어주는 경량 라이브러리입니다. Comlink를 쓰면 마치 메인 스레드에서 워커 내부 함수나 변수를 직접 호출하는 것처럼 프로그래밍할 수 있어요.
웹 워커와 통신 (postMessage) 흐름도
내부적으로는 postMessage를 추상화하여 자동으로 메시지를 전달해주기 때문에, 개발자는 복잡한 메시지 핸들러 대신 평범한 함수 호출 형태로 워커와 소통할 수 있습니다
예를 들어 Comlink 없이라면:
메인 스레드에서 worker.postMessage(data)로 데이터를 보내고,
워커 내부에서 self.onmessage로 이벤트를 받아 처리한 뒤,
다시 self.postMessage(result)로 결과를 메인에 보내고,
메인에서는 worker.onmessage로 결과를 받는
일련의 과정을 코딩해야 합니다.
Comlink를 쓰면 Web Worker와의 통신을 프록시 객체로 감싸서(일종의 RPC), 개발자가 쉽게 비동기 함수 호출처럼 다루도록 도와주는 도구입니다.
예제로 보는 Web Worker + OffscreenCanvas 활용 (Matter.js 물리 시뮬레이션)
이제 간단한 예시로 위 개념들을 연결해보겠습니다. Matter.js라는 자바스크립트 2D 물리 엔진을 이용해 공 몇 개가 튕기는 물리 시뮬레이션을 만든다고 가정해봅시다. 이 시뮬레이션은 계산량이 많으니 Web Worker에서 실행하고, 메인 스레드에서는 현재 프레임 수나 객체 개수 등의 count 상태만 화면에 표시하도록 해볼게요.
Main Thread
const canvas = document.getElementById('simCanvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('physics-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// UI에서 1초마다 count 증가 표시 (워커와는 별개로 동작)
let count = 0;
setInterval(() => {
document.getElementById('counter').innerText = `Count: ${count++}`;
}, 1000);
메인 스레드(main.js): 메인 HTML에 <canvas id="simCanvas"></canvas>와 <div id="counter"></div>가 있다고 합시다. 메인에서는 워커를 생성하고 캔버스를 OffscreenCanvas로 넘깁니다.
위 코드에서 transferControlToOffscreen()은 DOM 캔버스를 OffscreenCanvas 객체로 변환하여 워커로 소유권 이전(transfer) 하는 역할을 합니다. 이렇게 하면 이후부터는 워커가 해당 캔버스에 직접 그림을 그릴 수 있어요. 또한 메인에서는 1초마다 단순 카운트 숫자를 올려서 #counter 영역에 표시합니다. 이 count 증가 로직은 메인 스레드에서 별도로 돌기 때문에, 물리 시뮬레이션이 돌아가더라도 UI 카운터가 멈추지 않고 계속 업데이트 됩니다.
Worker Thread
importScripts('matter.min.js'); // 워커에서 Matter.js 라이브러리 로드
let engine, render;
onmessage = (event) => {
const offscreenCanvas = event.data.canvas;
const ctx = offscreenCanvas.getContext('2d');
// Matter.js 엔진 초기화 (중력, 세계 설정 등)
engine = Matter.Engine.create();
// Matter.js에서는 render를 직접 사용하지 않고 OffscreenCanvas의 ctx로 그림
function update() {
Matter.Engine.update(engine, 16); // 물리 세계 한 스텝 진행 (approx 60fps)
drawScene(ctx, engine.world); // 사용자 정의: world의 객체들을 ctx로 그리기
requestAnimationFrame(update); // 다음 프레임 업데이트 예약
}
update(); // 시뮬레이션 시작
};
워커 스레드(physics-worker.js): 워커 쪽에서는 메인으로부터 메시지를 받아 OffscreenCanvas 객체를 얻습니다. 그리고 Matter.js 엔진을 초기화하고, 주기적으로 물리 시뮬레이션 업데이트 + 캔버스 렌더링을 합니다.
위 예시는 단순화를 위해 의사코드 형태로 나타냈지만, 핵심은 워커에서 주기적으로 Matter.js 엔진을 업데이트하고 OffscreenCanvas의 2D 컨텍스트에 결과를 그림입니다.
requestAnimationFrame도 워커 환경에서 사용할 수 있는데, 이 경우 OffscreenCanvas에 그리는 것이므로 메인 화면에 바로 반영됩니다. 메인 스레드는 이 과정에 관여하지 않으므로, 물리 계산과 캔버스 렌더링으로 인한 메인 스레드 블로킹이 발생하지 않습니다. 메인은 그저 앞서 설정한 setInterval로 UI 카운트만 올리고 있을 뿐이죠.
worker 적용한 cavas(좌측)와 미적용된 canvas(우측), 상단의 count 올라가는 속도가 차이가 난다.
메인에서의 결과: 실제로 실행해보면, <canvas>에는 워커가 그린 Matter.js 물리 시뮬레이션 (예: 공들이 튕기는 애니메이션)이 매끄럽게 표시되고, 동시에 <div id="counter">에는 1, 2, 3... 하는 카운트 숫자가 끊김 없이 증가하는 것을 볼 수 있습니다. 만약 워커를 사용하지 않고 모든 작업을 메인에서 했다면, 물리 연산이나 그리기로 인해 카운트 업데이트가 지연되거나 멈췄을 겁니다. 이처럼 워커+오프스크린 캔버스 구조는 무거운 연산을 백그라운드에서 처리하면서도 메인 UI를 부드럽게 유지시킵니다.
맺으며
정리하면, Web Worker는 무거운 작업을 메인 스레드에서 떼어내어 비동기로 처리할 수 있게 해주고, OffscreenCanvas는 그런 워커에서 그래픽을 그릴 수 있도록 도와주며, Comlink는 메인-워커 간 통신을 개발자 친화적으로 만들어 줍니다. 이 세 가지를 조합하면, 초기 단계의 웹 개발자도 비교적 쉽게 메인 UI의 성능을 최적화하는 구조를 설계할 수 있습니다.
3D에 있는 한 점을 2D 화면에 찍는 것을 투영이라고 합니다. 투영을 쉽게 이해하려면, 손전등과 그림자 비유를 떠올릴 수 있습니다. 벽을 화면이라고 생각하고, 3D 물체에 빛을 비춰 벽에 드리운 그림자가 바로 그 물체의 투영입니다. 이때 손전등을 물체에 바짝 가까이 대면 그림자가 물체 크기와 다르게 크게 또는 작게 일그러져 보이는데, 이것이 원근 투영과 비슷합니다. 반대로 태양빛처럼 아주 멀리서 평행하게 오는 빛을 생각해보면, 물체의 그림자는 물체의 크기와 동일한 비율로 찍히게 됩니다. 이처럼 빛이 평행하게 온다면 물체까지의 거리에 상관없이 그림자 크기가 변하지 않는데, 이것이 정투영의 원리입니다.
컴퓨터 그래픽스에서 정투영 투영은 실제로 아주 간단한 수학적 변환입니다. 3차원 좌표 (X, Y, Z)가 주어지면, 그대로 X, Y 좌표만 가져오고 Z 좌표는 버립니다. 이렇게 X와 Y를 사용해 평면에 찍으면 3D 점을 2D 화면에 옮길 수 있습니다. (다른 방법으로는 원근 투영이 있습니다)
예를 들어, 높이가 다른 두 사람이 있다고 해도 정투영으로 보면 둘 다 같은 키로 보이겠지만, 원근 투영으로 보면 가까이 선 사람은 크게, 멀리 선 사람은 작게 표현되는 차이가 생기는 것입니다.
정투영이란 무엇인가?
정투영(Orthographic Projection, 직교 투영이라고도 합니다)은 3D 공간에 있는 물체를 2D 화면에 그릴 때, 거리와 상관없이 똑같은 비율로 투영하는 방법입니다. 다시 말해, 가까이 있는 물체나 멀리 있는 물체나 크기 변화 없이 동일한 비율로 그려집니다
반대로 현실 세계에서 우리가 보는 모습이나 3D 게임의 일반 카메라 시점은 원근 투영(Perspective Projection)이라고 하는데, 이는 가까운 것은 크게, 먼 것은 작게 보이도록 그리는 방식입니다. 원근 투영 덕분에 우리는 깊이감과 거리감을 느낄 수 있지만, 치수를 정확하게 재거나 평면도면을 그릴 때는 오히려 불편할 수 있습니다.
정투영과 원근 투영 차이
위 그림은 원근 투영과 정투영의 차이를 간단히 보여줍니다. 왼쪽은 원근 투영으로 본 큐브(정육면체)이고, 오른쪽은 정투영으로 본 모습입니다. 왼쪽 그림에서는 앞쪽 면의 빨간 테두리가 크게 보이고, 뒤쪽 면의 파란 테두리는 멀어지면서 작아져 내부의 작은 사각형처럼 보입니다.
회색으로 그린 모서리 선들도 멀어지면서 서로 가까워져, 마치 선들이 한 점으로 모이는 듯한 원근감이 나타납니다. 반면 오른쪽 정투영 그림에서는 앞면(빨간색)과 뒷면(파란색)의 크기가 동일하게 그려져 있습니다. 모든 모서리 선이 서로 평행하게 표시되며, 거리에 따른 크기 왜곡이 없어서 뒷면이 앞면에 정확히 겹쳐 보입니다. 이처럼 정투영은 거리와 무관하게 실제 크기를 그대로 보여주기 때문에, 도면 작업이나 멀리 있는 객체까지 정확한 치수로 표현해야 하는 경우에 유용합니다.
이제 실제 코드에서 이 단계들이 어떻게 구현되는지 projectOrtho 함수를 통해 알아보겠습니다. 우선, 원리를 간단히 설명해보자면 아래와 같습니다.
피벗 이동: 3D 점을 기준점(pivot)이 중심이 되도록 좌표를 이동합니다. (예: 카메라가 보는 중심으로 좌표계 원점을 맞춤)
회전 변환: X축, Y축, Z축 순서로 3D 점을 회전시킵니다. (예: 장면을 위아래로 보기 위해 X축 회전, 옆으로 보기 위해 Y축 회전 등)
투영: 회전된 3D 좌표에서 Z 값을 버리고 X, Y만 남겨 2D 평면에 투영합니다. (정투영이라서 원근 왜곡 없음)
배율 조정: 3D 단위를 픽셀 크기에 맞게 확대하거나 축소합니다. (예: 1미터를 100픽셀로 보이게 스케일링)
오프셋 이동: 계산된 2D 좌표에 화면상의 위치 offset을 더해 캔버스 좌표로 변환합니다. (예: 캔버스 중심이나 좌상단 기준으로 옮김)
실제 코드에서 이 단계들이 어떻게 구현되는지 projectOrtho 함수를 통해 알아보겠습니다.
코드 분석: projectOrtho 함수 단계별 설명
이제 provided된 projectOrtho 함수의 내부를 살펴보며, 앞에서 설명한 정투영 변환의 각 단계를 코드로 확인해봅시다. 이 함수는 3D 좌표를 받아 정투영을 적용한 2D 화면 좌표를 계산해주는데, React + TypeScript로 Canvas에 그릴 때 사용할 핵심 로직입니다. 복잡해 보이지만, 우리가 방금 이해한 다섯 단계를 차례로 수행하고 있을 뿐입니다. 코드와 함께 하나씩 살펴볼까요
// 3D 점을 정투영하여 2D 화면 좌표로 변환하는 함수
function projectOrtho(
point: { x: number, y: number, z: number },
pivot: { x: number, y: number, z: number },
rotation: { x: number, y: number, z: number }, // 각도 값(라디안)
scale: number,
offset: { x: number, y: number }
) {
// 1. 피벗 기준으로 좌표 이동 (pivot을 원점으로 옮기기)
let x = point.x - pivot.x;
let y = point.y - pivot.y;
let z = point.z - pivot.z;
// 2. X축 회전: 위아래 방향 회전 (pitch)
const cosX = Math.cos(rotation.x);
const sinX = Math.sin(rotation.x);
let y1 = y * cosX - z * sinX;
let z1 = y * sinX + z * cosX;
y = y1;
z = z1;
// 3. Y축 회전: 좌우 방향 회전 (yaw)
const cosY = Math.cos(rotation.y);
const sinY = Math.sin(rotation.y);
let x2 = x * cosY + z * sinY;
let z2 = -x * sinY + z * cosY;
x = x2;
z = z2;
// 4. Z축 회전: 평면 회전 (roll)
const cosZ = Math.cos(rotation.z);
const sinZ = Math.sin(rotation.z);
let x3 = x * cosZ - y * sinZ;
let y3 = x * sinZ + y * cosZ;
x = x3;
y = y3;
// 이제 정투영이므로 z는 사용하지 않습니다 (Z값 버림)
// 5. 스케일 조정: 3D 단위를 화면 확대배율에 맞춤
x = x * scale;
y = y * scale;
// 6. 오프셋 적용: 캔버스 좌표계로 이동 (예: 캔버스 중심을 (0,0)→(offset.x, offset.y)로)
const screenX = x + offset.x;
const screenY = y + offset.y;
// 계산된 2D 화면 좌표 반환
return { x: screenX, y: screenY };
}
피벗 이동 전후: 3D 공간에 좌표축과 점이 있다고 생각해 봅니다. 피벗 이동을 하기 전에는 점이 원점에서 point.x, point.y, point.z만큼 떨어진 곳에 있습니다. 피벗을 원점으로 맞추고 나면, 이제 그 점은 새로운 좌표계에서 (x - pivot.x, y - pivot.y, z - pivot.z) 위치로 보이게 됩니다. 쉽게 말하면, 기준점을 가운데로 옮겼더니 점의 좌표가 바뀌었다고 이해하면 됩니다. 하지만 실제 공간에서 점의 물리적 위치는 변하지 않았고, 우리가 좌표만 옮겨서 바라보고 있는 것입니다.
축별 회전 효과: 3차원 회전 행렬을 이용합니다.
Rotation Transformation
X축, Y축, Z축으로 차례로 회전하면, 3D 점은 공간에서 이리저리 자리를 바꿉니다. X축으로 회전하면 점이 위아래로 이동하는 것처럼 보이고, Y축으로 회전하면 좌우로, Z축으로 회전하면 화면 평면상에서 회전합니다. 최종적으로 세 번의 회전을 모두 마치면, 처음의 3D 점은 우리가 정한 각도에서 바라본 위치로 옮겨져 있게 됩니다. 여러 회전이 한꺼번에 적용되었기 때문에, 점의 새 좌표 (x3, y3, z3)는 원래 좌표와 많이 달라졌겠지만, 이 좌표는 우리가 장면을 해당 각도로 본 경우에 그 점이 어디 있는지를 나타낸다고 생각하면 됩니다.
투영 및 스케일: 이제 3D 좌표의 z는 버리고 (x3, y3)만 남깁니다. 이 단계에서 이미 2D 투영이 이루어졌다고 볼 수 있습니다. 남은 (x3, y3)는 아직 수치적으로는 작은 값일 수 있는데, 스케일을 곱해서 화면에 보일 크기로 확대합니다. 만약 점들이 서로 가까이 모여 있었다면 스케일을 키워서 벌려주고, 너무 넓게 퍼져 있었다면 스케일을 줄여서 화면에 잘 들어오도록 할 수 있습니다.
오프셋 적용: 마지막으로 offset을 더해 캔버스 좌표로 변환하면, 이제 진짜 화면 픽셀 좌표가 나옵니다. 이 좌표를 사용해서 Canvas에 점을 찍으면, 우리가 3D에서 지정했던 그 점이 2D 화면상의 정확한 위치에 표시됩니다.
요약하면, projectOrtho 함수는 (피벗 이동) → (회전) → (투영) → (스케일) → (오프셋) 순서로 3D 좌표를 변환하여 2D 화면 좌표를 반환합니다.
이제 projectOrtho 함수를 이용해서 실제 HTML5 Canvas에 3D 점들을 찍어보는 간단한 React 컴포넌트를 만들어보겠습니다. React와 TypeScript를 사용하므로, 함수형 컴포넌트와 훅(hook)을 활용해볼게요. 예제에서는 정육면체 모서리 8개 점을 3D 좌표로 정의하고, 이를 정투영으로 화면에 표시해보겠습니다.