> ## Documentation Index
> Fetch the complete documentation index at: https://docs.optimism.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Solidity로 OP Stack과 Ethereum 간 통신하기

> 서로 통신할 수 있는 OP Stack 및 Ethereum용 Solidity 컨트랙트를 작성하는 방법을 알아봅니다.

이 튜토리얼에서는 서로 통신할 수 있는 OP Stack 및 Ethereum용 Solidity 컨트랙트를 작성하는 방법을 설명합니다.
여기서는 OP Stack의 컨트랙트를 사용하여 Ethereum의 컨트랙트에 "greeting" 변수를 설정하고, 그 반대 방향으로도 설정합니다.
간단한 예제이지만, 동일한 기법을 사용하여 두 체인 간에 어떤 종류의 메시지든 보낼 수 있습니다.

이 튜토리얼에서는 실제로 스마트 컨트랙트를 배포하지 않습니다.
대신 OP Stack과 Ethereum에 이미 배포된 기존 컨트랙트를 재사용합니다.
튜토리얼 후반부에서는 이 컨트랙트들이 정확히 어떻게 동작하는지 배우게 되므로, 같은 패턴을 따라 직접 컨트랙트를 배포할 수 있습니다.

> [!NOTE]
>
> OP Stack과 Ethereum 간에 토큰을 브리징하는 방법만 찾고 있습니까?
> [viem으로 OP Stack에 ERC-20 토큰 브리징하기](https://docs.optimism.io/ko/app-developers/tutorials/bridging/cross-dom-bridge-erc20.md) 튜토리얼을 확인하십시오.

## 메시지 전달 기본 사항

OP Stack은 `CrossDomainMessenger`라는 스마트 컨트랙트를 사용하여 OP Stack과 Ethereum 간에 메시지를 전달합니다.
두 체인 모두 이 컨트랙트의 버전(`L1CrossDomainMessenger` 및 `L2CrossDomainMessenger`)을 가지고 있습니다.
Ethereum에서 OP Stack으로 보낸 메시지는 내부적으로 자동 릴레이됩니다.
OP Stack에서 Ethereum으로 보낸 메시지는 Ethereum에서 두 번째 트랜잭션을 통해 명시적으로 릴레이해야 합니다.
메시지 전달에 대한 자세한 내용은 [L1과 L2 간 데이터 전송](https://docs.optimism.io/ko/app-developers/guides/bridging/messaging.md) 가이드를 참조하십시오.

## 의존성

* [node](https://nodejs.org/en/)
* [pnpm](https://pnpm.io/installation)

## Sepolia 및 OP Sepolia에서 ETH 받기

이 튜토리얼에서는 Sepolia에서 OP Sepolia로 메시지를 보내는 방법을 설명합니다.
두 testnet 모두에서 약간의 ETH가 필요합니다.

> [!NOTE]
>
> [이 faucet](https://sepoliafaucet.com)을 사용하여 Sepolia에서
> ETH를 받을 수 있습니다.
>
> [Superchain Faucet](https://console.optimism.io/faucet?utm_source=op-docs\&utm_medium=docs)
> 을 사용하여 OP Sepolia에서 ETH를 받을 수 있습니다.

## 컨트랙트 검토

Sepolia와 OP Sepolia에 이미 배포된 두 개의 컨트랙트, 즉 `Greeter` 컨트랙트를 사용하게 됩니다.
L1 `Greeter` 컨트랙트의 소스 코드는 [Etherscan의 여기](https://sepolia.etherscan.io/address/0x31A6Dd971306bb72f2ffF771bF30b1B98dB8B2c5#code)에서 검토할 수 있습니다.
L2 `Greeter` 컨트랙트의 소스 코드는 [Etherscan의 여기](https://testnet-explorer.optimism.io/address/0x5DE8a2957eddb140567fF90ba5d57bc9769f3055#code)에서 검토할 수 있습니다.
두 컨트랙트의 소스 코드는 완전히 동일합니다.

원한다면 지금 두 컨트랙트의 소스 코드를 검토해도 좋습니다.
이 튜토리얼에서는 아래의 [동작 방식](#how-it-works) 섹션에서 이 컨트랙트들이 어떻게 동작하는지 자세히 설명합니다.

## L1 Greeter와 상호작용하기

먼저 L1 `Greeter` 컨트랙트를 사용하여 L2 `Greeter` 컨트랙트에 greeting을 설정합니다.
L1 `Greeter` 컨트랙트에 직접 트랜잭션을 보내면, 이 컨트랙트가 `L1CrossDomainMessenger`에 L2 `Greeter` 컨트랙트로 메시지를 보내도록 요청합니다.
몇 분만 지나면 L2 `Greeter` 컨트랙트에 해당 greeting이 설정된 것을 확인할 수 있습니다.

**1. Etherscan에 연결**

L1 `Greeter` 컨트랙트를 통해 L2 `Greeter` 컨트랙트로 메시지를 보내려면 `sendGreeting` 함수를 호출해야 합니다.
간단하게 하기 위해 Etherscan에서 컨트랙트와 직접 상호작용합니다.
[Sepolia Etherscan의 L1 `Greeter` 컨트랙트](https://sepolia.etherscan.io/address/0x31A6Dd971306bb72f2ffF771bF30b1B98dB8B2c5#writeContract)를 열고 "Connect to Web3" 버튼을 클릭합니다.

**2. greeting 보내기**

"sendGreeting" 함수 옆의 필드에 greeting을 입력하고 "Write" 버튼을 클릭합니다.
원하는 어떤 greeting이든 사용할 수 있습니다.

**3. 몇 분 기다리기**

메시지가 L2에 도달하기까지 몇 분 정도 걸립니다.
기다리는 동안 잠시 쉬어도 좋습니다.

> [!NOTE]
>
> viem을 사용하면 L1과 L2 간 모든 메시지의 상태를 프로그래밍 방식으로 확인할 수 있습니다.
> 이 튜토리얼 후반부에서는 viem과 `waitToProve` 함수를 사용하여 다양한 메시지 상태를 기다리는 방법을 배우게 됩니다.
> 이 함수는 L1에서 L2로 메시지가 릴레이될 때까지 기다리는 데에도 사용할 수 있습니다.

**4. L2 Greeter 확인**

몇 분 후 L2 `Greeter` 컨트랙트의 greeting이 설정한 greeting으로 변경된 것을 확인할 수 있습니다.
[OP Sepolia Etherscan의 L2 `Greeter` 컨트랙트](https://testnet-explorer.optimism.io/address/0x5DE8a2957eddb140567fF90ba5d57bc9769f3055#readContract)를 열고 "Read Contract" 버튼을 클릭합니다.
"greeting" 함수 옆의 필드에 주소를 붙여 넣고 "Query" 버튼을 클릭합니다.
L1에서 보낸 메시지가 표시되어야 합니다.

> [!NOTE]
>
> 아직 메시지가 보이지 않습니까?
> 조금 더 기다려야 할 수도 있습니다.
> L1에서 트리거된 L2 트랜잭션은 일반적으로 1분 이내에 처리되지만, 가끔 약간 지연될 수 있습니다.

## L2 Greeter와 상호작용하기

이제 L2 `Greeter` 컨트랙트를 사용하여 L1 `Greeter` 컨트랙트에 greeting을 설정합니다.
L2 `Greeter` 컨트랙트에 직접 트랜잭션을 보내면, 이 컨트랙트가 `L2CrossDomainMessenger`에 L1 `Greeter` 컨트랙트로 메시지를 보내도록 요청합니다.
이전 단계와 달리 L2에서 L1으로의 메시지는 직접 릴레이해야 합니다.
이를 위해 Sepolia에서 증명 트랜잭션 하나와 릴레이 트랜잭션 하나, 총 두 개의 트랜잭션을 보냅니다.

**1. Etherscan에 연결**

이전과 마찬가지로 L2 `Greeter` 컨트랙트를 통해 L1 `Greeter` 컨트랙트로 메시지를 보내려면 `sendGreeting` 함수를 호출해야 합니다.
[OP Sepolia Etherscan의 L2 `Greeter` 컨트랙트](https://testnet-explorer.optimism.io/address/0x5DE8a2957eddb140567fF90ba5d57bc9769f3055#writeContract)를 열고 "Connect to Web3" 버튼을 클릭합니다.

**2. greeting 보내기**

"sendGreeting" 함수 옆의 필드에 greeting을 입력하고 "Write" 버튼을 클릭합니다.
원하는 어떤 greeting이든 사용할 수 있습니다.

> [!NOTE]
>
> 방금 보낸 트랜잭션의 트랜잭션 해시를 복사하십시오.
> 다음 몇 단계에서 필요합니다.
> 나중에 트랜잭션 해시를 쉽게 복사할 수 있도록 이 탭을 열어 두어도 좋습니다.

**3. 데모 프로젝트 생성**

viem을 사용하여 메시지를 증명하고 L1으로 릴레이합니다.

```bash theme={null}
mkdir cross-dom
cd cross-dom
pnpm init
pnpm add viem@^2.51.0
```

**4. 환경 변수 추가**

개인 키와 트랜잭션 해시를 환경 변수로 설정합니다.

```bash theme={null}
export TUTORIAL_PRIVATE_KEY=0x...
export TUTORIAL_TRANSACTION_HASH=0x...
```

**5. Node REPL에서 스크립트 실행**

`node`로 Node.js REPL을 시작하고 다음 스크립트를 붙여 넣어 크로스 도메인 메시지를 모니터링, 증명 및 최종 확정합니다.

```js theme={null}
const { createPublicClient, http, createWalletClient } = require('viem');
const { optimismSepolia, sepolia } = require('viem/chains');
const { publicActionsL1, publicActionsL2, walletActionsL1, walletActionsL2, getWithdrawals, isSuperGameType } = require('viem/op-stack');
const { privateKeyToAccount } = require('viem/accounts');

const l1Provider = createPublicClient({
  chain: sepolia,
  transport: http('https://eth-sepolia.g.alchemy.com/v2/your-key')
}).extend(publicActionsL1());

const l2Provider = createPublicClient({
  chain: optimismSepolia,
  transport: http('https://opt-sepolia.g.alchemy.com/v2/your-key')
}).extend(publicActionsL2());

const account = privateKeyToAccount(process.env.TUTORIAL_PRIVATE_KEY);

const l1Wallet = createWalletClient({
  account,
  chain: sepolia,
  transport: http('https://eth-sepolia.g.alchemy.com/v2/your-key')
}).extend(walletActionsL1());

const l2Wallet = createWalletClient({
  account,
  chain: optimismSepolia,
  transport: http('https://opt-sepolia.g.alchemy.com/v2/your-key')
}).extend(walletActionsL2());

const receipt = await l2Provider.getTransactionReceipt({
  hash: process.env.TUTORIAL_TRANSACTION_HASH
});
const withdrawalBlock = await l2Provider.getBlock({
  blockNumber: receipt.blockNumber
});
const respectedGameType = await l1Provider.readContract({
  abi: [{
    inputs: [],
    name: 'respectedGameType',
    outputs: [{ type: 'uint32' }],
    stateMutability: 'view',
    type: 'function'
  }],
  address: l2Provider.chain.contracts.portal[l1Provider.chain.id].address,
  functionName: 'respectedGameType'
});
const proveContext = isSuperGameType(Number(respectedGameType))
  ? { l2Timestamp: withdrawalBlock.timestamp }
  : {};

console.log('Waiting for message to be provable...');
await l1Provider.getWithdrawalStatus({
  ...proveContext,
  receipt,
  targetChain: l2Provider.chain
});

console.log('Proving message...');
const { game, withdrawal } = await l1Provider.waitToProve({
  ...proveContext,
  receipt,
  targetChain: l2Provider.chain
});

const proveArgs = await l2Provider.buildProveWithdrawal({
  account,
  game,
  withdrawal
});

await l1Wallet.proveWithdrawal(proveArgs);

console.log('Waiting for message to be relayable...');
await l1Provider.waitToFinalize({
  targetChain: l2Provider.chain,
  withdrawalHash: withdrawal.withdrawalHash
});

console.log('Relaying message...');
await l1Wallet.finalizeWithdrawal({
  targetChain: l2Wallet.chain,
  withdrawal
});

console.log('Message relayed!');
```

**6. L1 greeting 확인**

최종 확정 후 [Sepolia Etherscan의 L1 `Greeter` 컨트랙트](https://sepolia.etherscan.io/address/0x31A6Dd971306bb72f2ffF771bF30b1B98dB8B2c5#readContract)를 엽니다.
greeting이 L2에서 보낸 메시지로 업데이트되었는지 확인합니다.

## 동작 방식

축하합니다! L1에서 L2로, 그리고 L2에서 L1으로 메시지를 성공적으로 보냈습니다.
이 섹션에서는 `Greeter` 컨트랙트가 어떻게 동작하는지 설명하므로, 같은 패턴을 따라 직접 컨트랙트를 배포할 수 있습니다.
다행히 두 `Greeter` 컨트랙트는 완전히 동일하므로 모든 것이 어떻게 맞물려 동작하는지 쉽게 확인할 수 있습니다.

### Messenger 변수

`Greeter` 컨트랙트에는 현재 체인의 `CrossDomainMessenger` 컨트랙트를 추적하는 `MESSENGER` 변수가 있습니다.
사용할 네트워크의 `CrossDomainMessenger` 컨트랙트 주소는 [컨트랙트 주소 페이지](https://docs.optimism.io/ko/op-mainnet/network-information/op-addresses.md)에서 확인하십시오.

```solidity theme={null}
address public immutable MESSENGER;
```

### 상대 Greeter 변수

`Greeter` 컨트랙트에는 다른 체인의 `Greeter` 컨트랙트를 추적하는 `OTHER_GREETER` 변수도 있습니다.
L1에서는 이 변수가 L2 `Greeter` 컨트랙트의 주소로 설정되며, 그 반대도 마찬가지입니다.

```solidity theme={null}
address public immutable OTHER_GREETER;
```

### Greetings 매핑

`Greeter` 컨트랙트는 사용자가 보낸 여러 greeting을 `greetings` 매핑에 저장하여 추적합니다.
매핑을 사용하므로 이 컨트랙트는 여러 사용자의 greeting을 동시에 추적할 수 있습니다.

```solidity theme={null}
mapping(address => string) public greetings;
```

### 생성자

`Greeter`에는 `MESSENGER` 및 `OTHER_GREETER` 변수를 설정하는 간단한 생성자가 있습니다.

```solidity theme={null}
constructor(address messenger, address otherGreeter) {
  MESSENGER = messenger;
  OTHER_GREETER = otherGreeter;
}
```

### sendGreeting 함수

`sendGreeting` 함수는 `Greeter` 컨트랙트에서 가장 중요한 함수입니다.
앞서 양방향으로 메시지를 보낼 때 호출한 함수가 바로 이것입니다.
이 함수는 `CrossDomainMessenger` 컨트랙트에 있는 `sendMessage` 함수를 사용하여 다른 체인의 `Greeter` 컨트랙트로 메시지를 보내는 일만 합니다.

```solidity theme={null}
function sendGreeting(string calldata newGreeting) external payable {
  ICrossDomainMessenger(MESSENGER).sendMessage(
    OTHER_GREETER,
    abi.encodeWithSelector(Greeter.setGreeting.selector, msg.sender, newGreeting),
    100_000
  );
}
```

### setGreeting 함수

`setGreeting` 함수는 실제로 greeting을 설정하는 함수입니다.
이 함수는 다른 체인의 `CrossDomainMessenger` 컨트랙트에 의해 호출됩니다.
이 함수는 `CrossDomainMessenger` 컨트랙트만 호출할 수 있는지 명시적으로 확인합니다.
또한 `CrossChainMessenger`가 해당 메시지가 다른 체인의 `Greeter` 컨트랙트에서 왔다고 알려 주는지도 확인합니다.
마지막으로 `greetings` 매핑에 greeting을 설정합니다.

```solidity theme={null}
function setGreeting(address sender, string calldata newGreeting) external {
  require(msg.sender == MESSENGER, "Only messenger");
  require(
    ICrossDomainMessenger(MESSENGER).xDomainMessageSender() == OTHER_GREETER,
    "Only remote greeter"
  );
  greetings[sender] = newGreeting;
}
```

> [!NOTE]
>
> 이 함수에 있는 두 개의 `require` 문은 중요합니다.
> 이 문이 없으면 누구나 이 함수를 호출하여 greeting을 원하는 값으로 설정할 수 있습니다.
> 직접 작성하는 스마트 컨트랙트에서도 비슷한 패턴을 따를 수 있습니다.

## 결론

방금 Sepolia와 OP Sepolia에서 서로 통신할 수 있는 Solidity 컨트랙트를 작성하는 방법을 배웠습니다.
같은 패턴을 따라 Ethereum과 OP Stack에서 서로 통신할 수 있는 컨트랙트를 작성할 수 있습니다.

```solidity theme={null}
interface ICrossDomainMessenger {
  function sendMessage(address target, bytes calldata message, uint32 gasLimit) external;
  function xDomainMessageSender() external view returns (address);
}
```

이러한 종류의 크로스체인 통신은 다양한 이유로 유용합니다.
예를 들어 [Standard Bridge](https://docs.optimism.io/ko/app-developers/guides/bridging/standard-bridge.md) 컨트랙트는 이와 동일한 시스템을 사용하여 Ethereum과 OP Stack 간에 ETH 및 ERC-20 토큰을 브리징합니다.
크로스체인 통신을 활용하는 좋은 방법 중 하나는 대부분의 무거운 작업을 OP Stack에서 수행한 다음, 공유할 중요한 결과가 있을 때만 Ethereum으로 메시지를 보내는 것입니다.
이렇게 하면 OP Stack의 낮은 gas 비용을 활용하면서도 필요할 때는 Ethereum을 사용할 수 있습니다.
