블록체인

Solidity 실습: Mapping으로 사용자별 데이터 저장

sungjae0309 2026. 5. 24. 00:17

1. 서론 

이전 실습에서는 Foundry 기본 예제인 Counter 컨트랙트를 수정하고 테스트해보았다.

Counter는 컨트랙트 전체에서 하나의 숫자 값을 저장하고, 그 값을 증가시키는 단순한 구조였다. 

 

이번 실습에선 한 단계 더 나아가 사용자 별로 값을 따로 저장하는 스마트 컨트랙트를 만들어보고자 한다. 

실제 dApp에서는 사용자마다 포인트, 예치금, 투표 여부, 권한 등이 다르게 관리되어야 하기 때문에, 이번 실습은 실제 스마트 컨트랙트 구조에 조금 더 가까운 내용이다.


2. 실습 순서

  1. 서론
  2. 실습 순서 
  3. 실제 스마트 컨트랙트에서의 활용 
  4. src/UserStorage.sol 파일 생성
  5. 코드 이해
       - mapping
       - address
       - msg.sender
  6. 함수 설명 
       - setMyValue()
       - getMyValue()
       - getValueOf()
  7. test/UserStorage.t.sol 테스트 파일 생성
  8. 테스트 코드 이해 
  9. 테스트 결과
  10. 실제 서비스로 확장한다면?  
  11. 마무리 

3. 실제 스마트 컨트랙트에서의 활용

이해를 위해 오늘 실습하는 주제가 실생활에서 어떻게 활용되는지 설명을 하겠다. 

예를 들어 블록체인 기반 출석 체크 서비스를 만든다고 해보자. 

사용자가 출석하면 포인트를 지급하려 한다. 

  • 이런 식으로 사용자 별로 포인트를 다르게 저장하려 한다. 
성재 지갑 주소 → 100 포인트
민수 지갑 주소 → 50 포인트
지수 지갑 주소 → 200 포인트
  • 블록체인에서는 wallet_address를 사용하면 된다. 
wallet_address | point
-------------- | -----
0xAAA...       | 100
0xBBB...       | 50
0xCCC...       | 200
  • 따라서 Solidity에선 이런 구조를 mapping으로 표현할 수 있다.
mapping(address => uint256) public points;
  • 즉, 이번 실습에서 만드는 UserStorage는 실제 서비스에서 다음과 같은 기능으로 확장될 수 있다
지갑 주소 → 포인트
지갑 주소 → 예치금
지갑 주소 → 투표 여부
지갑 주소 → NFT 보유 여부
지갑 주소 → 관리자 권한 여부

4. src/UserStorage.sol 파일 생성

  • src폴더에 아래와 같은 코드를 새로 생성한다.
  • 이 컨트랙트는 사용자의 지갑 주소를 기준으로 숫자 값을 저장하고 조회하는 기능을 가진다.
// SPDX-License-Identifier: UNLICENSED 
pragma solidity ^0.8.13;

contract UserStorage { 
  mapping(address => unit256) private values; 

  function setMyValue(uint256 _value) public{
    values[msg.sender] = _value;
  }

  function getMyValue() public view returns (uint256) {
    return values[msg.sender];
  }

  function getValueOf(address _user) public view returns (uint256) {
  return values[_user];
  }

}

5. 코드 분석 

5-1. 기본 선언부

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
  1. // SPDX-License-Identifier: UNLICENSED 
    • 이 Solidity 코드의 라이선스를 아직 공개 라이선스로 지정하지 않았다는 뜻이다.
    • 개인이나 비공개 코드의 경우에 해당한다.
    • 만약 오픈소스로 공개할 코드라면 보통 다음과 같이 적는다.
      • // SPDX-License-Identifier: MIT 
  2. pragma solidity ^0.8.13;
    • Solidity 컴파일러 버전을 지정한다.
    • Solidity 0.8.13 이상 버전에서 컴파일하되, 0.9.0 미만 버전에서만 컴파일하라는 뜻이다
    • 여기서 ^ 가 중요한데, 0.8.13, 0.8.20, 0.8.25 버전은 가능하지만 0.9.0이나 1.0.0 같은 버전은 문법이나 동작 방식이 달라지기 때문에 되지 않는다. 

5-2. Contract 선언 

contract UserStorage {

 

이건 UserStorage라는 스마트 컨트랙트를 만든다는 뜻이다.


5-3. Mapping 선언

mapping(address => uint256) private values;

Mapping은 쉽게 말해 사용자 지갑의 주소값을 넣었을 때 그 주소에 저장된 숫자 값을 가져오는 역할이다.

Mapping 이해를  위한 비유 

예를 들어서 JavaScript에서 아래와 같은 객체를 만들 수 있다.

const values = {
  "성재": 10,
  "민수": 50
};

 

 

그러면 아래와 같이 값을 찾을 수 있다.

values["성재"] // 10
values["민수"] // 50

 

 

 

Solidity의 mapping도 비슷하다. 다만 사용자의 이름 대신 지갑 주소를 key로 쓰는 것이다.

즉, 이런 식으로 저장된다는 뜻이다. 

0xAAA... 주소 → 10
0xBBB... 주소 → 50
0xCCC... 주소 → 100

5-4. private이란? 

private은 이 변수를 컨트랙트 외부에서 직접 읽을 수 없게 한다는 뜻이다.

private values;

 

 

외부에서 아래처럼 접근할 수 없다. 

values[userAddress]

 

 

그래서 값을 읽고 싶다면 아래 함수처럼 읽어야 한다.

getMyValue()
getValueOf(address _user)

6. 함수 기능 및 설명 

현재 UserStorage 컨트랙트에는 아래와 같이 함수가 3개 있다. 

setMyValue(uint256 _value)
getMyValue()
getValueOf(address _user)

6-1. setMyValue 함수 

이 함수는 내 주소에 값을 저장하는 함수이다. 

function setMyValue(uint256 _value) public {
    values[msg.sender] = _value;
}

 

 

 

예를 들어 사용자 A가 다음과 같이 호출을 했다고 해보자

setMyValue(0xAAA...)

 

 

그러면 컨트랙트는 이 함수를 호출한 사람의 지갑 주소를 확인한다.

그 역할을 하는 것이 msg.sender다. 

따라서 정리하면 아래와 같이 된다. 

msg.sender = 사용자의 지갑 주소
_value = 0xAAA...

 


6-2. getMyValue() 함수

이 함수는 내 지갑 주소에 저장된 값을 조회하는 함수다. 

사용자A 주소에 0xAAA... 이 저장되어 있다면, 사용자A가 getMyValue()를 호출했을 때 0xAAA에 저장되어 있는 10이 반환된다. 

function getMyValue() public view returns (uint256) {
    return values[msg.sender];
}

 

 

따라서 사용자 별로 다른 값이 반환된다.

A가 호출 → A 주소에 저장된 값 반환
B가 호출 → B 주소에 저장된 값 반환

 

 

이 함수에는 view가 붙어 있는데, view는 상태를 변경하지 않고 읽기만 한다는 뜻이다. 

즉, 이 함수는 블록체인에 저장된 값을 바꾸지 않고 조회만 한다. 

public view returns (uint256)

6-3. getValueOf() 

이 함수는 특정 지갑 주소에 저장된 값을 조회한다.

즉, 다른 사람의 지갑 주소를 알고 있다면 해당 주소의 값을 조회할 수 있는 기능이다. 

function getValueOf(address _user) public view returns (uint256) {
    return values[_user];
}

6-4. 세 함수 비교 

사실 가장 기본적인 기능만 놓고 보면 setMyValue()와 getMyValue()만 있어도 된다.

하지만 getValueOf()가 있으면 특정 주소의 값을 확인할 수 있어서 테스트와 디버깅에 편리하다.

함수 역할 필요한 이유
setMyValue() 내 값 저장 사용자가 자기 주소에 값을 저장
getMyValue() 내 값 조회 사용자가 자신의 값을 쉽게 확인
getValueOf() 특정 주소 값 조회 다른 주소의 값이나 테스트 결과를 확인

 


7. 테스트 파일 생성 

test 폴더 안에 다음 파일을 생성한다.

test/UserStorage.t.sol

 

그리고 아래 코드를 작성한다. 

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {UserStorage} from "../src/UserStorage.sol";

contract UserStorageTest is Test {
    UserStorage public userStorage;

    address public userA = address(1);
    address public userB = address(2);

    function setUp() public {
        userStorage = new UserStorage();
    }

    function test_SetMyValue() public {
        userStorage.setMyValue(10);
        assertEq(userStorage.getMyValue(), 10);
    }

    function test_DifferentUsersHaveDifferentValues() public {
        vm.prank(userA);
        userStorage.setMyValue(10);

        vm.prank(userB);
        userStorage.setMyValue(50);

        assertEq(userStorage.getValueOf(userA), 10);
        assertEq(userStorage.getValueOf(userB), 50);
    }

    function test_DefaultValueIsZero() public view {
        assertEq(userStorage.getValueOf(userA), 0);
    }
}

8. 테스트 파일 분석 

8-1. import 

Foundry에서 제공하는 테스트 도구를 가져오는 코드이다.

여기서 forge-std는 표준 라이브러리로 그 안에 있는 Test.sol에는 테스트에 필요한 여러 기능들이 들어 있다. 

import {Test} from "forge-std/Test.sol";

 

 

그리고 테스트 코드에서 사용한 다른 기능들도 이 라이브러리에서 나온다.

assertEq(...)
vm.prank(...)

8-2. 테스트 컨트랙트 선언

UserStorageTest라는 테스트용 컨트랙트를 만드는 코드이다. 

contract UserStorageTest is Test {

 

 

여기서 is Test란, UserStorageTest가 Foundry의 Test 기능을 상속 받는 것을 의미 한다.

is Test

따라서 테스트 컨트랙트 안에서 Foundry가 제공하는 여러 테스트 도구들을 사용할 수 있는 것이다. 

  • assertEq()
  • vm.prank() 

8-3. 가짜 사용자 주소 생성 

이 줄은 테스트에서 사용할 가짜 사용자 주소를 만든 것이다. 

테스트용 주소를 만들어서 이 주소가 userA고 이 주소가 userB라고 가정하는 것이다. 

address public userA = address(1);
address public userB = address(2);

 

 

실제 이더리움 주소는 보통 20바이트, 즉 40개의 16진수 문자로 이루어져 있다.

address public userA = 0x0000000000000000000000000000000000000aAa;

8-4. setUp()

setUP()은 각 테스트 함수가 실행 되기 전 자동으로 먼저 실행되는 함수이다. 

지금 내 코드에는 아래와 같이 3개의 테스트 함수가 있다.

test_SetMyValue()
test_DifferentUsersHaveDifferentValues()
test_DefaultValueIsZero()

 

 

그러면 Foundry는 대략 이렇게 실행한다.

테스트마다 새롭게 준비하는 것이다. 

setUp() 실행
test_SetMyValue() 실행

setUp() 실행
test_DifferentUsersHaveDifferentValues() 실행

setUp() 실행
test_DefaultValueIsZero() 실행

 

 

이게 필요한 이유는, 테스트끼리 서로 영향을 주면 안 되기 때문이다.

예를 들어 첫 번째 테스트에서 값을 저장했는데 그 상태가 두 번째 테스트에 그대로 남아있다면 결과가 꼬일 수 있다. 

그래서 각 테스트마다 새로운 UserStorage 컨트랙트를 만든다. 

userStorage = new UserStorage();

8-5. 첫번째 테스트: test_SetMyValue()

이 테스트는 가장 기본적인 저장/조회를 한다. 

검증 내용: 현재 호출자가 10을 저장하면, 다시 조회했을 때 10이 나오는가?

function test_SetMyValue() public {
    userStorage.setMyValue(10);
	
    assertEq(userStorage.getMyValue(), 10);
}

 

 

흐름은 아래와 같다. 

  1. userStorage.setMyValue(10) 실행
  2. 현재 호출자의 주소에 10 저장 
  3. userStorage.getMyValue() 실행
  4. 반환값이 10인지 확인 

 

assertEq()는 두 값이 같은지 확인하는 함수이다. 

뜻은, userStorage.getMyValue()의 결과가 10과 같은가? 이다. 

같으면 통과, 다르면 실패


8-6. 두번째 테스트: test_DifferentUsersHaveDifferentValues() 

이 테스트는 userA와 userB가 서로 다른 값을 가질 수 있는지 확인하는 것이다.

검증 내용: userA는 10, userB는 50을 각각 따로 저장할 수 있는가? 

function test_DifferentUsersHaveDifferentValues() public {
    vm.prank(userA);
    userStorage.setMyValue(10);

    vm.prank(userB);
    userStorage.setMyValue(50);

    assertEq(userStorage.getValueOf(userA), 10);
    assertEq(userStorage.getValueOf(userB), 50);
}

 

 

vm.prank()는 Foundry 테스트에서 제공하는 기능이다.

뜻은, 다음 함수 호출 한 번은 userA가 호출한 것처럼 실행하라는 뜻이다. 

이렇게 해석하면 된다. userA가 setMyValue(10)을 호출한 것처럼 실행하라

vm.prank(userA);
userStorage.setMyValue(10);

 

 

마지막으로 이제 값이 잘 저장됐는지를 확인한다.

assertEq(userStorage.getValueOf(userA), 10);
assertEq(userStorage.getValueOf(userB), 50);

 

8-7. 세번째 테스트: testDefaultValueIsZero() 

이 테스트는 아직 값을 저장하지 않은 주소의 기본값이 0인지를 확인한다. 

검증 내용: 아직 값을 저장하지 않은 주소의 기본값은 0인가?

 

 

Solidity에선 mapping(address => uint256)은 특정 주소에 값을 저장하지 않으면 기본값을 반환한다.

uint256의 기본값은 0이다. 

function test_DefaultValueIsZero() public view {
    assertEq(userStorage.getValueOf(userA), 0);
}

 

 

즉, 아무 값도 넣지 않은 상태에서 아래를 호출하면 0이 나와야 한다. 

따라서 이 테스트는 userA가 아직 아무 값도 저장하지 않았을 때 0이 나오는가? 를 확인한다. 

userStorage.getValueOf(userA)

 


8.8 테스트 파일 한 눈에 정리 

  1. import {Test} from "forge-std/Test.sol";
    → Foundry 테스트 기능 가져오기

  2. import {UserStorage} from "../src/UserStorage.sol";
    → 테스트할 컨트랙트 가져오기

  3. contract UserStorageTest is Test
    → Foundry 테스트 기능을 상속받는 테스트 컨트랙트 만들기

  4. UserStorage public userStorage;
    → 테스트할 컨트랙트 변수 선언

  5. address(1), address(2)
    → 테스트용 가짜 사용자 주소 만들기

  6. setUp()
    → 각 테스트 전에 새 UserStorage 컨트랙트 생성

  7. test_SetMyValue()
    → 내 주소에 값 저장/조회 테스트

  8. test_DifferentUsersHaveDifferentValues()
    → userA, userB가 서로 다른 값을 가질 수 있는지 테스트

  9. test_DefaultValueIsZero()
    → 저장하지 않은 주소의 기본값이 0인지 테스트

9. 테스트 결과 

테스트를 해본 결과 처음에 컴파일 오류가 났는데, 내가 uint256이라고 적여야 할 것을 unit256이라고 철자를 잘못 적어서 오류가 발생했다.

 

수정 후에 다시 컴파일 하니 모두 PASS가 된 것을 확인할 수 있다. 


10. 실제 서비스로 확장한다면?

이번 UserStorage 코드는 단순히 숫자 값을 저장하지만, 실제 서비스에서는 의미 있는 데이터로 확장할 수 있다.

 

10-1. 투표 시스템 

한 사람이 한 번만 투표하게 하는 시스템이라면 다음과 같이 사용할 수 있다.

  • mapping(address => bool) public hasVoted; 사용자 별로 투표했는지 여부를 저장하는 공간
  • 지갑 주소 -> 투표 여부
  • 예를 들면,
    A 주소 -> true
    B 주소 -> false
    C 주소 -> true  
mapping(address => bool) public hasVoted; 

function vote() public {
    require(hasVoted[msg.sender] == false, "Already voted");
    hasVoted[msg.sender] = true;
}
  • function vote() public 는 투표 함수이다. 
    누군가 이 함수를 호출하면, "투표한다"고 생각하면 된다. 
    public이므로 외부에서 호출이 가능하며, 사용자가 지갑을 연결하고 프론트엔드에서 투표 버튼을 누르면 이 함수가 실행되는 구조다 
  • require는 조건이 참이면 에러 메시지가 실행되는 게 아니라, 조건이 거짓일 때 에러 메시지가 실행된다 
    현재 호출한 사람이 아직 투표하지 않았으면 계속 진행하고, 이미 투표했다면 에러를 발생시키고 함수를 중단한다.
    • 예를 들어 내가 투표 버튼을 누른다면 => msg.sender = 내 지갑 주소 
      그래서 내 지갑 주소의 투표 여부가 false인지 확인하라는 뜻이 된다. 
      만약 아직 안했다면 hasVoted[내 주소] == false가 참이므로 다음 줄로 넘어간다. 
      그러나, 이미 투표했다면 true가 되어서 조건이 거짓이 되고 함수가 중단되어 "Already voted"가 뜬다. 

11. 마무리 

이번 실습에서는 UserStoragae 컨트랙트를 만들고, 사용자 별로 값을 저장하는 방법을 학습했다. 

핵심은 다음과 같다. 

  • Solidity에선 사용자를 지갑 주소로 구분한다.
  • msg.sender는 현재 함수를 호출한 사람의 주소이다. 
  • mapping(address => uint256)은 지갑 주소별 숫자 저장소이다. 

'블록체인' 카테고리의 다른 글

Hash란  (0) 2026.05.26
Gas fee란  (0) 2026.05.25
Solidity 실습: Foundry  (0) 2026.05.22
Solidity 실습 준비: MacOS에서 Foundry 개발 환경 설치  (0) 2026.05.20
Solidity 알아보기 ( 공식 문서 참고 )  (0) 2026.05.19