Devin.KR

프론트엔드 질문 - 렌더링·상태·성능·접근성

개발자KR 조회 1

이 장에서 배우는 것

중고 거래 서비스의 상품 상세 화면이 열렸다. 사진은 늦게 나타나고, 관심 버튼을 누르면 숫자가 잠깐 올랐다가 되돌아간다. 판매자 소개에는 사용자가 입력한 문장이 표시된다. 면접관은 이 화면 하나를 두고 렌더링, 상태, 비동기 처리, 보안을 차례로 묻는다. 질문의 중심은 특정 도구의 이름이 아니라, 화면에서 일어난 일을 원인과 결과로 설명할 수 있는가에 있다.

앞 장에서 요청과 응답을 서버의 관점에서 살펴보았다면, 여기서는 응답을 받은 브라우저가 화면을 만들고 사용자 입력에 반응하는 과정을 다룬다. Python 실험은 브라우저 전체를 구현하지 않는다. 실행 순서와 문자열의 해석 경계를 작게 재현해, 면접에서 한 설명을 눈으로 확인하는 데 목적이 있다.

  • 브라우저 렌더링 단계와 DOM 변경의 영향을 구분한다.
  • 이벤트 전파와 상태의 단방향 흐름을 사용자 동작에 연결한다.
  • 태스크와 마이크로태스크의 실행 순서를 예측하고 그 설명의 한계를 밝힌다.
  • 성능, 접근성, 보안을 기능의 동작 조건으로 설명한다.
  • 결론, 근거, 경험이나 예시, 한계와 대안의 순서로 답변한다.

문제 상황

가상의 중고 거래 서비스 팀에 지원한 신입 개발자가 기술 면접에서 자신이 만든 상품 상세 화면을 설명한다. 화면에는 상품 사진, 가격, 판매자 소개, 관심 버튼, 신고 대화상자가 있다. 면접관은 다음 상황을 제시한다.

사진이 로드되면서 가격과 버튼이 아래로 밀립니다. 관심 버튼을 빠르게 두 번 누르면 화면과 서버의 값이 달라집니다. 상품 목록에서는 관심 버튼을 눌러도 상세 화면으로 이동합니다. 키보드만 사용하면 신고 대화상자를 닫기 어렵습니다. 어디부터 확인하겠습니까?

이때 모든 문제를 “렌더링이 느려서 그렇다”라고 묶으면 진단이 흐려진다. 사진 때문에 요소가 밀리는 현상은 공간 예약과 레이아웃 문제다. 관심 수의 불일치는 상태 소유권과 응답 순서의 문제일 수 있다. 의도하지 않은 이동은 이벤트 처리와 마크업 구조를 살펴봐야 한다. 대화상자 문제는 초점 이동과 키보드 조작을 확인해야 한다.

좋은 출발점은 재현 조건을 나누는 것이다. 어떤 입력을 했는지, 기대한 화면은 무엇인지, 실제로 무엇이 달랐는지를 먼저 말한다. 도구 이름은 그다음이다. 경험이 없는 항목은 가상의 예시라고 밝히고, 실제로 구현한 범위와 앞으로 확인할 항목을 구분한다.

상품 상세 화면에서 이어지는 면접 질문과 평가 의도
질문의도답변 핵심꼬리 질문
브라우저는 HTML을 어떻게 화면으로 만듭니까?처리 단계와 비용을 구분하는가파싱, 스타일 계산, 레이아웃, 페인트, 합성색상과 너비를 바꾸면 차이가 있습니까?
DOM과 화면은 같은 것입니까?문서 구조와 표시 결과를 구별하는가DOM은 문서 객체 구조이며 변경 즉시 표시를 뜻하지 않는다숨겨진 요소도 DOM에 있습니까?
자식 버튼의 클릭이 부모까지 전달되는 이유는 무엇입니까?이벤트 처리 위치를 설명하는가캡처, 대상, 버블 단계와 이벤트 위임기본 동작 취소와 전파 중단은 어떻게 다릅니까?
관심 수는 어디에서 관리하겠습니까?상태 소유권을 정할 수 있는가중복 저장을 줄이고 필요한 공통 소유자를 정한다서버 요청이 실패하면 어떻게 합니까?
Promise 후속 처리와 타이머 중 무엇이 먼저 실행됩니까?비동기 실행 순서를 이해하는가동기 실행 뒤 마이크로태스크를 처리한 다음 다음 태스크로 진행한다마이크로태스크가 계속 추가되면 어떻게 됩니까?
상품 사진을 모두 지연 로딩하겠습니까?성능 기법의 적용 조건을 아는가첫 화면의 핵심 이미지와 화면 밖 이미지를 구분한다사진 때문에 화면이 밀리는 것은 어떻게 줄입니까?
캐시와 번들 분할은 무엇을 줄입니까?전송과 실행 비용을 구분하는가재전송과 초기 코드 양을 줄이되 갱신과 추가 요청 비용을 고려한다배포 뒤 이전 파일이 남으면 어떻게 합니까?
클릭되는 요소를 모두 일반 컨테이너로 만들어도 됩니까?키보드와 보조 기술 사용을 고려하는가동작은 버튼, 이동은 링크로 표현한다신고 대화상자의 초점은 어디로 이동합니까?
판매자 소개를 HTML로 넣으면 어떤 문제가 있습니까?신뢰 경계와 출력 위치를 이해하는가텍스트 출력과 허용된 HTML 처리를 구분해 XSS를 방어한다HTML 이스케이프만 하면 모든 위치가 안전합니까?
로그인 쿠키가 있으면 요청한 사람의 의도도 확인됩니까?인증과 요청 위조 방어를 구별하는가쿠키 자동 전송과 CSRF를 설명한다상태 변경을 GET으로 처리하면 왜 곤란합니까?

렌더링과 이벤트를 화면의 변화로 설명하기

브라우저는 HTML을 파싱해 문서 객체 모델(DOM)을 만들고, 스타일시트를 해석해 스타일 정보를 구성한다. 요소에 적용할 스타일을 계산한 뒤, 표시할 요소의 위치와 크기를 정하는 레이아웃을 수행한다. 페인트는 글자, 배경, 테두리 같은 시각적 내용을 그릴 정보를 만드는 과정이며, 합성은 준비된 레이어 등을 조합해 최종 화면을 구성하는 과정이다.

이 설명은 큰 흐름을 정리한 모델이다. 브라우저가 매번 문서 전체에 모든 단계를 수행하는 것은 아니다. 변경의 종류, 주변 요소와의 관계, 브라우저의 최적화에 따라 필요한 작업이 달라진다. 자바스크립트 실행과 자원 로딩도 이 과정에 영향을 준다.

문서와 스타일의 변경 종류에 따라 레이아웃이나 페인트부터 다시 작업할 수 있다

깊게 묻기: 스타일을 바꾸면 어떤 일이 생기는가

질문 상품 카드의 너비를 바꾸는 것과 글자 색을 바꾸는 것은 같은 비용입니까?

결론 먼저. 같은 비용이라고 단정할 수 없다. 너비 변경은 다른 요소의 위치와 크기까지 바꿀 수 있어 레이아웃을 다시 계산할 가능성이 있다. 글자 색 변경은 보통 기하 정보가 유지되므로 페인트와 관련된 작업이 중심이 된다.

근거. 렌더링 비용은 “DOM을 몇 번 바꿨는가”만으로 정해지지 않는다. 무엇을 바꿨고, 영향을 받는 범위가 어디까지인지가 중요하다. 일부 변환이나 불투명도 변경은 조건에 따라 합성 중심으로 처리될 수 있지만, 특정 속성이 언제나 같은 비용을 보장하는 것은 아니다.

예시. 중고 거래 목록에서 사진 크기를 미리 지정하지 않으면 이미지가 도착한 뒤 카드 높이가 바뀌어 아래 상품이 밀릴 수 있다. 이미지의 너비와 높이 또는 종횡비로 공간을 예약하면 이 변화를 줄일 수 있다.

한계와 대안. 눈으로 부드러워 보인다는 이유만으로 비용이 적다고 결론 내리지 않는다. 실제 기기에서 성능 기록을 보고 긴 작업, 레이아웃, 페인트에 걸린 시간을 확인한다. 변경을 모아서 처리하되, 최적화 전후에 같은 동작을 측정한다.

꼬리 질문 DOM을 수정한 직후 크기를 읽으면 어떤 문제가 있습니까?

대응 아직 계산되지 않은 최신 크기가 필요하면 브라우저가 스타일과 레이아웃을 동기적으로 계산할 수 있다. 반복문에서 쓰기와 크기 읽기를 번갈아 하면 계산을 반복하게 만들 수 있으므로, 가능한 읽기와 쓰기를 묶는다.

DOM은 화면의 픽셀 자체가 아니다. 표시되지 않는 요소도 DOM에 남아 있을 수 있으며, DOM 수정 뒤 화면 반영은 브라우저의 렌더링 기회에 이루어진다. 따라서 함수가 끝났다는 사실과 사용자가 바뀐 화면을 보았다는 사실을 구별해야 한다.

이벤트 전파는 캡처 단계에서 대상으로 내려가고, 대상 단계 뒤 버블 단계에서 조상으로 올라가는 흐름으로 설명한다. 다만 모든 이벤트가 버블링하는 것은 아니다. 목록의 부모에 처리기를 두고 실제 대상과 그 조상을 확인하는 이벤트 위임을 쓰면, 나중에 추가된 상품 카드도 같은 처리기로 다룰 수 있다.

이때 target은 이벤트가 발생한 대상을, currentTarget은 현재 처리기가 등록된 요소를 가리킨다. 버튼 안의 아이콘을 눌렀다면 target은 아이콘일 수 있다. 위임 처리에서는 가장 가까운 대상 버튼을 찾고, 그 버튼이 처리할 목록 안에 있는지도 확인한다.

기본 동작 취소인 preventDefault는 링크 이동 같은 기본 동작을 막는다. 전파 중단인 stopPropagation은 이후 전파를 막는다. 둘은 대체 관계가 아니다. 상품 카드 전체에 이동 처리를 붙이고 그 안에 관심 버튼을 두었다면, 무조건 전파를 중단하기 전에 링크와 버튼을 별도 조작 요소로 구성할 수 있는지 살펴보는 편이 좋다.

상태와 비동기 실행에 시간의 순서 붙이기

상태는 시간이 지나며 달라지고 화면의 결과에 영향을 주는 값이다. 상품 정보, 현재 사용자의 관심 여부, 요청 진행 여부가 예다. 서버에서 받은 데이터, 화면 내부의 임시 값, 다른 값으로 계산할 수 있는 결과를 구분하면 중복 저장을 줄일 수 있다. 가격과 수량이 있다면 합계는 대개 계산할 수 있으므로 별도 상태로 저장할 이유부터 확인한다.

깊게 묻기: 단방향 데이터 흐름과 상태 소유권

질문 상품 카드와 상세 화면에서 같은 관심 여부를 보여 준다면 상태를 어떻게 관리하겠습니까?

결론 먼저. 같은 상품의 관심 여부를 서로 다른 곳에서 독립적으로 수정하지 않도록, 일관성을 책임지는 소유자를 정하겠다. 단방향 데이터 흐름은 상태에서 화면으로 값을 전달하고, 사용자 동작은 변경 요청으로 소유자에게 전달하는 방식이다.

근거. 자식 화면이 전달받은 값을 임의로 바꾸거나 각 화면이 복사본을 따로 관리하면 갱신 누락이 생긴다. 함께 바뀌어야 하는 값은 공통 소유자로 모으고, 해당 화면에만 필요한 입력값은 가까운 곳에 둔다. 모든 값을 전역 저장소로 옮길 필요는 없다.

예시. 관심 버튼을 누르면 상품 식별자와 원하는 상태를 전달한다. 서버 응답을 기다린 뒤 확정하거나, 먼저 화면을 바꾸는 낙관적 갱신을 적용할 수 있다. 후자를 선택하면 이전 값, 요청 식별자, 실패 시 복구 규칙도 함께 설계한다.

한계와 대안. 이전 값을 저장했다고 끝나지는 않는다. 두 요청이 겹친 상태에서 오래된 실패가 최신 성공을 덮을 수 있다. 작은 화면이라면 요청 중 버튼을 비활성화해 겹침을 줄일 수 있다. 연속 조작을 허용해야 한다면 최신 요청 구분, 요청 직렬화, 서버 값 재조회 같은 방법을 요구사항에 맞게 선택한다.

꼬리 질문 관심 수가 음수가 되지 않게 화면에서 막으면 충분합니까?

대응 화면 검증은 사용자 경험을 돕지만 최종 데이터의 유효성은 서버에서도 보장해야 한다. 화면은 서버가 확정한 값으로 맞추고, 요청 실패와 재시도 중 상태를 사용자에게 알린다.

비동기 처리는 결과를 기다리는 동안 현재 실행을 계속할 수 있게 한다. Promise 생성자에 전달한 실행 함수는 동기적으로 호출된다. 반면 이미 이행된 Promise에 등록한 then의 반응 함수도 현재 실행 중간에 끼어들지는 않는다. 그 후속 처리는 마이크로태스크로 예약된다.

브라우저의 이벤트 루프(event loop)는 태스크 실행과 마이크로태스크 처리, 렌더링 기회 등을 조율한다. 타이머 콜백은 태스크로 실행되며, 지정한 지연 시간은 정확한 실행 시각을 보장하지 않는다. 현재 실행이 끝나고 마이크로태스크 체크포인트에 도달하면 대기 중인 마이크로태스크를 처리한다. 처리 중 추가된 마이크로태스크도 큐가 빌 때까지 이어서 처리한다.

태스크가 끝난 뒤 추가된 항목까지 마이크로태스크를 비우고 다음 태스크를 실행한다

깊게 묻기: Promise와 타이머의 순서

질문 동기 코드, 이행된 Promise의 후속 처리, 지연 시간이 0인 타이머가 있으면 무엇부터 실행됩니까?

결론 먼저. 한 번의 스크립트 실행에서 이들을 예약했다면 동기 코드가 먼저 끝나고, 준비된 Promise의 후속 처리를 마이크로태스크로 실행한 뒤 타이머 태스크를 실행한다.

근거. 지연 시간이 0이라는 것은 현재 코드를 중단하라는 뜻이 아니다. 마이크로태스크는 다음 태스크로 넘어가기 전에 처리된다. 다만 아직 이행되지 않은 Promise라면 후속 처리가 언제 준비되는지도 고려해야 하므로 “Promise는 언제나 타이머보다 먼저”라고 일반화하지 않는다.

예시. 관심 수를 동기 코드에서 바꾸고, 이미 준비된 Promise의 후속 처리에서 상태 메시지를 바꾼 뒤, 타이머에서 기록을 남겼다면 기록 순서는 예측할 수 있다. 그러나 이 세 단계 사이마다 사용자가 화면을 봤다고 결론 내릴 수는 없다.

한계와 대안. 마이크로태스크가 계속 자신을 추가하면 다음 태스크와 렌더링 기회가 지연될 수 있다. 큰 계산을 Promise 체인으로 감쌌다고 계산 비용이 사라지거나 다른 스레드로 옮겨지는 것은 아니다. 작업을 나눠 실행 기회를 양보하거나, 성격에 따라 별도 작업자 사용을 검토한다.

꼬리 질문 아래 Python 실험이 브라우저의 화면 갱신 시점도 알려 줍니까?

대응 아니다. 실험은 태스크마다 마이크로태스크 큐를 비운다는 규칙과 큐의 삽입 순서만 재현한다. 브라우저의 렌더링, 여러 태스크 소스, 실제 타이머 시간과 입출력은 구현하지 않는다.

성능·접근성·보안을 기능의 조건으로 보기

성능 답변은 “캐시를 쓴다”보다 어떤 비용을 줄이는지부터 말해야 한다. 지연 로딩은 당장 필요하지 않은 자원의 다운로드나 실행을 늦춘다. 상품 목록 아래쪽 사진에는 유용하지만, 첫 화면의 대표 사진까지 늦추면 핵심 콘텐츠의 표시가 지연될 수 있다. 이미지 공간 예약은 지연 로딩 여부와 별도로 필요하다.

번들 크기를 줄이면 전송량뿐 아니라 파싱과 실행 부담도 줄일 수 있다. 사용하지 않는 의존성을 제거하고, 특정 화면에서만 필요한 기능을 분리하는 방법이 있다. 너무 잘게 나누면 요청과 로딩 상태 관리 비용이 늘 수 있으므로 초기 화면에 필요한 코드부터 확인한다.

캐시는 재사용 가능한 응답이나 자원을 저장해 반복 작업을 줄인다. 내용이 바뀌면 이름도 바뀌는 정적 자원은 오래 재사용하기 좋다. 문서는 새 자원 이름을 안내할 수 있도록 갱신 정책을 맞춰야 한다. 사용자별 정보는 공유 캐시에 섞이지 않게 하고, 상품 가격처럼 바뀌는 데이터는 허용 가능한 오래됨과 재검증 조건을 정한다.

접근성은 마우스 사용 여부와 관계없이 기능을 조작하고 결과를 이해할 수 있게 만드는 조건이다. 관심 변경은 버튼, 상세 화면 이동은 링크로 표현한다. 아이콘만 있는 버튼에도 목적을 알 수 있는 접근 가능한 이름을 제공한다. 입력 요소에는 연결된 이름표가 필요하며, 오류는 색상만으로 구분하지 않는다.

신고 대화상자를 열면 초점을 적절한 내부 요소로 옮기고, 열린 동안 키보드 초점이 배경으로 빠지지 않도록 관리한다. 닫은 뒤에는 열었던 버튼 등 논리적인 위치로 돌려보낸다. 키보드로 열기, 이동, 취소, 제출을 확인하고 보조 기술이 이름과 상태를 인식하는지도 점검한다. 시맨틱 HTML은 좋은 출발점이지만, 사용자 정의 상호작용의 초점 관리까지 대신하지는 않는다.

깊게 묻기: 사용자 입력을 안전하게 표시하기

질문 판매자 소개에 태그처럼 생긴 문자열이 들어오면 어떻게 표시하겠습니까?

결론 먼저. 소개가 일반 텍스트라면 텍스트로 삽입하겠다. 문자열을 HTML로 해석하는 위치에 그대로 넣으면 교차 사이트 스크립팅(XSS)으로 이어질 수 있다.

근거. 중요한 경계는 입력 문자열이 데이터로 남는가, 브라우저가 해석할 문법이 되는가다. 텍스트 삽입 기능을 쓰거나 출력 문맥에 맞게 이스케이프하면 문자 자체를 표시할 수 있다. 아래 실험은 HTML의 일반 텍스트 위치에 들어갈 문자열을 변환한다.

예시. 판매자 소개가 <strong>예약 가능</strong>이라도, 일반 텍스트 기능이라면 태그까지 그대로 보여 준다. 서식 있는 소개가 요구된다면 허용할 태그와 속성을 정하고 검증된 정화 도구를 적용하는 별도 설계가 필요하다.

한계와 대안. HTML 이스케이프는 모든 문맥의 공통 해법이 아니다. URL에는 허용할 스킴 검사가 필요하고, 자바스크립트나 스타일 문맥에 사용자 문자열을 조립하는 방식은 피해야 한다. 콘텐츠 보안 정책은 추가 방어로 사용할 수 있지만 안전한 출력 처리를 대신하지 않는다.

꼬리 질문 CSRF도 같은 방식으로 막습니까?

대응 교차 사이트 요청 위조(CSRF)는 브라우저가 인증 쿠키 등을 자동으로 보내는 조건에서 사용자 의도와 다른 요청이 수행되는 문제다. 상태 변경 요청의 토큰 검증, 출처 확인, 적절한 SameSite 쿠키 정책 등을 함께 검토한다. HTML 이스케이프와 목적이 다르며, 상태 변경을 GET으로 처리하지 않는 것도 기본 조건이다.

쿠키의 HttpOnly 설정은 스크립트가 쿠키 값을 직접 읽는 것을 제한하지만, XSS로 실행된 코드가 사용자의 권한으로 요청하는 것까지 막지는 않는다. 또한 교차 출처 응답 읽기를 제한하는 설정만으로 CSRF가 해결된다고 답해서는 안 된다. 공격 요청이 전송되고 상태가 바뀌는 문제와 응답을 읽는 문제는 구분해야 한다.

같은 질문에서 답변의 근거와 한계가 드러나는 정도
질문나쁜 답보통 답좋은 답
너비 변경과 색상 변경의 차이는?둘 다 다시 그리니 같습니다.너비는 레이아웃, 색상은 페인트에 영향을 줍니다.영향 단계와 범위를 설명하고 이미지 공간 예약 사례와 실제 측정 계획을 덧붙인다.
관심 상태는 어디에 두는가?전역 저장소에 모두 넣습니다.공통 부모에서 관리합니다.공유 범위로 소유자를 정하고 응답 역전, 실패 복구, 서버 값과의 동기화를 설명한다.
Promise와 타이머 중 먼저인 것은?0초 타이머가 바로 실행됩니다.Promise가 먼저입니다.현재 실행과 이행 여부를 전제로 순서를 설명하고 렌더링 시점까지 보장하지 않는다고 밝힌다.
사용자 소개를 어떻게 표시하는가?입력에서 특정 태그만 지웁니다.HTML 이스케이프를 합니다.일반 텍스트 삽입을 우선하고 출력 문맥, 서식 허용 여부, 추가 방어의 한계를 구분한다.

면접관이 “그 방법을 실제로 적용했습니까?”라고 물으면 경험의 범위를 정확히 말한다. 구현하지 않은 대책을 이미 적용한 것처럼 말할 필요는 없다. “프로젝트에서는 요청 중 버튼을 막았고, 연속 입력을 허용하는 경우의 응답 역전은 아직 구현하지 않았다”라는 답도 판단 근거가 분명하다.

완성 코드

다음 프로그램을 main.py로 저장한다. Python 3.12 이상에서 표준 라이브러리만 사용한다. 실제 시간, 난수, 네트워크를 사용하지 않아 출력이 일정하다. 콜백의 반환값은 사용하지 않으며, 각 콜백이 메시지를 기록하거나 다른 콜백을 예약한다.

from collections import deque
from collections.abc import Callable
from html import escape

Callback = Callable[[], None]


class EventLoop:
    def __init__(self) -> None:
        self.tasks: deque[tuple[str, Callback]] = deque()
        self.microtasks: deque[tuple[str, Callback]] = deque()

    def add_task(self, name: str, callback: Callback) -> None:
        self.tasks.append((name, callback))

    def add_microtask(self, name: str, callback: Callback) -> None:
        self.microtasks.append((name, callback))

    def run(self) -> None:
        while self.tasks:
            name, callback = self.tasks.popleft()
            print(f"[태스크] {name}")
            callback()
            while self.microtasks:
                name, callback = self.microtasks.popleft()
                print(f"[마이크로태스크] {name}")
                callback()


def main() -> None:
    loop = EventLoop()

    def nested_microtask() -> None:
        print("중첩 후속 처리")

    def promise_reaction() -> None:
        print("관심 상태 후속 처리")
        loop.add_microtask("중첩 Promise", nested_microtask)

    def timer_microtask() -> None:
        print("타이머 A의 후속 처리")

    def timer_a() -> None:
        print("타이머 A 실행")
        loop.add_microtask("타이머 A 후속", timer_microtask)

    def timer_b() -> None:
        print("타이머 B 실행")

    def initial_script() -> None:
        print("동기 시작")
        loop.add_task("타이머 A", timer_a)
        loop.add_microtask("Promise 후속", promise_reaction)
        loop.add_task("타이머 B", timer_b)
        print("동기 끝")

    print("== 실행 순서 실험 ==")
    loop.add_task("초기 스크립트", initial_script)
    loop.run()

    print()
    print("== HTML 이스케이프 실험 ==")
    raw = '<strong>예약 가능</strong> & "직거래"'
    safe = escape(raw, quote=True)
    print(f"원문: {raw}")
    print(f"이스케이프: {safe}")
    print(f"원문 삽입: <p>{raw}</p>")
    print(f"변환 후 삽입: <p>{safe}</p>")


if __name__ == "__main__":
    main()

이스케이프 실험의 원문은 태그 해석의 차이를 보기 위한 서식 예시다. 실제 공격 동작을 실행하지 않는다. 터미널은 HTML을 렌더링하지 않으므로 두 문자열을 문자로 출력한다. 브라우저의 HTML 일반 텍스트 위치에서 파싱될 때 어떤 문법이 되는지 비교하는 실험이다.

줄별 해설

빈 줄을 포함해 첫 import를 1행으로 센다. 코드에는 실제 Promise 객체가 없다. Promise 후속 처리라는 이름의 콜백을 마이크로태스크 큐에 넣어 실행 순서만 흉내 낸다.

main.py의 행과 실험에서 맡는 역할
행내용확인할 원리
1~5큐, 호출 가능한 객체의 타입, 이스케이프 함수를 가져온다.큐의 앞에서 꺼내는 연산으로 예약 순서를 표현한다.
8~11태스크와 마이크로태스크를 별도 큐로 둔다.등록 시각만으로 두 종류를 한 줄에 섞지 않는다.
13~17이름과 콜백을 큐 끝에 추가한다.등록은 실행과 다르다.
19~23태스크 하나를 꺼내 이름을 출력하고 실행한다.실행 중인 콜백은 끝날 때까지 진행한다.
24~27마이크로태스크 큐가 빌 때까지 실행한다.처리 중 추가된 마이크로태스크도 같은 반복에서 실행한다.
30~38첫 후속 처리가 중첩 후속 처리를 예약한다.중첩 항목이 다음 타이머보다 먼저 실행되는지 확인한다.
40~48두 타이머와 첫 타이머의 후속 처리를 정의한다.타이머 사이에도 마이크로태스크 처리가 들어간다.
50~55동기 출력 사이에서 세 작업을 예약한다.예약한 콜백은 동기 끝 출력 뒤에 실행된다.
57~59초기 스크립트를 첫 태스크로 넣고 실행한다.초기 스크립트도 실험의 태스크 하나로 취급한다.
61~68원문과 변환 결과, 삽입 문자열을 출력한다.데이터가 HTML 문법으로 해석되는 경계를 비교한다.
71~72직접 실행한 경우 main을 호출한다.정해진 실험을 한 번 실행하고 종료한다.

24행의 반복문이 실험의 핵심이다. 처음 들어 있던 항목 수만큼 실행하는 방식이었다면 처리 중 추가된 중첩 항목이 남았을 것이다. 여기서는 매번 큐가 비었는지 검사하므로 새 항목까지 처리한다. 반대로 이 반복문을 모든 태스크가 끝난 뒤로 옮기면 브라우저의 해당 순서 규칙을 재현하지 못한다.

64행의 escape는 앰퍼샌드, 꺾쇠괄호와 따옴표를 HTML 문자 참조로 바꾼다. 이 결과를 HTML의 일반 텍스트 위치에 한 번 삽입하면 원문의 문자가 표시된다. 문자 참조에서 복원된 꺾쇠괄호가 같은 파싱 과정에서 다시 태그로 해석되는 것은 아니다. 변환한 문자열을 다른 API로 다시 해석하는 처리까지 안전하다는 뜻은 아니다.

실행 결과

main.py를 저장한 디렉터리에서 실행한다.

python3 main.py

예상 출력은 다음과 같다.

== 실행 순서 실험 ==
[태스크] 초기 스크립트
동기 시작
동기 끝
[마이크로태스크] Promise 후속
관심 상태 후속 처리
[마이크로태스크] 중첩 Promise
중첩 후속 처리
[태스크] 타이머 A
타이머 A 실행
[마이크로태스크] 타이머 A 후속
타이머 A의 후속 처리
[태스크] 타이머 B
타이머 B 실행

== HTML 이스케이프 실험 ==
원문: <strong>예약 가능</strong> & "직거래"
이스케이프: &lt;strong&gt;예약 가능&lt;/strong&gt; &amp; &quot;직거래&quot;
원문 삽입: <p><strong>예약 가능</strong> & "직거래"</p>
변환 후 삽입: <p>&lt;strong&gt;예약 가능&lt;/strong&gt; &amp; &quot;직거래&quot;</p>

타이머 A는 Promise 후속 처리보다 먼저 등록되었지만 나중에 실행된다. 서로 다른 큐 사이에서는 단순한 등록 순서만으로 실행 순서를 정할 수 없기 때문이다. 타이머 A에서 예약한 후속 처리도 타이머 B보다 먼저 실행된다.

실제 브라우저에는 여러 태스크 소스와 스케줄링 규칙이 있다. 여기서 두 타이머의 순서를 보장하는 것은 실험이 정의한 하나의 선입선출 큐다. 이 결과를 서로 다른 종류의 브라우저 작업 전체에 그대로 확대하면 안 된다.

실무에서 자주 틀리는 것

마이크로태스크를 마지막에 한 번만 처리한다

다음은 실험의 규칙을 잘못 구현한 부분 코드다. 모든 태스크를 먼저 실행하므로 타이머 A의 후속 처리가 타이머 B 뒤로 밀린다.

while tasks:
    tasks.popleft()()
while microtasks:
    microtasks.popleft()()

고친 코드는 태스크 하나가 끝날 때마다 마이크로태스크 큐를 비운다. 이 예시의 큐에는 이름 없이 콜백만 들어 있다고 가정한다.

while tasks:
    tasks.popleft()()
    while microtasks:
        microtasks.popleft()()

동기 계산을 마이크로태스크에 옮기는 것만으로 반응성이 좋아진다고 생각하는 실수도 피해야 한다. 큐를 비우는 동안 다음 태스크는 기다린다.

사용자 문자열을 HTML 문법으로 결합한다

다음 코드는 판매자 소개가 일반 텍스트라는 요구를 지키지 못한다. 저장된 문자열이 태그를 포함하면 브라우저가 문법으로 해석할 수 있다.

seller_intro = '<strong>예약 가능</strong>'
markup = f"<p>{seller_intro}</p>"

고친 코드는 HTML 일반 텍스트 위치에 맞게 변환한다. 브라우저에서 DOM을 직접 조작한다면 문자열로 HTML을 조립하는 대신 텍스트 삽입 기능을 우선 선택할 수 있다.

from html import escape

seller_intro = '<strong>예약 가능</strong>'
markup = f"<p>{escape(seller_intro, quote=True)}</p>"

저장 시 무조건 변환하고 출력 시 다시 변환하면 문자 참조 자체가 보이는 이중 변환 문제가 생길 수 있다. 원본 데이터와 출력 문맥을 구분하고 변환 책임을 일관되게 둔다.

파생 값을 별도 상태로 저장한다

다음 Python 부분 코드는 상태 중복을 재현한다. 수량을 바꾼 뒤 total은 이전 값을 유지한다.

price = 12000
quantity = 1
total = price * quantity
quantity = 2

고친 코드는 필요한 시점에 계산한다. 계산 비용이 크다면 캐시를 검토할 수 있지만, 입력이 바뀔 때 갱신하는 규칙도 필요하다.

price = 12000
quantity = 1


def total(price: int, quantity: int) -> int:
    return price * quantity


quantity = 2
display_total = total(price, quantity)

이 예시는 화면 프레임워크의 갱신 기능을 구현한 것이 아니다. 같은 사실을 여러 변수에 저장하면 동기화 책임이 늘어난다는 점을 보여 준다.

클릭 모양만 만들고 조작 의미를 빠뜨린다

다음 HTML은 관심 동작을 표현하기에 부족하다. 기본 키보드 조작과 버튼 의미가 제공되지 않는다.

<p>관심 등록</p>

고친 코드는 버튼의 기본 의미와 조작 방식을 사용한다. 실제 클릭 처리는 별도로 연결하되, 토글 상태도 함께 갱신한다.

<button type="button" aria-pressed="false">
  관심 등록
</button>

접근성 속성만 바꾸고 실제 동작을 구현하지 않아도 문제다. 화면의 관심 상태, 버튼의 눌림 상태, 서버가 확정한 결과가 같은 사실을 전달해야 한다.

한눈에 보기

프론트엔드 답변에서 연결해야 할 원인과 확인 방법
주제핵심 판단확인 방법답변의 경계
렌더링변경이 기하 정보에 영향을 주는가화면 이동과 성능 기록 확인속성 이름만으로 비용을 확정하지 않는다
이벤트어디서 발생하고 어디서 처리되는가대상과 현재 처리 요소 확인기본 동작 취소와 전파 중단을 구별한다
상태누가 값을 소유하고 변경하는가연속 클릭, 실패, 응답 역전 재현전역화만으로 일관성이 생기지 않는다
비동기어떤 큐에 언제 준비되는가동기 출력과 후속 처리 순서 비교실행 순서가 매번의 화면 표시를 보장하지 않는다
성능전송, 실행, 배치 중 무엇이 비싼가첫 방문과 재방문을 나눠 측정지연 로딩과 분할에도 비용이 있다
접근성이름, 역할, 상태와 초점이 전달되는가키보드와 보조 기술로 조작속성 추가만으로 동작이 완성되지는 않는다
보안문자열이 어디에서 해석되는가출력 위치와 상태 변경 요청 점검XSS와 CSRF의 방어 목적이 다르다

면접 답변의 분량은 결론과 조건을 먼저 제시한 뒤 조절한다. 면접관이 근거를 물으면 처리 단계를 풀고, 경험을 물으면 자신이 관찰한 현상과 선택을 말한다. 도구나 용어를 많이 나열하기보다, 실패 조건 하나를 정확히 설명하는 편이 판단 과정을 잘 드러낸다.

연습 문제

  1. 완성 코드의 promise_reaction에서 중첩 Promise를 예약한 직후 “추가 후속”이라는 마이크로태스크를 하나 더 예약하라. 이 콜백은 “추가 처리”를 출력한다. 타이머 A 전에 어떤 순서로 실행되는지 예측하고 확인하라.
  2. 타이머 A가 자신의 마이크로태스크를 예약한 다음 새 태스크 “타이머 C”를 예약한다고 가정하라. 타이머 B와 C, 타이머 A의 후속 처리를 순서대로 적고 이유를 설명하라.
  3. 첫 화면의 대표 사진까지 지연 로딩한 상품 화면에서 사진이 늦게 뜨고 아래 버튼도 밀린다. 두 현상을 구분해 개선안을 말하고, 개선 여부를 어떻게 확인할지 적어라.
  4. “판매자 소개는 HTML 이스케이프했고 인증 쿠키는 HttpOnly이므로 XSS와 CSRF 대응이 끝났다”라는 답을 고쳐라. 출력 위치, 쿠키의 역할, 상태 변경 요청을 포함해 네 문장 안으로 답하라.

정답과 해설

  1. 동기 끝 이후 Promise 후속이 실행되고 “관심 상태 후속 처리”가 출력된다. 이어 중첩 Promise의 “중첩 후속 처리”, 추가 후속의 “추가 처리” 순서가 된다. 두 항목은 같은 마이크로태스크 큐의 끝에 등록 순서대로 들어가며, 큐가 빌 때까지 처리되므로 둘 다 타이머 A보다 먼저 실행된다.

  2. 타이머 A의 후속 처리, 타이머 B, 타이머 C 순서다. 타이머 A가 끝나면 마이크로태스크 큐부터 비운다. 태스크 큐에는 B가 이미 들어 있으므로, A가 나중에 추가한 C는 B 뒤에 놓인다. 이 결론은 실험에서 사용한 단일 태스크 큐 규칙에 따른 것이다.

  3. 대표 사진의 늦은 표시는 필요한 자원을 너무 늦게 요청하는지 확인하고, 첫 화면의 핵심 이미지에는 지연 로딩을 적용하지 않는 방안을 검토한다. 버튼 이동은 이미지의 너비와 높이 또는 종횡비로 공간을 예약해 줄인다. 같은 화면 크기와 비슷한 네트워크 조건에서 캐시가 없는 방문과 있는 방문을 나누어, 핵심 이미지 표시 시점과 레이아웃 이동을 비교한다. 지연 로딩을 제거하는 것만으로 공간 예약 문제가 해결되는 것은 아니다.

  4. 일반 텍스트 소개는 텍스트로 삽입하거나 HTML 출력 위치에 맞게 이스케이프한다. URL 등 다른 문맥은 별도 검증이 필요하며 HttpOnly는 스크립트의 쿠키 직접 읽기를 제한할 뿐 XSS 전체를 막지 않는다. 인증 쿠키가 자동으로 전송되는 상태 변경 요청에는 CSRF 토큰과 출처 검증, 적절한 SameSite 정책을 검토한다. 상태 변경은 GET으로 처리하지 않고 서버에서 요청의 유효성을 검증한다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.