컨텍스트(Context): props를 거치지 않고 값 전달하기
컨텍스트(Context): props를 거치지 않고 값 전달하기
여러 단계의 컴포넌트를 거쳐 값을 전달해야 할 때, 매번 props로 내려보내는 건 번거로워요. 중간 컴포넌트들이 그 값에 관심이 없는데도 일일이 넘겨야 하니까요. 이런 'prop-drilling'을 피하는 방법이 컨텍스트(context) 예요.
컨텍스트를 쓰면 부모 컴포넌트가 가진 값을, 중간 컴포넌트를 통하지 않고 자식 컴포넌트가 직접 접근할 수 있어요. 부모가 값을 설정하고, 자식이 그 값을 꺼내 쓰는 구조예요.
기본 사용법
Svelte 5.40부터 createContext 로 [get, set, has] 트리플 함수를 만들어 사용할 수 있어요. 부모에서 set 으로 값을 설정하고, 자식에서 get 으로 꺼내요.
/// file: context.ts
import { createContext } from 'svelte';
interface User {
name: string;
}
export const [getUserContext, setUserContext] = createContext<User>();
<!--- file: App.svelte --->
<script>
import Parent from './Parent.svelte';
import Child from './Child.svelte';
</script>
<Parent>
<Child />
</Parent>
<!--- file: Parent.svelte --->
<script>
import { setUserContext } from './context';
let { children } = $props();
setUserContext({ name: 'world' });
</script>
{@render children()}
<!--- file: Child.svelte --->
<script>
import { getUserContext } from './context';
const user = getUserContext();
</script>
<h1>hello {user.name}, inside Child.svelte</h1>
⚠️
createContext는 5.40 버전에 추가됐어요. 더 이른 버전을 쓰고 있다면setContext/getContext를 사용해야 해요.
이 방식은 Parent.svelte 가 Child.svelte 를 직접 알지 못하고, children snippet 안에서만 렌더링할 때 특히 유용해요.
setContext / getContext
createContext 의 대안으로 setContext 와 getContext 를 직접 쓸 수도 있어요. 부모가 setContext(key, value) 로 설정하고:
<!--- file: Parent.svelte --->
<script>
import { setContext } from 'svelte';
setContext('my-context', 'hello from Parent.svelte');
</script>
자식이 getContext 로 꺼내요:
<!--- file: Child.svelte --->
<script>
import { getContext } from 'svelte';
const message = getContext('my-context');
</script>
<h1>{message}, inside Child.svelte</h1>
키('my-context')와 컨텍스트 자체는 어떤 JavaScript 값이든 될 수 있어요.
⚠️
createContext가 더 나은 타입 안전성을 제공하고 키가 필요 없어서 더 권장돼요.
setContext/getContext 외에도 Svelte는 hasContext 와 getAllContexts 함수를 제공해요.
컨텍스트와 상태 함께 쓰기
컨텍스트에 반응형 상태를 담을 수 있어요. 상태 객체를 컨텍스트로 내려보내면 여러 자식이 같은 상태를 공유해요.
<!--- file: App.svelte --->
<script>
import { setCounter } from './context.ts';
import Child from './Child.svelte';
let counter = $state({
count: 0
});
setCounter(counter);
</script>
<button onclick={() => counter.count += 1}>
increment
</button>
<Child />
<Child />
<Child />
<button onclick={() => counter.count = 0}>
reset
</button>
<!--- file: Child.svelte --->
<script>
import { getCounter } from './context.ts';
const counter = getCounter();
</script>
<p>{counter.count}</p>
/// file: context.ts
import { createContext } from 'svelte';
interface Counter {
count: number;
}
export const [getCounter, setCounter] = createContext<Counter>();
다만 counter 를 재할당하면 연결이 끊어져요. 객체를 통째로 다시 넣는 대신:
<button onclick={() => counter = { count: 0 } }>
reset
</button>
속성을 바꿔야 해요:
<button onclick={() => counter.count = 0}>
reset
</button>
Svelte가 잘못 쓰면 경고해 줘요. 마찬가지로 컨텍스트로 원시값을 전달하려면 함수를 사용해야 해요.
컨텍스트와 함께 컴포넌트 마운트
특정 컨텍스트를 가진 상태로 컴포넌트를 마운트하려면, 컴포넌트를 렌더링하기 전에 컨텍스트를 설정하는 wrapper 컴포넌트를 만들어요. 컴포넌트 테스트나 mount 로 컨텍스트를 제공해야 하는 상황에 유용해요. 5.49 버전부터는:
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import { setUserContext } from './context';
import MyComponent from './MyComponent.svelte';
test('MyComponent', () => {
function Wrapper(...args) {
setUserContext({ name: 'Bob' });
return MyComponent(...args);
}
const component = mount(Wrapper, {
target: document.body
});
expect(document.body.innerHTML).toBe('<h1>Hello Bob!</h1>');
unmount(component);
});
이 방식은 hydrate 나 render 에서도 동작해요. wrapper가 설정한 컨텍스트는 그 마운트된 컴포넌트 트리에만 적용되고, mount/hydrate/render 호출마다 별도 wrapper 인스턴스가 생기므로 다른 마운트된 컴포넌트로 새지 않아요.
전역 상태 대체
많은 컴포넌트가 공유하는 상태를 모듈에 두고 필요할 때마다 import 하고 싶은 유혹이 들어요.
/// file: state.svelte.js
export const myGlobalState = $state({
user: {
// ...
}
// ...
});
대부분의 경우 이건 괜찮지만 한 가지 위험이 있어요. 서버 사이드 렌더링 중에 이 상태를 변형하면(권장되진 않지만 가능은 해요), 그 데이터가 다음 사용자에게 보일 수 있어요. 컨텍스트는 요청 간에 공유되지 않으므로 이 문제를 해결해 줘요.
더 알아보기
- 공식 문서 (1차): Context
- 이어지는 개념: 컴포넌트 props($props) · 반응형 상태($state)