현재 한 요소를 다른 요소에 연결하는 방법은 무엇인가요? 위치를 추적하거나 래퍼 요소를 사용할 수 있습니다.
<!-- index.html -->
<div class="container">
<a href="/link" class="anchor">I’m the anchor</a>
<div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
position: relative;
}
.anchored {
position: absolute;
}
이러한 솔루션은 종종 이상적이지 않습니다. JavaScript가 필요하거나 추가 마크업을 도입해야 합니다. CSS 앵커 포지셔닝 API는 테더링 요소용 CSS API를 제공하여 이 문제를 해결하는 것을 목표로 합니다. 다른 요소의 위치와 크기를 기반으로 한 요소의 위치와 크기를 지정하는 수단을 제공합니다.
브라우저 지원
Chrome Canary의 '실험용 웹 플랫폼 기능' 플래그 아래에서 CSS 앵커 위치 지정 API를 사용해 볼 수 있습니다. 이 플래그를 사용 설정하려면 Chrome Canary를 열고 chrome://flags
를 방문합니다. 그런 다음 '실험용 웹 플랫폼 기능' 플래그를 사용 설정합니다.
Oddbird팀에서 개발 중인 폴리필도 있습니다. github.com/oddbird/css-anchor-positioning에서 저장소를 확인하세요.
다음을 사용하여 고정 지원을 확인할 수 있습니다.
@supports(anchor-name: --foo) {
/* Styles... */
}
이 API는 아직 실험 단계에 있으며 변경될 수 있습니다. 이 도움말에서는 중요한 부분을 개략적으로 설명합니다. 또한 현재 구현은 CSS 워킹 그룹 사양과 완전히 동기화되지 않습니다.
문제
이 작업이 필요한 이유는 무엇인가요? 대표적인 사용 사례는 도움말 또는 도움말과 유사한 환경을 만드는 것입니다. 이 경우 툴팁을 참조하는 콘텐츠에 연결하는 것이 좋습니다. 요소를 다른 요소에 연결하는 방법이 필요한 경우가 많습니다. 또한 페이지와 상호작용해도 테더링이 끊어지지 않습니다(예: 사용자가 UI를 스크롤하거나 크기를 조절하는 경우).
또 다른 문제는 테더링된 요소가 화면에 계속 표시되도록 하는 경우입니다. 예를 들어 도움말을 열었을 때 뷰포트 경계로 잘리는 경우를 들 수 있습니다. 이는 사용자에게 만족스러운 환경을 제공하지 못할 수 있습니다. 도움말이 조정되도록 하려면
현재 솔루션
현재 이 문제에 접근하는 방법에는 여러 가지가 있습니다.
먼저 기본적인 '앵커 래핑' 접근 방식을 살펴보겠습니다. 두 요소를 모두 가져와 컨테이너로 래핑합니다. 그런 다음 position
를 사용하여 앵커를 기준으로 도움말을 배치할 수 있습니다.
<div class="containing-block">
<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
</div>
.containing-block {
position: relative;
}
.tooltip {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
}
컨테이너를 이동해도 대부분의 항목은 원하는 위치에 유지됩니다.
앵커의 위치를 알고 있거나 어떻게든 추적할 수 있는 경우 다른 접근 방식을 사용할 수 있습니다. 맞춤 속성을 사용하여 도움말에 전달할 수 있습니다.
<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
--anchor-width: 120px;
--anchor-top: 40vh;
--anchor-left: 20vmin;
}
.anchor {
position: absolute;
top: var(--anchor-top);
left: var(--anchor-left);
width: var(--anchor-width);
}
.tooltip {
position: absolute;
top: calc(var(--anchor-top));
left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
transform: translate(-50%, calc(-100% - 10px));
}
하지만 앵커의 위치를 모르는 경우는 어떻게 해야 하나요? JavaScript로 개입해야 할 수 있습니다. 다음 코드와 같이 할 수 있지만, 이제 스타일이 CSS에서 JavaScript로 유출되기 시작합니다.
const setAnchorPosition = (anchored, anchor) => {
const bounds = anchor.getBoundingClientRect().toJSON();
for (const [key, value] of Object.entries(bounds)) {
anchored.style.setProperty(`--${key}`, value);
}
};
const update = () => {
setAnchorPosition(
document.querySelector('.tooltip'),
document.querySelector('.anchor')
);
};
window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);
이로 인해 몇 가지 질문이 제기됩니다.
- 스타일은 언제 계산하나요?
- 스타일은 어떻게 계산하나요?
- 스타일은 얼마나 자주 계산하나요?
문제가 해결되었나요? 사용 사례에 적합할 수 있지만 한 가지 문제가 있습니다. Google 솔루션이 적응하지 못합니다. 응답하지 않습니다. 고정된 요소가 뷰포트로 인해 잘리면 어떻게 되나요?
이제 이에 반응할지 여부와 방법을 결정해야 합니다. 해야 할 질문과 결정의 수가 늘어나기 시작합니다. 한 요소를 다른 요소에 고정하기만 하면 됩니다. 이상적인 경우 솔루션이 주변 환경에 맞게 조정되고 반응합니다.
이러한 문제를 완화하려면 JavaScript 솔루션을 사용하는 것이 좋습니다. 이렇게 하면 프로젝트에 종속 항목을 추가하는 비용이 발생하며 사용 방법에 따라 성능 문제가 발생할 수 있습니다. 예를 들어 일부 패키지는 requestAnimationFrame
를 사용하여 위치를 올바르게 유지합니다. 즉, 개발자와 팀은 패키지와 패키지의 구성 옵션을 숙지해야 합니다. 따라서 질문과 결정이 줄어들지 않고 대신 달라질 수 있습니다. 이는 CSS 앵커 포지셔닝의 '이유' 중 하나입니다. 이를 통해 위치를 계산할 때 성능 문제를 고려하지 않아도 됩니다.
이 문제에 많이 사용되는 패키지인 'floating-ui'를 사용하는 코드는 다음과 같습니다.
import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';
const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')
const updatePosition = () => {
computePosition(anchor, tooltip, {
placement: 'top',
middleware: [offset(10), flip()]
})
.then(({x, y}) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`
})
})
};
const clean = autoUpdate(anchor, tooltip, updatePosition);
이 코드를 사용하는 데모에서 앵커의 위치를 다시 지정해 보세요.
'도움말'이 예상대로 작동하지 않을 수 있습니다. y축의 표시 영역 외부로 이동할 때는 반응하지만 x축의 표시 영역 외부로 이동할 때는 반응하지 않습니다. 문서를 살펴보면 적합한 솔루션을 찾을 수 있습니다.
하지만 프로젝트에 적합한 패키지를 찾는 데는 많은 시간이 걸릴 수 있습니다. 추가 결정사항이며 원하는 대로 작동하지 않으면 불편을 겪을 수 있습니다.
앵커 위치 지정 사용
CSS 앵커 포지셔닝 API를 입력합니다. CSS에서 스타일을 유지하고 결정해야 할 횟수를 줄이는 것이 목표입니다. 동일한 결과를 얻으려는 것이지만 목표는 개발자 환경을 개선하는 것입니다.
- JavaScript가 필요하지 않습니다.
- 브라우저가 안내에 따라 가장 적합한 위치를 찾도록 합니다.
- 서드 파티 종속 항목 없음
- 래퍼 요소가 없습니다.
- 최상위 레이어에 있는 요소와 함께 작동합니다.
위에서 해결하려고 했던 문제를 다시 만들어 해결해 보겠습니다. 대신 닻이 있는 배의 비유를 사용하세요. 이는 고정된 요소와 고정점을 나타냅니다. 물은 포함된 블록을 나타냅니다.
먼저 앵커를 정의하는 방법을 선택해야 합니다. CSS 내에서 앵커 요소에 anchor-name
속성을 설정하면 됩니다. dashed-ident 값을 허용합니다.
.anchor {
anchor-name: --my-anchor;
}
또는 anchor
속성을 사용하여 HTML에서 앵커를 정의할 수 있습니다. 속성 값은 앵커 요소의 ID입니다. 이렇게 하면 암시적 앵커가 생성됩니다.
<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>
앵커를 정의한 후에는 anchor
함수를 사용할 수 있습니다. anchor
함수는 세 가지 인수를 사용합니다.
- 앵커 요소: 사용할 앵커의
anchor-name
입니다. 또는 값을 생략하여implicit
앵커를 사용할 수 있습니다. HTML 관계를 통해 정의하거나anchor-name
값이 있는anchor-default
속성을 사용하여 정의할 수 있습니다. - 앵커 측면: 사용하려는 위치의 키워드입니다.
top
,right
,bottom
,left
,center
등이 될 수 있습니다. 또는 비율을 전달할 수도 있습니다. 예를 들어 50% 는center
와 같습니다. - 대체: 길이 또는 비율을 허용하는 선택적 대체 값입니다.
anchor
함수를 고정된 요소의 인셋 속성 (top
, right
, bottom
, left
또는 이에 상응하는 논리적 값)의 값으로 사용합니다. calc
에서 anchor
함수를 사용할 수도 있습니다.
.boat {
bottom: anchor(--my-anchor top);
left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}
/* alternative with anchor-default */
.boat {
anchor-default: --my-anchor;
bottom: anchor(top);
left: calc(anchor(center) - (var(--boat-size) * 0.5));
}
center
inset 속성은 없으므로 고정된 요소의 크기를 알고 있는 경우 calc
를 사용하는 것이 좋습니다. translate
를 사용하지 않는 이유는 무엇인가요? 다음을 사용할 수 있습니다.
.boat {
anchor-default: --my-anchor;
bottom: anchor(top);
left: anchor(center);
translate: -50% 0;
}
하지만 브라우저는 고정된 요소의 변환된 위치를 고려하지 않습니다. 이는 위치 대체 및 자동 위치 지정을 고려할 때 그 중요성이 분명해집니다.
위에서 맞춤 속성 --boat-size
가 사용된 것을 확인할 수 있습니다. 그러나 고정된 요소의 크기를 고정점의 크기에 기반하도록 하려면 해당 크기에 액세스할 수도 있습니다. 직접 계산하는 대신 anchor-size
함수를 사용할 수 있습니다. 예를 들어 배를 닻의 너비의 4배로 만들려면 다음 단계를 따르세요.
.boat {
width: calc(4 * anchor-size(--my-anchor width));
}
anchor-size(--my-anchor height)
를 사용하여 높이에도 액세스할 수 있습니다. 이를 사용하여 축의 크기를 설정하거나 둘 다 설정할 수 있습니다.
absolute
배치로 요소에 고정하려면 어떻게 해야 하나요? 요소가 형제일 수 없다는 규칙이 적용됩니다. 이 경우 relative
위치 지정이 있는 컨테이너로 앵커를 래핑할 수 있습니다. 그런 다음 앵커링할 수 있습니다.
<div class="anchor-wrapper">
<a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>
앵커를 드래그하면 배가 따라오는 이 데모를 확인하세요.
스크롤 위치 추적
경우에 따라 앵커 요소가 스크롤 컨테이너 내에 있을 수 있습니다. 동시에 고정된 요소가 해당 컨테이너 외부에 있을 수도 있습니다. 스크롤은 레이아웃과는 다른 스레드에서 발생하므로 이를 추적하는 방법이 필요합니다. anchor-scroll
속성을 사용하면 됩니다. 고정된 요소에서 이 속성을 설정하고 추적할 앵커의 값을 지정합니다.
.boat { anchor-scroll: --my-anchor; }
모서리에 있는 체크박스로 anchor-scroll
를 켜거나 끌 수 있는 이 데모를 사용해 보세요.
하지만 이 비유는 약간 어색합니다. 이상적인 상황에서는 배와 닻이 모두 물에 있기 때문입니다. 또한 Popover API와 같은 기능은 관련 요소를 가까이에 유지할 수 있도록 지원합니다. 하지만 앵커 위치 지정은 최상위 레이어에 있는 요소에 작동합니다. 이는 API의 주요 이점 중 하나인 여러 흐름에서 요소를 테더링할 수 있다는 점입니다.
도움말이 있는 앵커가 있는 스크롤 컨테이너가 있는 이 데모를 살펴보세요. 팝오버인 도움말 요소는 앵커와 함께 배치되지 않을 수 있습니다.
하지만 팝오버가 각 앵커 링크를 추적하는 방식을 확인할 수 있습니다. 스크롤 컨테이너의 크기를 조절하면 위치가 자동으로 업데이트됩니다.
게재순위 대체 및 자동 게재순위 지정
이때 앵커 위치 지정 기능이 한 단계 업그레이드됩니다. position-fallback
는 제공된 대체 값 집합을 기반으로 고정된 요소의 위치를 지정할 수 있습니다. 스타일을 사용하여 브라우저를 안내하고 브라우저가 위치를 알아서 처리하도록 합니다.
여기서 일반적인 사용 사례는 앵커 위 또는 아래에 표시되는 툴팁입니다. 이 동작은 도움말이 컨테이너에 의해 잘리는지에 따라 달라집니다. 이 컨테이너는 일반적으로 뷰포트입니다.
지난 데모의 코드를 살펴보면 사용 중인 position-fallback
속성이 있음을 알 수 있습니다. 컨테이너를 스크롤하면 고정된 팝오버가 튀는 것을 볼 수 있습니다. 이는 각 앵커가 뷰포트 경계에 가까워질 때 발생했습니다. 이때 팝오버가 뷰포트에 머물도록 조정하려고 합니다.
명시적 position-fallback
를 만들기 전에 앵커 위치 지정은 자동 위치 지정도 제공합니다. 앵커 함수와 반대 인셋 속성 모두에서 auto
값을 사용하면 무료로 뒤집을 수 있습니다. 예를 들어 bottom
에 anchor
를 사용하는 경우 top
를 auto
로 설정합니다.
.tooltip {
position: absolute;
bottom: anchor(--my-anchor auto);
top: auto;
}
자동 배치의 대안은 명시적 position-fallback
를 사용하는 것입니다. 이렇게 하려면 위치 대체 세트를 정의해야 합니다. 브라우저는 사용할 수 있는 위치를 찾을 때까지 이러한 위치를 확인한 후 해당 위치를 적용합니다. 작동하는 옵션을 찾을 수 없는 경우 정의된 첫 번째 옵션이 기본값으로 사용됩니다.
위쪽에 도움말을 표시한 다음 아래쪽에 표시하려는 position-fallback
는 다음과 같이 표시될 수 있습니다.
@position-fallback --top-to-bottom {
@try {
bottom: anchor(top);
left: anchor(center);
}
@try {
top: anchor(bottom);
left: anchor(center);
}
}
툴팁에 적용하면 다음과 같이 표시됩니다.
.tooltip {
anchor-default: --my-anchor;
position-fallback: --top-to-bottom;
}
anchor-default
를 사용하면 다른 요소에 position-fallback
를 재사용할 수 있습니다. 범위가 지정된 맞춤 속성을 사용하여 anchor-default
를 설정할 수도 있습니다.
배를 다시 사용하는 이 데모를 살펴보세요. position-fallback
가 설정되어 있습니다. 앵커의 위치를 변경하면 배가 컨테이너 내에 있도록 조정됩니다. 본문 패딩을 조정하는 패딩 값도 변경해 보세요. 브라우저가 어떻게 위치를 수정하는지 확인합니다. 컨테이너의 그리드 정렬을 변경하여 위치가 변경됩니다.
이번에는 position-fallback
가 시계 방향으로 위치를 시도하면서 더 상세해졌습니다.
.boat {
anchor-default: --my-anchor;
position-fallback: --compass;
}
@position-fallback --compass {
@try {
bottom: anchor(top);
right: anchor(left);
}
@try {
bottom: anchor(top);
left: anchor(right);
}
@try {
top: anchor(bottom);
right: anchor(left);
}
@try {
top: anchor(bottom);
left: anchor(right);
}
}
예
이제 앵커 배치의 주요 기능을 알게 되었습니다. 도움말 외에도 몇 가지 흥미로운 예를 살펴보겠습니다. 이 예시는 앵커 위치 지정을 사용하는 방법에 대한 아이디어를 얻는 데 도움이 됩니다. 사양을 개선하는 가장 좋은 방법은 여러분과 같은 실제 사용자의 의견을 받는 것입니다.
컨텍스트 메뉴
Popover API를 사용하는 컨텍스트 메뉴로 시작해 보겠습니다. 셰이본이 있는 버튼을 클릭하면 컨텍스트 메뉴가 표시됩니다. 이 메뉴에는 펼칠 수 있는 자체 메뉴가 있습니다.
마크업은 여기서 중요한 부분이 아닙니다. 하지만 popovertarget
를 사용하는 버튼이 세 개 있습니다. 그러면 popover
속성을 사용하는 요소가 세 개 있습니다. 이렇게 하면 JavaScript 없이 컨텍스트 메뉴를 열 수 있습니다. 다음과 같이 표시될 수 있습니다.
<button popovertarget="context">
Toggle Menu
</button>
<div popover="auto" id="context">
<ul>
<li><button>Save to your Liked Songs</button></li>
<li>
<button popovertarget="playlist">
Add to Playlist
</button>
</li>
<li>
<button popovertarget="share">
Share
</button>
</li>
</ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>
이제 position-fallback
를 정의하고 컨텍스트 메뉴 간에 공유할 수 있습니다. 팝오버의 inset
스타일도 설정 해제합니다.
[popovertarget="share"] {
anchor-name: --share;
}
[popovertarget="playlist"] {
anchor-name: --playlist;
}
[popovertarget="context"] {
anchor-name: --context;
}
#share {
anchor-default: --share;
position-fallback: --aligned;
}
#playlist {
anchor-default: --playlist;
position-fallback: --aligned;
}
#context {
anchor-default: --context;
position-fallback: --flip;
}
@position-fallback --aligned {
@try {
top: anchor(top);
left: anchor(right);
}
@try {
top: anchor(bottom);
left: anchor(right);
}
@try {
top: anchor(top);
right: anchor(left);
}
@try {
bottom: anchor(bottom);
left: anchor(right);
}
@try {
right: anchor(left);
bottom: anchor(bottom);
}
}
@position-fallback --flip {
@try {
bottom: anchor(top);
left: anchor(left);
}
@try {
right: anchor(right);
bottom: anchor(top);
}
@try {
top: anchor(bottom);
left: anchor(left);
}
@try {
top: anchor(bottom);
right: anchor(right);
}
}
이렇게 하면 조정 가능한 중첩된 컨텍스트 메뉴 UI를 사용할 수 있습니다. 선택 도구를 사용하여 콘텐츠 위치를 변경해 보세요. 선택한 옵션에 따라 그리드 정렬이 업데이트됩니다. 이는 앵커 위치 지정이 팝오버를 배치하는 방식에 영향을 미칩니다.
포커스 및 팔로우
이 데모에서는 :has()를 가져와 CSS 원시를 결합합니다. 포커스가 있는 input
의 시각적 표시기 전환을 구현하는 것이 목표입니다.
런타임에 새 앵커를 설정하면 됩니다. 이 데모에서는 입력 포커스가 있을 때 범위가 지정된 맞춤 속성이 업데이트됩니다.
#email {
anchor-name: --email;
}
#name {
anchor-name: --name;
}
#password {
anchor-name: --password;
}
:root:has(#email:focus) {
--active-anchor: --email;
}
:root:has(#name:focus) {
--active-anchor: --name;
}
:root:has(#password:focus) {
--active-anchor: --password;
}
:root {
--active-anchor: --name;
--active-left: anchor(var(--active-anchor) right);
--active-top: calc(
anchor(var(--active-anchor) top) +
(
(
anchor(var(--active-anchor) bottom) -
anchor(var(--active-anchor) top)
) * 0.5
)
);
}
.form-indicator {
left: var(--active-left);
top: var(--active-top);
transition: all 0.2s;
}
하지만 어떻게 더 나아갈 수 있을까요? 어떤 형태로든 안내 오버레이에 사용할 수 있습니다. 도움말은 관심 장소 간에 이동하고 콘텐츠를 업데이트할 수 있습니다. 콘텐츠를 크로스페이드할 수 있습니다. display
또는 뷰 전환을 애니메이션으로 표시할 수 있는 개별 애니메이션을 사용할 수 있습니다.
막대 그래프 계산
앵커 위치 지정으로 할 수 있는 또 다른 재미있는 작업은 calc
와 결합하는 것입니다. 차트에 주석을 달 수 있는 팝오버가 있는 차트를 가정해 보겠습니다.
CSS min
및 max
를 사용하여 최대값과 최솟값을 추적할 수 있습니다. CSS는 다음과 같이 표시될 수 있습니다.
.chart__tooltip--max {
left: anchor(--chart right);
bottom: max(
anchor(--anchor-1 top),
anchor(--anchor-2 top),
anchor(--anchor-3 top)
);
translate: 0 50%;
}
차트 값을 업데이트하는 JavaScript와 차트 스타일을 지정하는 CSS가 있습니다. 하지만 앵커 위치 지정은 레이아웃 업데이트를 자동으로 처리합니다.
핸들 크기 조절
하나의 요소에만 고정할 필요는 없습니다. 요소에 여러 앵커를 사용할 수 있습니다. 막대 그래프 예에서 이를 확인할 수 있습니다. 도움말이 차트에 고정된 다음 적절한 막대에 고정되었습니다. 이 개념을 조금 더 확장하면 요소의 크기를 조절하는 데 사용할 수 있습니다.
앵커 포인트를 맞춤 크기 조절 핸들처럼 취급하고 inset
값을 사용할 수 있습니다.
.container {
position: absolute;
inset:
anchor(--handle-1 top)
anchor(--handle-2 right)
anchor(--handle-2 bottom)
anchor(--handle-1 left);
}
이 데모에서는 GreenSock Draggable이 핸들을 Draggable로 만듭니다. 하지만 <img>
요소는 핸들 간의 간격을 채우도록 조정되는 컨테이너를 채우도록 크기가 조절됩니다.
SelectMenu?
마지막은 향후 출시될 기능을 미리 살짝 보여드리려고 합니다. 하지만 포커스를 설정할 수 있는 팝오버를 만들면 이제 앵커 위치 지정이 가능합니다. 스타일 지정 가능한 <select>
요소의 기반을 만들 수 있습니다.
<div class="select-menu">
<button popovertarget="listbox">
Select option
<svg>...</svg>
</button>
<div popover="auto" id="listbox">
<option>A</option>
<option>Styled</option>
<option>Select</option>
</div>
</div>
암시적 anchor
를 사용하면 더 쉽게 할 수 있습니다. 하지만 초보적인 시작을 위한 CSS는 다음과 같을 수 있습니다.
[popovertarget] {
anchor-name: --select-button;
}
[popover] {
anchor-default: --select-button;
top: anchor(bottom);
width: anchor-size(width);
left: anchor(left);
}
Popover API의 기능을 CSS 앵커 포지셔닝과 결합하면 됩니다.
:has()
와 같은 항목을 도입할 때 유용합니다. 엽서를 열어 마커를 회전할 수 있습니다.
.select-menu:has(:open) svg {
rotate: 180deg;
}
다음으로 어디로 가실 건가요? 작동하는 select
를 만들기 위해 필요한 다른 사항이 있나요? 다음 도움말에서 다루겠습니다. 하지만 걱정하지 마세요. 스타일 지정 가능한 선택 요소가 제공될 예정입니다. 계속해서 많은 관심 부탁드립니다.
완료되었습니다.
웹 플랫폼이 진화하고 있습니다. CSS 앵커 위치 지정은 UI 컨트롤을 개발하는 방법을 개선하는 데 중요한 부분입니다. 이를 통해 어려운 결정을 내릴 필요가 없습니다. 하지만 이전에는 할 수 없었던 작업도 할 수 있게 됩니다. 예를 들어 <select>
요소의 스타일을 지정할 수 있습니다. 여러분의 의견을 보내주세요.
사진: CHUTTERSNAP(Unsplash 제공)